Merge branch 'master' into testing-improvements
This commit is contained in:
@@ -6,4 +6,10 @@ export const app = buildApplication(rootRouteMap, {
|
||||
versionInfo: {
|
||||
currentVersion: "0.0.0",
|
||||
},
|
||||
scanner: {
|
||||
caseStyle: "allow-kebab-for-camel",
|
||||
},
|
||||
documentation: {
|
||||
caseStyle: "convert-camel-to-kebab",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { buildRouteMap } from "@stricli/core";
|
||||
import { adminRouteMap } from "./admin/_command.js";
|
||||
import { authRouteMap } from "./auth/_command.js";
|
||||
import { bootstrapCommand } from "./bootstrap.js";
|
||||
import { completionsCommand } from "./completions.js";
|
||||
@@ -8,6 +9,7 @@ import { userRouteMap } from "./user/_command.js";
|
||||
export const rootRouteMap = buildRouteMap({
|
||||
routes: {
|
||||
bootstrap: bootstrapCommand,
|
||||
admin: adminRouteMap,
|
||||
auth: authRouteMap,
|
||||
user: userRouteMap,
|
||||
org: orgRouteMap,
|
||||
|
||||
11
apps/cli/src/routes/admin/_command.ts
Normal file
11
apps/cli/src/routes/admin/_command.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { buildRouteMap } from "@stricli/core";
|
||||
import { completeLoginCommand } from "./complete-login.js";
|
||||
|
||||
export const adminRouteMap = buildRouteMap({
|
||||
routes: {
|
||||
"complete-login": completeLoginCommand,
|
||||
},
|
||||
docs: {
|
||||
brief: "Admin commands (requires superuser)",
|
||||
},
|
||||
});
|
||||
51
apps/cli/src/routes/admin/complete-login.ts
Normal file
51
apps/cli/src/routes/admin/complete-login.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import type { LocalContext } from "../../context.js";
|
||||
import { ORPCError } from "@orpc/client";
|
||||
import { buildCommand } from "@stricli/core";
|
||||
import { createApiClient } from "../../utils/api-client.js";
|
||||
|
||||
interface CompleteLoginFlags {
|
||||
email: string;
|
||||
}
|
||||
|
||||
async function completeLogin(
|
||||
this: LocalContext,
|
||||
flags: CompleteLoginFlags,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const api = await createApiClient();
|
||||
|
||||
await api.admin.auth.completeLogin({
|
||||
email: flags.email,
|
||||
});
|
||||
|
||||
console.log(`Completed login request for: ${flags.email}`);
|
||||
} catch (error) {
|
||||
if (error instanceof ORPCError) {
|
||||
console.error(`Error [${String(error.code)}]:`, error.message);
|
||||
} else {
|
||||
console.error(
|
||||
"Error:",
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
}
|
||||
this.process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
export const completeLoginCommand = buildCommand({
|
||||
func: completeLogin,
|
||||
parameters: {
|
||||
flags: {
|
||||
email: {
|
||||
kind: "parsed",
|
||||
parse: String,
|
||||
brief: "Email address of user with pending login request",
|
||||
},
|
||||
},
|
||||
},
|
||||
docs: {
|
||||
brief: "Complete pending login request",
|
||||
fullDescription:
|
||||
"Completes the most recent pending login request for a user. This is useful for development when email sending is not configured or to bypass email confirmation.",
|
||||
},
|
||||
});
|
||||
@@ -4,39 +4,11 @@ import { createApiClient } from "../../utils/api-client.js";
|
||||
import { getConfigPath, readConfig } from "../../utils/config.js";
|
||||
import { TOKEN_PREFIX } from "../../utils/token.js";
|
||||
|
||||
interface AuthStatusResponse {
|
||||
user: {
|
||||
id: number;
|
||||
email: string;
|
||||
displayName: string | null;
|
||||
fullName: string | null;
|
||||
isSuperuser: boolean;
|
||||
emailVerified: boolean;
|
||||
};
|
||||
auth:
|
||||
| {
|
||||
method: "api_token";
|
||||
tokenId: string;
|
||||
tokenName: string;
|
||||
expiresAt: string;
|
||||
lastUsedAt: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
| {
|
||||
method: "session";
|
||||
sessionId: string;
|
||||
expiresAt: string;
|
||||
createdAt: string;
|
||||
};
|
||||
}
|
||||
|
||||
function formatDate(dateStr: string): string {
|
||||
const date = new Date(dateStr);
|
||||
function formatDate(date: Date): string {
|
||||
return date.toLocaleString();
|
||||
}
|
||||
|
||||
function formatRelativeTime(dateStr: string): string {
|
||||
const date = new Date(dateStr);
|
||||
function formatRelativeTime(date: Date): string {
|
||||
const now = new Date();
|
||||
const diffMs = date.getTime() - now.getTime();
|
||||
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||
@@ -86,8 +58,8 @@ async function status(this: LocalContext): Promise<void> {
|
||||
// Try to fetch status from API
|
||||
console.log("\nAPI Status:");
|
||||
try {
|
||||
const client = await createApiClient();
|
||||
const response = await client.call<AuthStatusResponse>("me.authStatus");
|
||||
const api = await createApiClient();
|
||||
const response = await api.me.authStatus();
|
||||
|
||||
// User info
|
||||
console.log("\n User:");
|
||||
|
||||
@@ -6,6 +6,7 @@ import { writeConfig } from "../utils/config.js";
|
||||
interface BootstrapFlags {
|
||||
email: string;
|
||||
password: string;
|
||||
dangerouslyOverwriteExisting: boolean;
|
||||
}
|
||||
|
||||
async function bootstrap(
|
||||
@@ -28,6 +29,7 @@ async function bootstrap(
|
||||
const result = await executeBootstrap(db, {
|
||||
email: flags.email,
|
||||
password: flags.password,
|
||||
dangerouslyOverwriteExisting: flags.dangerouslyOverwriteExisting,
|
||||
});
|
||||
|
||||
console.log(`Created superuser: ${result.user.email}`);
|
||||
@@ -68,6 +70,11 @@ export const bootstrapCommand = buildCommand({
|
||||
parse: String,
|
||||
brief: "Password for the superuser",
|
||||
},
|
||||
dangerouslyOverwriteExisting: {
|
||||
kind: "boolean",
|
||||
brief: "Delete existing user and reviq org if they exist",
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
docs: {
|
||||
|
||||
@@ -9,9 +9,9 @@ interface AddSiteFlags {
|
||||
|
||||
async function addSite(this: LocalContext, flags: AddSiteFlags): Promise<void> {
|
||||
try {
|
||||
const client = await createApiClient();
|
||||
const api = await createApiClient();
|
||||
|
||||
await client.call("admin.orgs.addSite", {
|
||||
await api.admin.orgs.addSite({
|
||||
slug: flags.org,
|
||||
domain: flags.domain,
|
||||
});
|
||||
|
||||
@@ -13,9 +13,9 @@ async function create(
|
||||
flags: CreateOrgFlags,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const client = await createApiClient();
|
||||
const api = await createApiClient();
|
||||
|
||||
const result = await client.call<{ slug: string }>("admin.orgs.create", {
|
||||
const result = await api.admin.orgs.create({
|
||||
slug: flags.slug,
|
||||
displayName: flags.name,
|
||||
ownerEmail: flags.owner,
|
||||
|
||||
@@ -2,19 +2,11 @@ import type { LocalContext } from "../../context.js";
|
||||
import { buildCommand } from "@stricli/core";
|
||||
import { createApiClient } from "../../utils/api-client.js";
|
||||
|
||||
interface OrgOutput {
|
||||
id: number;
|
||||
slug: string;
|
||||
displayName: string;
|
||||
logoUrl: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
async function list(this: LocalContext): Promise<void> {
|
||||
try {
|
||||
const client = await createApiClient();
|
||||
const api = await createApiClient();
|
||||
|
||||
const orgs = await client.call<OrgOutput[]>("admin.orgs.list", {});
|
||||
const orgs = await api.admin.orgs.list();
|
||||
|
||||
if (orgs.length === 0) {
|
||||
console.log("No organizations found");
|
||||
@@ -27,7 +19,7 @@ async function list(this: LocalContext): Promise<void> {
|
||||
for (const org of orgs) {
|
||||
console.log(org.slug);
|
||||
console.log(` Name: ${org.displayName}`);
|
||||
console.log(` Created: ${new Date(org.createdAt).toLocaleDateString()}`);
|
||||
console.log(` Created: ${org.createdAt.toLocaleDateString()}`);
|
||||
console.log();
|
||||
}
|
||||
|
||||
|
||||
@@ -11,9 +11,9 @@ async function confirmEmail(
|
||||
flags: ConfirmEmailFlags,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const client = await createApiClient();
|
||||
const api = await createApiClient();
|
||||
|
||||
await client.call("admin.users.confirmEmail", {
|
||||
await api.admin.users.confirmEmail({
|
||||
email: flags.email,
|
||||
});
|
||||
|
||||
|
||||
@@ -2,6 +2,22 @@ import type { LocalContext } from "../../context.js";
|
||||
import { buildCommand } from "@stricli/core";
|
||||
import { createApiClient } from "../../utils/api-client.js";
|
||||
|
||||
type OrgRole = "owner" | "admin" | "member";
|
||||
|
||||
const validRoles: OrgRole[] = ["owner", "admin", "member"];
|
||||
|
||||
function parseRole(role: string | undefined): OrgRole | undefined {
|
||||
if (!role) {
|
||||
return undefined;
|
||||
}
|
||||
if (validRoles.includes(role as OrgRole)) {
|
||||
return role as OrgRole;
|
||||
}
|
||||
throw new Error(
|
||||
`Invalid role: ${role}. Must be one of: ${validRoles.join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
interface CreateUserFlags {
|
||||
email: string;
|
||||
name?: string;
|
||||
@@ -14,13 +30,14 @@ async function create(
|
||||
flags: CreateUserFlags,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const client = await createApiClient();
|
||||
const orgRole = parseRole(flags.role);
|
||||
const api = await createApiClient();
|
||||
|
||||
await client.call("admin.users.create", {
|
||||
await api.admin.users.create({
|
||||
email: flags.email,
|
||||
name: flags.name,
|
||||
orgSlug: flags.org,
|
||||
orgRole: flags.role,
|
||||
orgRole,
|
||||
});
|
||||
|
||||
console.log(`Created user: ${flags.email}`);
|
||||
|
||||
@@ -2,18 +2,19 @@
|
||||
* API client utilities for CLI commands
|
||||
*/
|
||||
|
||||
import type { ContractRouterClient } from "@orpc/contract";
|
||||
import type { contract } from "@reviq/api-contract";
|
||||
import { createORPCClient } from "@orpc/client";
|
||||
import { RPCLink } from "@orpc/client/fetch";
|
||||
import { readConfig } from "./config.js";
|
||||
|
||||
export interface ApiClientError {
|
||||
code: string;
|
||||
message: string;
|
||||
}
|
||||
export type ApiClient = ContractRouterClient<typeof contract>;
|
||||
|
||||
/**
|
||||
* Create an API client with the stored credentials
|
||||
* Create an oRPC API client with the stored credentials
|
||||
* Throws an error if not logged in
|
||||
*/
|
||||
export const createApiClient = async () => {
|
||||
export const createApiClient = async (): Promise<ApiClient> => {
|
||||
const config = await readConfig();
|
||||
if (!config) {
|
||||
throw new Error(
|
||||
@@ -21,41 +22,13 @@ export const createApiClient = async () => {
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
/**
|
||||
* Call an oRPC procedure
|
||||
*/
|
||||
call: async <T>(path: string, input?: unknown): Promise<T> => {
|
||||
const url = `${config.apiUrl}/api/v1/rpc/${path}`;
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-API-Key": config.token,
|
||||
},
|
||||
body: input !== undefined ? JSON.stringify(input) : undefined,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
let errorMessage = `API error: ${String(response.status)} ${response.statusText}`;
|
||||
try {
|
||||
const error = JSON.parse(text) as ApiClientError;
|
||||
if (error.message) {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
} catch {
|
||||
if (text) {
|
||||
errorMessage = text;
|
||||
}
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
return response.json() as Promise<T>;
|
||||
const link = new RPCLink({
|
||||
url: `${config.apiUrl}/api/v1/rpc`,
|
||||
headers: {
|
||||
"X-API-Key": config.token,
|
||||
},
|
||||
config,
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
export type ApiClient = Awaited<ReturnType<typeof createApiClient>>;
|
||||
// Cast to ApiClient for type-safe API calls
|
||||
return createORPCClient(link) as unknown as ApiClient;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user