Skip to content

Commit e79c15a

Browse files
committed
feat: harper login and logout
This saves jwts, per target, in your ~/.harper directory.
1 parent c1d7e3c commit e79c15a

10 files changed

Lines changed: 604 additions & 8 deletions

File tree

bin/cliCredentials.js

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
'use strict';
2+
3+
const path = require('node:path');
4+
const os = require('node:os');
5+
const fs = require('fs-extra');
6+
7+
function getHomeDir() {
8+
let homeDir = undefined;
9+
try {
10+
homeDir = os.homedir();
11+
} catch {
12+
// could get here in android
13+
homeDir = process.env.HOME;
14+
}
15+
return homeDir;
16+
}
17+
18+
function getCredentialsFile() {
19+
return path.join(getHomeDir(), '.harper', 'credentials.json');
20+
}
21+
22+
function getCredentialsDir() {
23+
return path.join(getHomeDir(), '.harper');
24+
}
25+
26+
/**
27+
* Loads the JWT credentials from the ~/.harper/credentials.json file.
28+
* @returns {Object|null} The credentials object or null if not found.
29+
*/
30+
function loadCredentials() {
31+
const credentialsFile = getCredentialsFile();
32+
if (fs.existsSync(credentialsFile)) {
33+
try {
34+
return fs.readJsonSync(credentialsFile);
35+
} catch (err) {
36+
console.error(`Error reading credentials file: ${err.message}`);
37+
return null;
38+
}
39+
}
40+
return null;
41+
}
42+
43+
/**
44+
* Saves the JWT credentials to the ~/.harper/credentials.json file.
45+
* @param {Object} credentials The credentials object containing operation_token and refresh_token.
46+
*/
47+
function saveCredentials(credentials) {
48+
try {
49+
const allCredentials = loadCredentials() || {};
50+
const { target, ...tokens } = credentials;
51+
52+
if (target) {
53+
allCredentials.targets = allCredentials.targets || {};
54+
allCredentials.targets[target] = tokens;
55+
allCredentials.last_target = target;
56+
} else {
57+
// Fallback for when target is not provided (shouldn't happen with new login)
58+
Object.assign(allCredentials, tokens);
59+
}
60+
61+
fs.ensureDirSync(getCredentialsDir());
62+
fs.writeJsonSync(getCredentialsFile(), allCredentials, { spaces: 2, mode: 0o600 });
63+
} catch (err) {
64+
console.error(`Error saving credentials file: ${err.message}`);
65+
}
66+
}
67+
68+
/**
69+
* Deletes the credentials for a specific target or all if no target provided.
70+
* @param {string} [target] The target URL to logout from.
71+
*/
72+
function clearCredentials(target) {
73+
const credentialsFile = getCredentialsFile();
74+
try {
75+
if (target) {
76+
const allCredentials = loadCredentials();
77+
if (allCredentials && allCredentials.targets && allCredentials.targets[target]) {
78+
delete allCredentials.targets[target];
79+
if (allCredentials.last_target === target) {
80+
const remainingTargets = Object.keys(allCredentials.targets);
81+
allCredentials.last_target = remainingTargets.length > 0 ? remainingTargets[0] : null;
82+
}
83+
fs.writeJsonSync(credentialsFile, allCredentials, { spaces: 2, mode: 0o600 });
84+
console.log(`Logged out from ${target}`);
85+
}
86+
} else if (fs.existsSync(credentialsFile)) {
87+
fs.removeSync(credentialsFile);
88+
console.log('Logged out from all targets');
89+
}
90+
} catch (err) {
91+
console.error(`Error clearing credentials file: ${err.message}`);
92+
}
93+
}
94+
95+
module.exports = {
96+
loadCredentials,
97+
saveCredentials,
98+
clearCredentials,
99+
};

bin/cliOperations.js

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const { packageDirectory } = require('../components/packageComponent.ts');
1111
const { encode } = require('cbor-x');
1212
const { getHdbPid } = require('../utility/processManagement/processManagement.js');
1313
const { initConfig, getConfigPath } = require('../config/configUtils.js');
14+
const { loadCredentials } = require('./cliCredentials.js');
1415

1516
const OP_ALIASES = { deploy: 'deploy_component', package: 'package_component' };
1617

@@ -56,15 +57,29 @@ function buildRequest() {
5657
return req;
5758
}
5859

