studiocms
Version:
Astro Native CMS for AstroDB. Built from the ground up by the Astro community.
230 lines (205 loc) • 6.98 kB
text/typescript
import { StudioCMSColorwayError, StudioCMSColorwayInfo } from '@withstudiocms/cli-kit/colors';
import { log, note, password, select, text } from '@withstudiocms/effect/clack';
import { eq } from 'drizzle-orm';
import { Effect, runEffect } from '../../../../effect.js';
import { buildDebugLogger } from '../../../utils/logger.js';
import type { EffectStepFn } from '../../../utils/types.js';
import { libSQLDrizzleClient, Permissions, Users } from '../../../utils/useLibSQLDb.js';
import { getCheckers, hashPassword, verifyPasswordStrength } from '../../../utils/user-utils.js';
export enum UserFieldOption {
password = 'password',
username = 'username',
name = 'name',
}
export const libsqlModifyUsers: EffectStepFn = Effect.fn(function* (context, debug, dryRun) {
const [checker, debugLogger] = yield* Effect.all([getCheckers, buildDebugLogger(debug)]);
const { ASTRO_DB_REMOTE_URL, ASTRO_DB_APP_TOKEN } = process.env;
const [_drop, db] = yield* Effect.all([
debugLogger('Running libsqlUsers...'),
libSQLDrizzleClient(ASTRO_DB_REMOTE_URL as string, ASTRO_DB_APP_TOKEN as string),
]);
yield* debugLogger('Getting Users from DB...');
const [currentUsers, currentPermissions] = yield* db.execute((tx) =>
tx.batch([tx.select().from(Users), tx.select().from(Permissions)])
);
if (currentUsers.length === 0) {
yield* note('There are no users in the database.', 'No Users Available');
yield* context.exit(0);
}
const allUsers: { value: string; label: string; hint?: string }[] = [];
for (const user of currentUsers) {
allUsers.push({
value: user.id,
label: user.username,
hint: currentPermissions.find((perm) => perm.user === user.id)?.rank,
});
}
const userSelection = yield* select({
message: 'Select a user to modify',
options: allUsers,
});
if (typeof userSelection === 'symbol') {
return yield* context.pCancel(userSelection);
}
yield* note(`User ID Selected: ${userSelection}`);
const action = yield* select({
message: 'Which user field would you like to update?',
options: [
{ value: UserFieldOption.password, label: 'Password' },
{ value: UserFieldOption.username, label: 'Username' },
{ value: UserFieldOption.name, label: 'Display Name' },
],
});
switch (action) {
case UserFieldOption.name: {
const newDisplayName = yield* text({
message: 'Enter new display name',
placeholder: currentUsers.find((u) => u.id === userSelection)?.name || 'John Doe',
validate: (v) => (v.trim().length === 0 ? 'Display name cannot be empty' : undefined),
});
if (typeof newDisplayName === 'symbol') {
return yield* context.pCancel(newDisplayName);
}
if (dryRun) {
context.tasks.push({
title: `${StudioCMSColorwayInfo.bold('--dry-run')} ${context.chalk.dim('Skipping user modification')}`,
task: async (message) => {
message('Modifying user... (skipped)');
},
});
} else {
context.tasks.push({
title: context.chalk.dim('Modifying user...'),
task: async (message) => {
try {
await runEffect(
db.execute((tx) =>
tx.update(Users).set({ name: newDisplayName }).where(eq(Users.id, userSelection))
)
);
message('User modified successfully');
} catch (e) {
if (e instanceof Error) {
await runEffect(log.error(StudioCMSColorwayError(`Error: ${e.message}`)));
await runEffect(context.exit(1));
} else {
await runEffect(
log.error(StudioCMSColorwayError('Unknown Error: Unable to modify user.'))
);
await runEffect(context.exit(1));
}
}
},
});
}
break;
}
case UserFieldOption.username: {
const newUsername = yield* text({
message: 'Enter new username',
placeholder: currentUsers.find((u) => u.id === userSelection)?.username || 'johndoe',
validate: (user) => {
const u = user.trim();
const isUser = currentUsers.find(({ username }) => username === u);
if (isUser) return 'Username is already in use, please try another one';
if (Effect.runSync(checker.username(u))) {
return 'Username should not be a commonly used unsafe username (admin, root, etc.)';
}
return undefined;
},
});
if (typeof newUsername === 'symbol') {
return yield* context.pCancel(newUsername);
}
if (dryRun) {
context.tasks.push({
title: `${StudioCMSColorwayInfo.bold('--dry-run')} ${context.chalk.dim('Skipping user modification')}`,
task: async (message) => {
message('Modifying user... (skipped)');
},
});
} else {
context.tasks.push({
title: context.chalk.dim('Modifying user...'),
task: async (message) => {
try {
await runEffect(
db.execute((tx) =>
tx.update(Users).set({ username: newUsername }).where(eq(Users.id, userSelection))
)
);
message('User modified successfully');
} catch (e) {
if (e instanceof Error) {
await runEffect(log.error(StudioCMSColorwayError(`Error: ${e.message}`)));
await runEffect(context.exit(1));
} else {
await runEffect(
log.error(StudioCMSColorwayError('Unknown Error: Unable to modify user.'))
);
await runEffect(context.exit(1));
}
}
},
});
}
break;
}
case UserFieldOption.password: {
const newPassword = yield* password({
message: `Enter the user's new password`,
validate: (pass) => {
const passCheck = Effect.runSync(verifyPasswordStrength(pass));
if (passCheck !== true) {
return passCheck;
}
return undefined;
},
});
if (typeof newPassword === 'symbol') {
return yield* context.pCancel(newPassword);
}
if (dryRun) {
context.tasks.push({
title: `${StudioCMSColorwayInfo.bold('--dry-run')} ${context.chalk.dim('Skipping user modification')}`,
task: async (message) => {
message('Modifying user... (skipped)');
},
});
} else {
context.tasks.push({
title: context.chalk.dim('Modifying user...'),
task: async (message) => {
try {
// Environment variables are already checked by checkRequiredEnvVars
const hashedPassword = await runEffect(hashPassword(newPassword));
await runEffect(
db.execute((tx) =>
tx
.update(Users)
.set({ password: hashedPassword })
.where(eq(Users.id, userSelection))
)
);
message('User modified successfully');
} catch (e) {
if (e instanceof Error) {
await runEffect(log.error(StudioCMSColorwayError(`Error: ${e.message}`)));
await runEffect(context.exit(1));
} else {
await runEffect(
log.error(StudioCMSColorwayError('Unknown Error: Unable to modify user.'))
);
await runEffect(context.exit(1));
}
}
},
});
}
break;
}
default: {
return yield* context.pCancel(action);
}
}
});