Skip to content

Commit 50b332f

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

10 files changed

Lines changed: 633 additions & 7 deletions

File tree

bin/cliCredentials.js

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
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+
let { target, ...tokens } = credentials;
51+
52+
if (target) {
53+
if (!target.endsWith('/')) {
54+
target += '/';
55+
}
56+
allCredentials.targets = allCredentials.targets || {};
57+
allCredentials.targets[target] = tokens;
58+
allCredentials.last_target = target;
59+
} else {
60+
// Fallback for when target is not provided (shouldn't happen with new login)
61+
Object.assign(allCredentials, tokens);
62+
}
63+
64+
fs.ensureDirSync(getCredentialsDir());
65+
fs.writeJsonSync(getCredentialsFile(), allCredentials, { spaces: 2, mode: 0o600 });
66+
} catch (err) {
67+
console.error(`Error saving credentials file: ${err.message}`);
68+
}
69+
}
70+
71+
/**
72+
* Deletes the credentials for a specific target or all if no target provided.
73+
* @param {string} [target] The target URL to logout from.
74+
*/
75+
function clearCredentials(target) {
76+
const credentialsFile = getCredentialsFile();
77+
try {
78+
if (target) {
79+
if (!target.endsWith('/')) {
80+
target += '/';
81+
}
82+
const allCredentials = loadCredentials();
83+
if (allCredentials && allCredentials.targets) {
84+
if (allCredentials.targets[target]) {
85+
delete allCredentials.targets[target];
86+
} else {
87+
// try without trailing slash just in case
88+
const altTarget = target.replace(/\/$/, '');
89+
delete allCredentials.targets[altTarget];
90+
}
91+
92+
if (allCredentials.last_target === target || allCredentials.last_target === target.replace(/\/$/, '')) {
93+
const remainingTargets = Object.keys(allCredentials.targets);
94+
allCredentials.last_target = remainingTargets.length > 0 ? remainingTargets[0] : null;
95+
}
96+
fs.writeJsonSync(credentialsFile, allCredentials, { spaces: 2, mode: 0o600 });
97+
console.log(`Logged out from ${target}`);
98+
}
99+
} else if (fs.existsSync(credentialsFile)) {
100+
fs.removeSync(credentialsFile);
101+
console.log('Logged out from all targets');
102+
}
103+
} catch (err) {
104+
console.error(`Error clearing credentials file: ${err.message}`);
105+
}
106+
}
107+
108+
module.exports = {
109+
loadCredentials,
110+
saveCredentials,
111+
clearCredentials,
112+
};

bin/cliOperations.js

Lines changed: 42 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,19 @@ 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) {
133+
const lookupKey = target.resolvedTarget.endsWith('/') ? target.resolvedTarget : target.resolvedTarget + '/';
134+
tokens =
135+
allCredentials.targets[lookupKey] ?? allCredentials.targets[target.resolvedTarget.replace(/\/$/, '')] ?? null;
136+
} else if (allCredentials.operation_token) {
137+
tokens = allCredentials;
138+
}
139+
140+
if (tokens && tokens.operation_token) {
141+
options.headers.Authorization = `Bearer ${tokens.operation_token}`;
142+
}
113143
}
114144
if (req.cborEncode) {
115145
options.headers['Content-Type'] = 'application/cbor';
@@ -141,7 +171,13 @@ async function cliOperations(req) {
141171
process.exit(1);
142172
}
143173

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

146182
return responseData;
147183
} 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');
@@ -25,6 +26,8 @@ copy-db <source> <target> - Copies a database from source path to target p
2526
dev <path> - Run the application in dev mode with debugging, foreground logging, no auth
2627
install - Install harperdb
2728
<api-operation> <param>=<value> - Run an API operation and return result to the CLI, not all operations are supported
29+
login [username] - Login to a remote or local Harper instance
30+
logout [target] - Logout from Harper and clear saved JWT
2831
register - Register harperdb
2932
renew-certs - Generate a new set of self-signed certificates
3033
restart - Restart the harperdb background process
@@ -81,6 +84,16 @@ async function harper() {
8184
.then(() => 'Your instance of Harper is up to date!');
8285
case SERVICE_ACTIONS_ENUM.STATUS:
8386
return require('./status.js')();
87+
case SERVICE_ACTIONS_ENUM.LOGIN: {
88+
const target = process.argv[3];
89+
const username = process.argv[4];
90+
const { login } = require('./login.js');
91+
return login(target, username);
92+
}
93+
case SERVICE_ACTIONS_ENUM.LOGOUT: {
94+
const target = process.argv[3];
95+
return require('./logout.js')(target);
96+
}
8497
case SERVICE_ACTIONS_ENUM.RENEWCERTS:
8598
return require('../security/keys.js')
8699
.renewSelfSigned()

0 commit comments

Comments
 (0)