60+
/**
61+
* Resolves the target URL from various sources.
62+
* @param {Object} req The request object.
63+
* @param {Object} allCredentials Stored credentials.
64+
* @returns {string|null} The resolved target URL.
65+
*/
66+
function resolveTarget(req, allCredentials) {
67+
return (
68+
req.target ||
69+
process.env.HARPER_CLI_TARGET ||
70+
process.env.CLI_TARGET ||
71+
(allCredentials && allCredentials.last_target)
72+
);
73+
}
74+
5975
/**
6076
* Using a unix domain socket will send a request to hdb operations API server
6177
* @param req
6278
* @returns {Promise<void>}
6379
*/
64-
async function cliOperations(req) {
65-
if (!req.target) {
66-
req.target = process.env.HARPER_CLI_TARGET || process.env.CLI_TARGET;
67-
}
80+
async function cliOperations(req, skipResponseLog = false) {
81+
const allCredentials = loadCredentials();
82+
req.target = resolveTarget(req, allCredentials);
6883
let target;
6984
if (req.target) {
7085
try {
@@ -76,15 +91,17 @@ async function cliOperations(req) {
7691
throw error;
7792
}
7893
}
94+
const resolvedTarget = `${target.protocol}//${target.hostname}${target.port ? ':' + target.port : ''}`;
7995
target = {
8096
protocol: target.protocol,
8197
hostname: target.hostname,
8298
port: target.port,
8399
username: req.username || target.username || process.env.HARPER_CLI_USERNAME || process.env.CLI_TARGET_USERNAME,
84100
password: req.password || target.password || process.env.HARPER_CLI_PASSWORD || process.env.CLI_TARGET_PASSWORD,
85101
rejectUnauthorized: req.rejectUnauthorized,
102+
resolvedTarget,
86103
};
87-
console.error(`Connecting to ${target.protocol}//${target.hostname}:${target.port}`);
104+
console.error(`Connecting to ${resolvedTarget}`);
88105
} else {
89106
// if we aren't doing a targeted operation (like deploy), we initialize the config and verify that local harper
90107
// is running and that we can communicate with it.
@@ -110,6 +127,17 @@ async function cliOperations(req) {
110127
options.headers = { 'Content-Type': 'application/json' };
111128
if (target?.username) {
112129
options.headers.Authorization = `Basic ${Buffer.from(`${target.username}:${target.password}`).toString('base64')}`;
130+
} else if (allCredentials) {
131+
let tokens = null;
132+
if (target && allCredentials.targets && allCredentials.targets[target.resolvedTarget]) {
133+
tokens = allCredentials.targets[target.resolvedTarget];
134+
} else if (allCredentials.operation_token) {
135+
tokens = allCredentials;
136+
}
137+
138+
if (tokens && tokens.operation_token) {
139+
options.headers.Authorization = `Bearer ${tokens.operation_token}`;
140+
}
113141
}
114142
if (req.cborEncode) {
115143
options.headers['Content-Type'] = 'application/cbor';
@@ -141,7 +169,13 @@ async function cliOperations(req) {
141169
process.exit(1);
142170
}
143171

144-
console.log(responseLog);
172+
if (!skipResponseLog) {
173+
console.log(responseLog);
174+
}
175+
176+
if (target) {
177+
responseData.resolvedTarget = target.resolvedTarget;
178+
}
145179

146180
return responseData;
147181
} catch (err) {

bin/harper.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#!/usr/bin/env node
22
'use strict';
33

4+
require('dotenv').config();
45
const fs = require('node:fs');
56
const path = require('node:path');
67
const logger = require('../utility/logging/harper_logger.js');
@@ -23,6 +24,8 @@ copy-db <source> <target> - Copies a database from source path to target p
2324
dev <path> - Run the application in dev mode with debugging, foreground logging, no auth
2425
install - Install harperdb
2526
<api-operation> <param>=<value> - Run an API operation and return result to the CLI, not all operations are supported
27+
login [username] - Login to a remote or local Harper instance
28+
logout [target] - Logout from Harper and clear saved JWT
2629
register - Register harperdb
2730
renew-certs - Generate a new set of self-signed certificates
2831
restart - Restart the harperdb background process
@@ -79,6 +82,16 @@ async function harper() {
7982
.then(() => 'Your instance of Harper is up to date!');
8083
case SERVICE_ACTIONS_ENUM.STATUS:
8184
return require('./status.js')();
85+
case SERVICE_ACTIONS_ENUM.LOGIN: {
86+
const target = process.argv[3];
87+
const username = process.argv[4];
88+
const { login } = require('./login.js');
89+
return login(target, username);
90+
}
91+
case SERVICE_ACTIONS_ENUM.LOGOUT: {
92+
const target = process.argv[3];
93+
return require('./logout.js')(target);
94+
}
8295
case SERVICE_ACTIONS_ENUM.RENEWCERTS:
8396
return require('../security/keys.js')
8497
.renewSelfSigned()

0 commit comments

Comments
 (0)