mirror of
https://github.com/versia-pub/server.git
synced 2026-03-13 05:49:16 +01:00
refactor(cli): ♻️ Rewrite CLI with Clerk. Removes a bunch of commands now covered by API.
This commit is contained in:
parent
28577d017a
commit
5b756ea2dd
32 changed files with 536 additions and 2721 deletions
87
cli/user/create.ts
Normal file
87
cli/user/create.ts
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import chalk from "chalk";
|
||||
// @ts-expect-error - Root import is required or the Clec type definitions won't work
|
||||
// biome-ignore lint/correctness/noUnusedImports: Root import is required or the Clec type definitions won't work
|
||||
import { type Root, defineCommand } from "clerc";
|
||||
import { and, eq, isNull } from "drizzle-orm";
|
||||
import { renderUnicodeCompact } from "uqr";
|
||||
import { User } from "~/classes/database/user";
|
||||
import { config } from "~/config";
|
||||
import { Users } from "~/drizzle/schema";
|
||||
|
||||
export const createUserCommand = defineCommand(
|
||||
{
|
||||
name: "user create",
|
||||
description: "Create a new user.",
|
||||
parameters: ["<username>"],
|
||||
flags: {
|
||||
password: {
|
||||
description: "Password for the new user",
|
||||
type: String,
|
||||
alias: "p",
|
||||
},
|
||||
email: {
|
||||
description: "Email for the new user",
|
||||
type: String,
|
||||
alias: "e",
|
||||
},
|
||||
admin: {
|
||||
description: "Make the new user an admin",
|
||||
type: Boolean,
|
||||
alias: "a",
|
||||
},
|
||||
},
|
||||
},
|
||||
async (context) => {
|
||||
const { admin, email, password } = context.flags;
|
||||
const { username } = context.parameters;
|
||||
|
||||
if (!username.match(/^[a-z0-9_-]+$/)) {
|
||||
throw new Error("Username must be alphanumeric and lowercase.");
|
||||
}
|
||||
|
||||
// Check if user already exists
|
||||
const existingUser = await User.fromSql(
|
||||
and(eq(Users.username, username), isNull(Users.instanceId)),
|
||||
);
|
||||
|
||||
if (existingUser) {
|
||||
throw new Error(`User ${chalk.gray(username)} is taken.`);
|
||||
}
|
||||
|
||||
const user = await User.fromDataLocal({
|
||||
email,
|
||||
password,
|
||||
username,
|
||||
admin,
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new Error("Failed to create user.");
|
||||
}
|
||||
|
||||
console.info(`User ${chalk.gray(username)} created.`);
|
||||
|
||||
if (!password) {
|
||||
const token = await user.resetPassword();
|
||||
|
||||
const link = new URL(
|
||||
`${config.frontend.routes.password_reset}?${new URLSearchParams(
|
||||
{
|
||||
token,
|
||||
},
|
||||
)}`,
|
||||
config.http.base_url,
|
||||
);
|
||||
|
||||
console.info(`Password reset link for ${chalk.gray(username)}:`);
|
||||
console.info(chalk.blue(link.href));
|
||||
|
||||
const qrcode = renderUnicodeCompact(link.href, {
|
||||
border: 2,
|
||||
});
|
||||
|
||||
// Pad all lines of QR code with spaces
|
||||
console.info(`\n ${qrcode.replaceAll("\n", "\n ")}`);
|
||||
}
|
||||
},
|
||||
);
|
||||
60
cli/user/delete.ts
Normal file
60
cli/user/delete.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import confirm from "@inquirer/confirm";
|
||||
import chalk from "chalk";
|
||||
// @ts-expect-error - Root import is required or the Clec type definitions won't work
|
||||
// biome-ignore lint/correctness/noUnusedImports: Root import is required or the Clec type definitions won't work
|
||||
import { type Root, defineCommand } from "clerc";
|
||||
import { retrieveUser } from "../utils.ts";
|
||||
|
||||
export const deleteUserCommand = defineCommand(
|
||||
{
|
||||
name: "user delete",
|
||||
alias: "user rm",
|
||||
description:
|
||||
"Delete a user from the database. Can use username or handle.",
|
||||
parameters: ["<username_or_handle>"],
|
||||
flags: {
|
||||
confirm: {
|
||||
description: "Ask for confirmation before deleting the user",
|
||||
type: Boolean,
|
||||
alias: "c",
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
async (context) => {
|
||||
const { confirm: confirmFlag } = context.flags;
|
||||
const { usernameOrHandle } = context.parameters;
|
||||
|
||||
const user = await retrieveUser(usernameOrHandle);
|
||||
|
||||
if (!user) {
|
||||
throw new Error(`User ${chalk.gray(usernameOrHandle)} not found.`);
|
||||
}
|
||||
|
||||
console.info(`About to delete user ${chalk.gray(user.data.username)}!`);
|
||||
console.info(`Username: ${chalk.blue(user.data.username)}`);
|
||||
console.info(`Display Name: ${chalk.blue(user.data.displayName)}`);
|
||||
console.info(`Created At: ${chalk.blue(user.data.createdAt)}`);
|
||||
console.info(
|
||||
`Instance: ${chalk.blue(user.data.instance?.baseUrl || "Local")}`,
|
||||
);
|
||||
|
||||
if (confirmFlag) {
|
||||
const choice = await confirm({
|
||||
message: `Are you sure you want to delete this user? ${chalk.red(
|
||||
"This is irreversible.",
|
||||
)}`,
|
||||
});
|
||||
|
||||
if (!choice) {
|
||||
throw new Error("Operation aborted.");
|
||||
}
|
||||
}
|
||||
|
||||
await user.delete();
|
||||
|
||||
console.info(
|
||||
`User ${chalk.gray(user.data.username)} has been deleted.`,
|
||||
);
|
||||
},
|
||||
);
|
||||
42
cli/user/refetch.ts
Normal file
42
cli/user/refetch.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import chalk from "chalk";
|
||||
// @ts-expect-error - Root import is required or the Clec type definitions won't work
|
||||
// biome-ignore lint/correctness/noUnusedImports: Root import is required or the Clec type definitions won't work
|
||||
import { type Root, defineCommand } from "clerc";
|
||||
import ora from "ora";
|
||||
import { retrieveUser } from "../utils.ts";
|
||||
|
||||
export const refetchUserCommand = defineCommand(
|
||||
{
|
||||
name: "user refetch",
|
||||
description: "Refetches user data from their remote instance.",
|
||||
parameters: ["<handle>"],
|
||||
},
|
||||
async (context) => {
|
||||
const { handle } = context.parameters;
|
||||
|
||||
const user = await retrieveUser(handle);
|
||||
|
||||
if (!user) {
|
||||
throw new Error(`User ${chalk.gray(handle)} not found.`);
|
||||
}
|
||||
|
||||
if (user.isLocal()) {
|
||||
throw new Error(
|
||||
"This user is local and as such cannot be refetched.",
|
||||
);
|
||||
}
|
||||
|
||||
const spinner = ora("Refetching user").start();
|
||||
|
||||
try {
|
||||
await user.updateFromRemote();
|
||||
} catch (error) {
|
||||
spinner.fail(
|
||||
`Failed to refetch user ${chalk.gray(user.data.username)}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
|
||||
spinner.succeed(`User ${chalk.gray(user.data.username)} refetched.`);
|
||||
},
|
||||
);
|
||||
37
cli/user/token.ts
Normal file
37
cli/user/token.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { randomString } from "@/math.ts";
|
||||
import chalk from "chalk";
|
||||
// @ts-expect-error - Root import is required or the Clec type definitions won't work
|
||||
// biome-ignore lint/correctness/noUnusedImports: Root import is required or the Clec type definitions won't work
|
||||
import { type Root, defineCommand } from "clerc";
|
||||
import { Token } from "~/classes/database/token.ts";
|
||||
import { retrieveUser } from "../utils.ts";
|
||||
|
||||
export const generateTokenCommand = defineCommand(
|
||||
{
|
||||
name: "user token",
|
||||
description: "Generates a new access token for a user.",
|
||||
parameters: ["<username>"],
|
||||
},
|
||||
async (context) => {
|
||||
const { username } = context.parameters;
|
||||
|
||||
const user = await retrieveUser(username);
|
||||
|
||||
if (!user) {
|
||||
throw new Error(`User ${chalk.gray(username)} not found.`);
|
||||
}
|
||||
|
||||
const token = await Token.insert({
|
||||
accessToken: randomString(64, "base64url"),
|
||||
code: null,
|
||||
scope: "read write follow",
|
||||
tokenType: "Bearer",
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
console.info(
|
||||
`Token generated for user ${chalk.gray(user.data.username)}.`,
|
||||
);
|
||||
console.info(`Access Token: ${chalk.blue(token.data.accessToken)}`);
|
||||
},
|
||||
);
|
||||
Loading…
Add table
Add a link
Reference in a new issue