UNPKG

studiocms

Version:

Astro Native CMS for AstroDB. Built from the ground up by the Astro community.

230 lines (205 loc) 6.98 kB
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); } } });