@wirechunk/cli 0.0.7 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/main.js +1692 -1129
- package/package.json +7 -2
- package/src/commands/bootstrap.ts +0 -85
- package/src/commands/create-extension-version.ts +0 -182
- package/src/commands/create-extension.ts +0 -14
- package/src/commands/create-user.ts +0 -220
- package/src/commands/edit-admin.ts +0 -139
- package/src/commands/ext-dev/get-db-url.ts +0 -36
- package/src/commands/ext-dev/init-db.ts +0 -229
- package/src/core-api/api.ts +0 -6418
- package/src/core-api/mutations/create-extension-version.generated.ts +0 -96
- package/src/core-api/mutations/create-extension-version.graphql +0 -14
- package/src/core-api/operations.ts +0 -23
- package/src/env.ts +0 -85
- package/src/errors.ts +0 -30
- package/src/global-options.ts +0 -6
- package/src/main.ts +0 -136
- package/src/users/permissions.ts +0 -41
- package/src/util.ts +0 -81
- package/tsconfig.build.json +0 -10
- package/tsconfig.json +0 -27
- package/vite.config.ts +0 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirechunk/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -20,11 +20,16 @@
|
|
|
20
20
|
"bin": {
|
|
21
21
|
"wirechunk": "build/main.js"
|
|
22
22
|
},
|
|
23
|
+
"files": [
|
|
24
|
+
"build/main.js"
|
|
25
|
+
],
|
|
23
26
|
"imports": {
|
|
24
27
|
"#api": "./src/core-api/api.ts"
|
|
25
28
|
},
|
|
26
29
|
"dependencies": {
|
|
27
|
-
"
|
|
30
|
+
"@wirechunk/schemas": "^0.0.30",
|
|
31
|
+
"argon2": "^0.44.0",
|
|
32
|
+
"jsonwebtoken": "^9.0.3"
|
|
28
33
|
},
|
|
29
34
|
"devDependencies": {
|
|
30
35
|
"@commander-js/extra-typings": "^14.0.0",
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { cleanTinyId } from '@wirechunk/lib/clean-small-id.ts';
|
|
3
|
-
import { normalizeDomain } from '@wirechunk/lib/domains.ts';
|
|
4
|
-
import { normalizeEmailAddress } from '@wirechunk/lib/emails.ts';
|
|
5
|
-
import { defaultNotificationEmailBodyTemplate } from '@wirechunk/lib/mixer/form-formatting-templates.ts';
|
|
6
|
-
import type { DatabasePool } from 'slonik';
|
|
7
|
-
import { createPool, sql } from 'slonik';
|
|
8
|
-
import type { Env } from '../env.ts';
|
|
9
|
-
import { requireCoreDbUrl } from '../env.ts';
|
|
10
|
-
import type { WithGlobalOptions } from '../global-options.ts';
|
|
11
|
-
import { voidSelectSchema } from '../util.ts';
|
|
12
|
-
|
|
13
|
-
const platformHandleAvailable = async (handle: string, db: DatabasePool) =>
|
|
14
|
-
!(await db.maybeOne(
|
|
15
|
-
sql.type(voidSelectSchema)`select from "Platforms" where "handle" = ${handle}`,
|
|
16
|
-
));
|
|
17
|
-
|
|
18
|
-
const randomString = () => randomUUID().replaceAll('-', '').slice(0, 10);
|
|
19
|
-
|
|
20
|
-
type BootstrapOptions = {
|
|
21
|
-
name: string;
|
|
22
|
-
handle?: string;
|
|
23
|
-
// TODO: Require this be set in the environment instead.
|
|
24
|
-
adminSiteDomain: string;
|
|
25
|
-
emailSendFrom?: string;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
export const bootstrap = async (
|
|
29
|
-
opts: WithGlobalOptions<BootstrapOptions>,
|
|
30
|
-
env: Env,
|
|
31
|
-
): Promise<void> => {
|
|
32
|
-
const db = await createPool(requireCoreDbUrl(env));
|
|
33
|
-
|
|
34
|
-
const platformId = cleanTinyId();
|
|
35
|
-
const name = opts.name.trim();
|
|
36
|
-
let handle = opts.handle?.trim().toLowerCase().replace(/\s+/g, '-');
|
|
37
|
-
if (!handle) {
|
|
38
|
-
handle = name.toLowerCase().replace(/\s+/g, '-');
|
|
39
|
-
if (!(await platformHandleAvailable(handle, db))) {
|
|
40
|
-
handle = `${handle}-${randomString()}`;
|
|
41
|
-
}
|
|
42
|
-
} else if (!(await platformHandleAvailable(handle, db))) {
|
|
43
|
-
console.error(`Handle "${handle}" is already in use`);
|
|
44
|
-
process.exit(1);
|
|
45
|
-
}
|
|
46
|
-
const adminSiteDomain = normalizeDomain(opts.adminSiteDomain, { allowPort: true });
|
|
47
|
-
if (!adminSiteDomain) {
|
|
48
|
-
console.error('Invalid admin site domain');
|
|
49
|
-
process.exit(1);
|
|
50
|
-
}
|
|
51
|
-
let emailSendFrom: string;
|
|
52
|
-
if (opts.emailSendFrom) {
|
|
53
|
-
const normalizeEmailSendFromResult = normalizeEmailAddress(opts.emailSendFrom);
|
|
54
|
-
if (!normalizeEmailSendFromResult.ok) {
|
|
55
|
-
console.error(normalizeEmailSendFromResult.error);
|
|
56
|
-
process.exit(1);
|
|
57
|
-
}
|
|
58
|
-
emailSendFrom = normalizeEmailSendFromResult.value;
|
|
59
|
-
} else {
|
|
60
|
-
emailSendFrom = `site@${adminSiteDomain}`;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
await db.transaction(async (db) => {
|
|
64
|
-
await db.query(
|
|
65
|
-
sql.type(voidSelectSchema)`
|
|
66
|
-
insert into "Platforms" (
|
|
67
|
-
"id",
|
|
68
|
-
"handle",
|
|
69
|
-
"name",
|
|
70
|
-
"defaultFormNotificationEmailBodyTemplate",
|
|
71
|
-
"emailSendFromAddress"
|
|
72
|
-
)
|
|
73
|
-
values (
|
|
74
|
-
${platformId},
|
|
75
|
-
${handle},
|
|
76
|
-
${name},
|
|
77
|
-
${defaultNotificationEmailBodyTemplate},
|
|
78
|
-
${emailSendFrom}
|
|
79
|
-
)
|
|
80
|
-
`,
|
|
81
|
-
);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
console.log(`Created platform ${name} with handle ${handle} (ID ${platformId})`);
|
|
85
|
-
};
|
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises';
|
|
2
|
-
import { resolve } from 'node:path';
|
|
3
|
-
import { parseEnv } from 'node:util';
|
|
4
|
-
import { requireValidExtensionDir } from '@wirechunk/backend-lib/extensions/require-extension-dir.ts';
|
|
5
|
-
import { parseErrorMessage } from '@wirechunk/lib/errors.ts';
|
|
6
|
-
import archiver from 'archiver';
|
|
7
|
-
import { GraphQLClient } from 'graphql-request';
|
|
8
|
-
import { createExtensionVersion as createExtensionVersionRequest } from '../core-api/operations.ts';
|
|
9
|
-
import type { Env } from '../env.ts';
|
|
10
|
-
import { requireApiToken } from '../env.ts';
|
|
11
|
-
import type { WithGlobalOptions } from '../global-options.js';
|
|
12
|
-
import { requireExtensionIdOptionOrEnvVar } from '../util.ts';
|
|
13
|
-
|
|
14
|
-
const bytesFormat = Intl.NumberFormat('en', {
|
|
15
|
-
notation: 'compact',
|
|
16
|
-
style: 'unit',
|
|
17
|
-
unit: 'byte',
|
|
18
|
-
unitDisplay: 'narrow',
|
|
19
|
-
minimumFractionDigits: 1,
|
|
20
|
-
maximumFractionDigits: 1,
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
const ignoreGlobs = [
|
|
24
|
-
'**/.git/**/*',
|
|
25
|
-
'.git/**/*',
|
|
26
|
-
'node_modules/**/*',
|
|
27
|
-
'**/node_modules/**/*',
|
|
28
|
-
'yalc.lock',
|
|
29
|
-
'**/yalc.lock',
|
|
30
|
-
];
|
|
31
|
-
|
|
32
|
-
type CreateExtensionVersionOptions = {
|
|
33
|
-
extensionId?: string;
|
|
34
|
-
versionName: string;
|
|
35
|
-
configFile?: string;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
export const createExtensionVersion = async (
|
|
39
|
-
opts: WithGlobalOptions<CreateExtensionVersionOptions>,
|
|
40
|
-
env: Env,
|
|
41
|
-
): Promise<void> => {
|
|
42
|
-
const extensionId = requireExtensionIdOptionOrEnvVar(opts, env);
|
|
43
|
-
const apiToken = requireApiToken(env);
|
|
44
|
-
const cwd = process.cwd();
|
|
45
|
-
const { manifest } = await requireValidExtensionDir(cwd);
|
|
46
|
-
let enableServer: boolean;
|
|
47
|
-
let enableDb: boolean;
|
|
48
|
-
if (manifest.server) {
|
|
49
|
-
enableServer = !!manifest.server.enable;
|
|
50
|
-
if (manifest.server.database?.enable && manifest.server.enable === false) {
|
|
51
|
-
// Server was explicitly disabled, so don't allow database.
|
|
52
|
-
console.warn('WARNING: Automatically disabling database because server is disabled');
|
|
53
|
-
enableDb = false;
|
|
54
|
-
} else if (manifest.server.database?.enable && !manifest.server.enable) {
|
|
55
|
-
// Server was unspecified, so enable it because database is enabled.
|
|
56
|
-
console.warn('WARNING: Automatically enabling server because database is enabled');
|
|
57
|
-
enableServer = true;
|
|
58
|
-
enableDb = true;
|
|
59
|
-
} else {
|
|
60
|
-
enableDb = !!manifest.server.database?.enable;
|
|
61
|
-
}
|
|
62
|
-
} else {
|
|
63
|
-
enableServer = false;
|
|
64
|
-
enableDb = false;
|
|
65
|
-
}
|
|
66
|
-
let config: Record<string, string> | null = null;
|
|
67
|
-
if (opts.configFile) {
|
|
68
|
-
const configFilePath = resolve(cwd, opts.configFile);
|
|
69
|
-
if (opts.verbose) {
|
|
70
|
-
console.log(`Loading config file ${configFilePath}`);
|
|
71
|
-
}
|
|
72
|
-
const configFile = await readFile(configFilePath, 'utf8');
|
|
73
|
-
try {
|
|
74
|
-
config = parseEnv(configFile) as Record<string, string>;
|
|
75
|
-
} catch (e) {
|
|
76
|
-
console.error(`Failed to parse config file at ${configFilePath}:`, parseErrorMessage(e));
|
|
77
|
-
process.exit(1);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const { versionName } = opts;
|
|
82
|
-
console.log(`Creating extension version ${versionName} (Extension ID ${extensionId})
|
|
83
|
-
Server: ${enableServer ? 'enabled' : 'disabled'}
|
|
84
|
-
Database: ${enableDb ? 'enabled' : 'disabled'}
|
|
85
|
-
Components: ${Object.keys(manifest.components ?? {}).length}`);
|
|
86
|
-
|
|
87
|
-
let extensionVersionId: string;
|
|
88
|
-
let signedUrl: string;
|
|
89
|
-
try {
|
|
90
|
-
const url = `${env.CORE_SERVER_URL}/api`;
|
|
91
|
-
if (opts.verbose) {
|
|
92
|
-
console.log(`POST ${url}`);
|
|
93
|
-
}
|
|
94
|
-
const result = await createExtensionVersionRequest({
|
|
95
|
-
client: new GraphQLClient(url),
|
|
96
|
-
variables: {
|
|
97
|
-
input: {
|
|
98
|
-
extensionId,
|
|
99
|
-
extensionName: manifest.name,
|
|
100
|
-
versionName,
|
|
101
|
-
manifest: JSON.stringify(manifest),
|
|
102
|
-
enableServer,
|
|
103
|
-
enableDb,
|
|
104
|
-
config: config ? JSON.stringify(config) : undefined,
|
|
105
|
-
},
|
|
106
|
-
},
|
|
107
|
-
sessionAuthToken: apiToken,
|
|
108
|
-
});
|
|
109
|
-
if (result.createExtensionVersion.__typename === 'CreateExtensionVersionSuccessResult') {
|
|
110
|
-
extensionVersionId = result.createExtensionVersion.extensionVersion.id;
|
|
111
|
-
signedUrl = result.createExtensionVersion.signedUrl;
|
|
112
|
-
} else {
|
|
113
|
-
console.error(
|
|
114
|
-
`Failed to create an extension version (${result.createExtensionVersion.__typename}):`,
|
|
115
|
-
result.createExtensionVersion.message,
|
|
116
|
-
);
|
|
117
|
-
process.exit(1);
|
|
118
|
-
}
|
|
119
|
-
} catch (e) {
|
|
120
|
-
console.error('Failed to create an extension version:', parseErrorMessage(e));
|
|
121
|
-
process.exit(1);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const archive = archiver('zip', {
|
|
125
|
-
zlib: { level: 6 },
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
archive.on('error', (err) => {
|
|
129
|
-
console.error('Failed to archive:', parseErrorMessage(err));
|
|
130
|
-
process.exit(1);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
archive.on('warning', (err) => {
|
|
134
|
-
console.warn('Archive warning:', err.message);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
let processedBytes = 0;
|
|
138
|
-
let lastProcessedBytes = 0;
|
|
139
|
-
|
|
140
|
-
if (opts.verbose) {
|
|
141
|
-
archive.on('progress', (progress) => {
|
|
142
|
-
processedBytes += progress.fs.processedBytes;
|
|
143
|
-
if (processedBytes >= lastProcessedBytes + 1024 * 100) {
|
|
144
|
-
lastProcessedBytes = processedBytes;
|
|
145
|
-
console.log(`Processed ${bytesFormat.format(processedBytes)}`);
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
archive.glob('**/*', {
|
|
151
|
-
cwd,
|
|
152
|
-
ignore: ignoreGlobs,
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
if (opts.verbose) {
|
|
156
|
-
console.log(`Uploading files at ${cwd}`);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Must not await here because otherwise it doesn't work.
|
|
160
|
-
const finalizePromise = archive.finalize();
|
|
161
|
-
await finalizePromise;
|
|
162
|
-
|
|
163
|
-
const uploadResponse = await fetch(signedUrl, {
|
|
164
|
-
method: 'PUT',
|
|
165
|
-
headers: {
|
|
166
|
-
'Content-Type': 'application/zip',
|
|
167
|
-
},
|
|
168
|
-
duplex: 'half',
|
|
169
|
-
body: archive,
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
if (uploadResponse.status !== 200) {
|
|
173
|
-
console.error('Failed to upload:', uploadResponse.statusText);
|
|
174
|
-
process.exit(1);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
if (opts.verbose) {
|
|
178
|
-
console.log('Uploaded');
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
console.log(`Created version ${versionName} (ID ${extensionVersionId})`);
|
|
182
|
-
};
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { Env } from '../env.ts';
|
|
2
|
-
import type { WithGlobalOptions } from '../global-options.ts';
|
|
3
|
-
|
|
4
|
-
type CreateExtensionOptions = {
|
|
5
|
-
platformId: string;
|
|
6
|
-
name: string;
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
export const createExtension = async (
|
|
10
|
-
_opts: WithGlobalOptions<CreateExtensionOptions>,
|
|
11
|
-
_env: Env,
|
|
12
|
-
): Promise<void> => {
|
|
13
|
-
// TODO
|
|
14
|
-
};
|
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
import { hashPassword, validatePasswordComplexity } from '@wirechunk/backend-lib/passwords.ts';
|
|
2
|
-
import { cleanSmallId } from '@wirechunk/lib/clean-small-id.ts';
|
|
3
|
-
import { normalizeEmailAddress } from '@wirechunk/lib/emails.ts';
|
|
4
|
-
import { createPool, sql, UniqueIntegrityConstraintViolationError } from 'slonik';
|
|
5
|
-
import { z } from 'zod';
|
|
6
|
-
import type { Env } from '../env.ts';
|
|
7
|
-
import { requireCoreDbUrl } from '../env.ts';
|
|
8
|
-
import { detailedUniqueIntegrityConstraintViolationError } from '../errors.ts';
|
|
9
|
-
import type { WithGlobalOptions } from '../global-options.ts';
|
|
10
|
-
import { voidSelectSchema } from '../util.ts';
|
|
11
|
-
|
|
12
|
-
const findOrgResult = z.object({
|
|
13
|
-
platformId: z.string(),
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
// Copied from backend-lib/roles.ts for now because different zod versions.
|
|
17
|
-
const roleSchema = z.object({
|
|
18
|
-
id: z.string(),
|
|
19
|
-
name: z.string(),
|
|
20
|
-
default: z.boolean().optional().default(false),
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
// The schema for the platform roles configuration.
|
|
24
|
-
const rolesSchema = z.array(roleSchema);
|
|
25
|
-
|
|
26
|
-
const findPlatformResult = z.object({
|
|
27
|
-
id: z.string(),
|
|
28
|
-
name: z.string(),
|
|
29
|
-
roles: rolesSchema.nullable(),
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
type CreateUserOptions = {
|
|
33
|
-
email: string;
|
|
34
|
-
password: string;
|
|
35
|
-
firstName: string;
|
|
36
|
-
lastName: string;
|
|
37
|
-
orgId?: string;
|
|
38
|
-
platformId?: string;
|
|
39
|
-
role?: string;
|
|
40
|
-
emailVerified: boolean;
|
|
41
|
-
pending: boolean;
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
export const createUser = async (
|
|
45
|
-
opts: WithGlobalOptions<CreateUserOptions>,
|
|
46
|
-
env: Env,
|
|
47
|
-
): Promise<void> => {
|
|
48
|
-
const db = await createPool(requireCoreDbUrl(env));
|
|
49
|
-
|
|
50
|
-
const normalizeEmailResult = normalizeEmailAddress(opts.email);
|
|
51
|
-
if (!normalizeEmailResult.ok) {
|
|
52
|
-
console.error(normalizeEmailResult.error);
|
|
53
|
-
process.exit(1);
|
|
54
|
-
}
|
|
55
|
-
const email = normalizeEmailResult.value;
|
|
56
|
-
const validatePasswordResult = validatePasswordComplexity(opts.password);
|
|
57
|
-
if (!validatePasswordResult.ok) {
|
|
58
|
-
console.error(validatePasswordResult.error);
|
|
59
|
-
process.exit(1);
|
|
60
|
-
}
|
|
61
|
-
const password = await hashPassword(opts.password);
|
|
62
|
-
const inputRole = opts.role || '';
|
|
63
|
-
const status = opts.pending ? 'Pending' : 'Active';
|
|
64
|
-
const firstName = opts.firstName.trim();
|
|
65
|
-
if (!firstName.length) {
|
|
66
|
-
console.error('First name must not be empty');
|
|
67
|
-
process.exit(1);
|
|
68
|
-
}
|
|
69
|
-
const lastName = opts.lastName.trim();
|
|
70
|
-
if (!lastName.length) {
|
|
71
|
-
console.error('Last name must not be empty');
|
|
72
|
-
process.exit(1);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
let platformId: string | null | undefined = opts.platformId;
|
|
76
|
-
let orgId: string | null | undefined = opts.orgId;
|
|
77
|
-
let orgPrimary = false;
|
|
78
|
-
|
|
79
|
-
try {
|
|
80
|
-
const userId = cleanSmallId();
|
|
81
|
-
await db.transaction(async (db) => {
|
|
82
|
-
if (!platformId) {
|
|
83
|
-
if (!orgId) {
|
|
84
|
-
throw new Error('Either --org-id or --platform-id must be specified');
|
|
85
|
-
}
|
|
86
|
-
platformId = await db.maybeOneFirst(
|
|
87
|
-
sql.type(findOrgResult)`select "platformId" from "Orgs" where "id" = ${orgId}`,
|
|
88
|
-
);
|
|
89
|
-
if (!platformId) {
|
|
90
|
-
throw new Error(`No org found with ID ${orgId}`);
|
|
91
|
-
}
|
|
92
|
-
} else if (orgId) {
|
|
93
|
-
// Verify that the specified org belongs to the specified platform.
|
|
94
|
-
const orgPlatformId = await db.maybeOneFirst(
|
|
95
|
-
sql.type(findOrgResult)`select "platformId" from "Orgs" where "id" = ${orgId}`,
|
|
96
|
-
);
|
|
97
|
-
if (!orgPlatformId) {
|
|
98
|
-
throw new Error(`No org found with ID ${orgId}`);
|
|
99
|
-
}
|
|
100
|
-
if (orgPlatformId !== platformId) {
|
|
101
|
-
throw new Error(`Org ID ${orgId} does not belong to platform ID ${platformId}`);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const platform = await db.maybeOne(
|
|
106
|
-
sql.type(
|
|
107
|
-
findPlatformResult,
|
|
108
|
-
)`select "id", "name", "roles" from "Platforms" where "id" = ${platformId}`,
|
|
109
|
-
);
|
|
110
|
-
|
|
111
|
-
if (!platform) {
|
|
112
|
-
throw new Error(`No platform found with ID ${platformId}`);
|
|
113
|
-
}
|
|
114
|
-
if (opts.verbose) {
|
|
115
|
-
console.log(`Found platform ${platform.name} (ID ${platform.id})`);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Determine the role to use.
|
|
119
|
-
let roleToUse = '';
|
|
120
|
-
if (platform.roles && platform.roles.length) {
|
|
121
|
-
if (inputRole) {
|
|
122
|
-
const roleExists = platform.roles.some((r) => r.name === inputRole);
|
|
123
|
-
if (roleExists) {
|
|
124
|
-
roleToUse = inputRole;
|
|
125
|
-
} else {
|
|
126
|
-
const validRoles = platform.roles.map((r) => r.name).join(', ');
|
|
127
|
-
console.error(
|
|
128
|
-
`Error: Invalid role "${inputRole}". Valid roles for this platform are: ${validRoles}`,
|
|
129
|
-
);
|
|
130
|
-
process.exit(1);
|
|
131
|
-
}
|
|
132
|
-
} else {
|
|
133
|
-
const defaultRole = platform.roles.find((r) => r.default);
|
|
134
|
-
if (defaultRole) {
|
|
135
|
-
roleToUse = defaultRole.name;
|
|
136
|
-
if (opts.verbose) {
|
|
137
|
-
console.log(`No role provided. Using default role: ${roleToUse}`);
|
|
138
|
-
}
|
|
139
|
-
} else {
|
|
140
|
-
// No default role exists and no role was provided.
|
|
141
|
-
if (opts.verbose) {
|
|
142
|
-
console.log(
|
|
143
|
-
'No default role exists and no role was provided. Using empty string for the role.',
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
} else {
|
|
149
|
-
// No roles defined for this platform.
|
|
150
|
-
if (inputRole) {
|
|
151
|
-
console.error('Error: A role was specified but no roles are defined for this platform.');
|
|
152
|
-
process.exit(1);
|
|
153
|
-
}
|
|
154
|
-
if (opts.verbose) {
|
|
155
|
-
console.log('No roles defined for this platform. Using empty string for the role.');
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
if (!orgId) {
|
|
160
|
-
orgId = cleanSmallId();
|
|
161
|
-
await db.maybeOne(
|
|
162
|
-
sql.type(
|
|
163
|
-
voidSelectSchema,
|
|
164
|
-
)`insert into "Orgs" ("id", "platformId") values (${orgId}, ${platform.id})`,
|
|
165
|
-
);
|
|
166
|
-
if (opts.verbose) {
|
|
167
|
-
console.log(`Created org ID ${orgId}`);
|
|
168
|
-
}
|
|
169
|
-
orgPrimary = true;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const user = await db.maybeOne(
|
|
173
|
-
sql.type(voidSelectSchema)`insert into "Users" (
|
|
174
|
-
"id",
|
|
175
|
-
"platformId",
|
|
176
|
-
"email",
|
|
177
|
-
"emailVerified",
|
|
178
|
-
"password",
|
|
179
|
-
"passwordStatus",
|
|
180
|
-
"orgId",
|
|
181
|
-
"role",
|
|
182
|
-
"status",
|
|
183
|
-
"firstName",
|
|
184
|
-
"lastName"
|
|
185
|
-
)
|
|
186
|
-
values (
|
|
187
|
-
${userId},
|
|
188
|
-
${platformId},
|
|
189
|
-
${email},
|
|
190
|
-
${opts.emailVerified},
|
|
191
|
-
${password},
|
|
192
|
-
'Ok',
|
|
193
|
-
${orgId},
|
|
194
|
-
${roleToUse},
|
|
195
|
-
${status},
|
|
196
|
-
${firstName},
|
|
197
|
-
${lastName}
|
|
198
|
-
)`,
|
|
199
|
-
);
|
|
200
|
-
|
|
201
|
-
if (orgPrimary) {
|
|
202
|
-
await db.maybeOne(
|
|
203
|
-
sql.type(
|
|
204
|
-
voidSelectSchema,
|
|
205
|
-
)`update "Orgs" set "primaryUserId" = ${userId} where "id" = ${orgId}`,
|
|
206
|
-
);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
return user;
|
|
210
|
-
});
|
|
211
|
-
console.log(`Created user (ID ${userId})`);
|
|
212
|
-
} catch (e) {
|
|
213
|
-
if (e instanceof UniqueIntegrityConstraintViolationError) {
|
|
214
|
-
console.error(detailedUniqueIntegrityConstraintViolationError(e));
|
|
215
|
-
process.exit(1);
|
|
216
|
-
}
|
|
217
|
-
console.error(e);
|
|
218
|
-
process.exit(1);
|
|
219
|
-
}
|
|
220
|
-
};
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
import { cleanSmallId } from '@wirechunk/lib/clean-small-id.ts';
|
|
2
|
-
import { createPool, sql, UniqueIntegrityConstraintViolationError } from 'slonik';
|
|
3
|
-
import { z } from 'zod';
|
|
4
|
-
import type { Env } from '../env.ts';
|
|
5
|
-
import { requireCoreDbUrl } from '../env.ts';
|
|
6
|
-
import { detailedUniqueIntegrityConstraintViolationError } from '../errors.ts';
|
|
7
|
-
import type { WithGlobalOptions } from '../global-options.ts';
|
|
8
|
-
import {
|
|
9
|
-
grantAllUserPlatformPermissions,
|
|
10
|
-
revokeAllUserPlatformPermissions,
|
|
11
|
-
} from '../users/permissions.ts';
|
|
12
|
-
import { voidSelectSchema } from '../util.ts';
|
|
13
|
-
|
|
14
|
-
const findPlatformAdminSchema = z.object({
|
|
15
|
-
id: z.string(),
|
|
16
|
-
platformId: z.string(),
|
|
17
|
-
active: z.boolean(),
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
const findUserSchema = z.object({
|
|
21
|
-
id: z.string(),
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
type EditAdminOptions = {
|
|
25
|
-
platformId: string;
|
|
26
|
-
userId: string;
|
|
27
|
-
owner?: boolean;
|
|
28
|
-
active?: boolean;
|
|
29
|
-
revokeAllPermissions?: boolean;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
export const editAdmin = async (
|
|
33
|
-
opts: WithGlobalOptions<EditAdminOptions>,
|
|
34
|
-
env: Env,
|
|
35
|
-
): Promise<void> => {
|
|
36
|
-
const db = await createPool(requireCoreDbUrl(env));
|
|
37
|
-
const { platformId, userId, owner, active, revokeAllPermissions } = opts;
|
|
38
|
-
|
|
39
|
-
if (owner && revokeAllPermissions) {
|
|
40
|
-
console.error(
|
|
41
|
-
'Cannot set a user as a platform owner and revoke all permissions at the same time',
|
|
42
|
-
);
|
|
43
|
-
process.exit(1);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
try {
|
|
47
|
-
await db.transaction(async (db) => {
|
|
48
|
-
let platformAdmin = await db.maybeOne(
|
|
49
|
-
sql.type(
|
|
50
|
-
findPlatformAdminSchema,
|
|
51
|
-
)`select "id" from "PlatformAdmins" where "platformId" = ${platformId} and "userId" = ${userId}`,
|
|
52
|
-
);
|
|
53
|
-
if (!platformAdmin) {
|
|
54
|
-
const user = await db.maybeOne(
|
|
55
|
-
sql.type(findUserSchema)`select "id" from "Users" where "id" = ${userId}`,
|
|
56
|
-
);
|
|
57
|
-
if (!user) {
|
|
58
|
-
throw new Error(`User with ID ${userId} not found`);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
if (owner) {
|
|
62
|
-
if (!platformAdmin) {
|
|
63
|
-
platformAdmin = await db.one(
|
|
64
|
-
sql.type(findPlatformAdminSchema)`
|
|
65
|
-
insert into "PlatformAdmins" ("id", "platformId", "userId", "owner", "active")
|
|
66
|
-
values (${cleanSmallId()}, ${platformId}, ${userId}, true, ${active ?? true})
|
|
67
|
-
returning "id", "platformId", "active"
|
|
68
|
-
`,
|
|
69
|
-
);
|
|
70
|
-
} else {
|
|
71
|
-
await db.query(
|
|
72
|
-
sql.type(voidSelectSchema)`
|
|
73
|
-
update "PlatformAdmins"
|
|
74
|
-
set "owner" = true
|
|
75
|
-
where "id" = ${platformAdmin.id}
|
|
76
|
-
`,
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
await grantAllUserPlatformPermissions({ platformAdminId: platformAdmin.id }, db);
|
|
80
|
-
if (opts.verbose) {
|
|
81
|
-
console.log('Set the user as an owner on the platform');
|
|
82
|
-
}
|
|
83
|
-
} else if (owner === false) {
|
|
84
|
-
if (platformAdmin) {
|
|
85
|
-
await db.query(
|
|
86
|
-
sql.type(voidSelectSchema)`
|
|
87
|
-
update "PlatformAdmins"
|
|
88
|
-
set "owner" = false
|
|
89
|
-
where "id" = ${platformAdmin.id}
|
|
90
|
-
`,
|
|
91
|
-
);
|
|
92
|
-
if (opts.verbose) {
|
|
93
|
-
console.log('Removed the user’s owner privileges on the platform');
|
|
94
|
-
}
|
|
95
|
-
} else {
|
|
96
|
-
console.log('This user is not an admin on this platform');
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
if (typeof active === 'boolean') {
|
|
100
|
-
if (platformAdmin) {
|
|
101
|
-
await db.query(
|
|
102
|
-
sql.type(voidSelectSchema)`
|
|
103
|
-
update "PlatformAdmins"
|
|
104
|
-
set "active" = ${active}
|
|
105
|
-
where "id" = ${platformAdmin.id}
|
|
106
|
-
`,
|
|
107
|
-
);
|
|
108
|
-
} else {
|
|
109
|
-
if (active) {
|
|
110
|
-
// Automatically create a platform admin.
|
|
111
|
-
await db.one(
|
|
112
|
-
sql.type(voidSelectSchema)`
|
|
113
|
-
insert into "PlatformAdmins" ("id", "platformId", "userId", "owner", "active")
|
|
114
|
-
values (${cleanSmallId()}, ${platformId}, ${userId}, false, ${active})
|
|
115
|
-
`,
|
|
116
|
-
);
|
|
117
|
-
} else {
|
|
118
|
-
console.log('This user is not an admin on this platform');
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
if (revokeAllPermissions) {
|
|
123
|
-
if (platformAdmin) {
|
|
124
|
-
await revokeAllUserPlatformPermissions({ platformAdminId: platformAdmin.id }, db);
|
|
125
|
-
console.log('Revoked all platform permissions of user');
|
|
126
|
-
} else {
|
|
127
|
-
console.log('This user is not an admin on this platform');
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
});
|
|
131
|
-
} catch (e) {
|
|
132
|
-
if (e instanceof UniqueIntegrityConstraintViolationError) {
|
|
133
|
-
console.error(detailedUniqueIntegrityConstraintViolationError(e));
|
|
134
|
-
process.exit(1);
|
|
135
|
-
}
|
|
136
|
-
console.error(e);
|
|
137
|
-
process.exit(1);
|
|
138
|
-
}
|
|
139
|
-
};
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
|
-
import { readFile, writeFile } from 'node:fs/promises';
|
|
3
|
-
import { resolve } from 'node:path';
|
|
4
|
-
import type { Env } from '../../env.ts';
|
|
5
|
-
import type { WithGlobalOptions } from '../../global-options.js';
|
|
6
|
-
import { getExtensionDbConnectInfo, replaceEnvVar } from '../../util.ts';
|
|
7
|
-
|
|
8
|
-
type GetDbUrlOptions = {
|
|
9
|
-
extensionId?: string;
|
|
10
|
-
dbName?: string;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export const getDbUrl = async (
|
|
14
|
-
opts: WithGlobalOptions<GetDbUrlOptions>,
|
|
15
|
-
env: Env,
|
|
16
|
-
): Promise<void> => {
|
|
17
|
-
const connInfo = getExtensionDbConnectInfo(opts, env);
|
|
18
|
-
const url = connInfo.url.toString();
|
|
19
|
-
const cwd = process.cwd();
|
|
20
|
-
const localEnvFilePath = opts.envMode
|
|
21
|
-
? resolve(cwd, `./.env.${opts.envMode}.local`)
|
|
22
|
-
: resolve(cwd, './.env.local');
|
|
23
|
-
if (opts.verbose) {
|
|
24
|
-
console.log(`Loading file ${localEnvFilePath} to update environment variables`);
|
|
25
|
-
}
|
|
26
|
-
if (existsSync(localEnvFilePath)) {
|
|
27
|
-
const localEnv = await readFile(localEnvFilePath, 'utf8');
|
|
28
|
-
const lines = replaceEnvVar(localEnv.split('\n'), 'DATABASE_URL', url);
|
|
29
|
-
await writeFile(localEnvFilePath, lines.join('\n'));
|
|
30
|
-
} else {
|
|
31
|
-
if (opts.verbose) {
|
|
32
|
-
console.log(`Creating file ${localEnvFilePath}`);
|
|
33
|
-
}
|
|
34
|
-
await writeFile(localEnvFilePath, `DATABASE_URL=${url}\n`);
|
|
35
|
-
}
|
|
36
|
-
};
|