- Add reviq auth login --token <token> command for CLI authentication - Create /account/api-tokens page for token management (superuser only) - Add me.apiTokens endpoints (list, create, delete) - Require superuser status and trusted session for token creation - Show API Tokens nav link only for superusers Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
110 lines
2.8 KiB
TypeScript
110 lines
2.8 KiB
TypeScript
/**
|
|
* API token management procedures
|
|
* Allows users to create and manage API tokens for CLI/programmatic access
|
|
*/
|
|
|
|
import { ORPCError } from "@orpc/server";
|
|
import {
|
|
generateSecureBase58Token,
|
|
hashToken,
|
|
TOKEN_PREFIX,
|
|
} from "../../utils/crypto.js";
|
|
import { authMiddleware, os } from "../base.js";
|
|
|
|
/** Token expiration: 365 days */
|
|
const TOKEN_EXPIRATION_DAYS = 365;
|
|
|
|
/**
|
|
* List all API tokens for the current user
|
|
* Returns token metadata (not the actual token values)
|
|
*/
|
|
export const listApiTokens = os.me.apiTokens.list
|
|
.use(authMiddleware)
|
|
.handler(async ({ context }) => {
|
|
const tokens = await context.db
|
|
.selectFrom("api_tokens")
|
|
.select(["id", "name", "last_used_at", "created_at", "expires_at"])
|
|
.where("user_id", "=", context.user.id)
|
|
.orderBy("created_at", "desc")
|
|
.execute();
|
|
|
|
return tokens.map((token) => ({
|
|
id: Number(token.id),
|
|
name: token.name,
|
|
lastUsedAt: token.last_used_at?.toISOString() ?? null,
|
|
createdAt: token.created_at.toISOString(),
|
|
expiresAt: token.expires_at.toISOString(),
|
|
}));
|
|
});
|
|
|
|
/**
|
|
* Create a new API token
|
|
* Requires superuser status and trusted session
|
|
*/
|
|
export const createApiToken = os.me.apiTokens.create
|
|
.use(authMiddleware)
|
|
.handler(async ({ input, context }) => {
|
|
// Require superuser status
|
|
if (!context.user.isSuperuser) {
|
|
throw new ORPCError("FORBIDDEN", {
|
|
message: "Only superusers can create API tokens.",
|
|
});
|
|
}
|
|
|
|
// Require trusted session for creating API tokens
|
|
if (!context.session.trustedMode) {
|
|
throw new ORPCError("FORBIDDEN", {
|
|
message:
|
|
"Creating API tokens requires a trusted session. Please re-authenticate.",
|
|
});
|
|
}
|
|
|
|
const { name } = input;
|
|
|
|
// Generate a new API token
|
|
const token = generateSecureBase58Token(TOKEN_PREFIX);
|
|
const tokenHash = await hashToken(token);
|
|
|
|
// Calculate expiration
|
|
const expiresAt = new Date(
|
|
Date.now() + TOKEN_EXPIRATION_DAYS * 24 * 60 * 60 * 1000,
|
|
);
|
|
|
|
// Insert into api_tokens table
|
|
await context.db
|
|
.insertInto("api_tokens")
|
|
.values({
|
|
user_id: context.user.id,
|
|
token_hash: tokenHash,
|
|
name,
|
|
expires_at: expiresAt,
|
|
})
|
|
.execute();
|
|
|
|
return {
|
|
token,
|
|
expiresAt: expiresAt.toISOString(),
|
|
};
|
|
});
|
|
|
|
/**
|
|
* Delete an API token
|
|
*/
|
|
export const deleteApiToken = os.me.apiTokens.delete
|
|
.use(authMiddleware)
|
|
.handler(async ({ input, context }) => {
|
|
const result = await context.db
|
|
.deleteFrom("api_tokens")
|
|
.where("id", "=", String(input.tokenId))
|
|
.where("user_id", "=", context.user.id)
|
|
.executeTakeFirst();
|
|
|
|
if (result.numDeletedRows === 0n) {
|
|
throw new ORPCError("NOT_FOUND", {
|
|
message: "API token not found",
|
|
});
|
|
}
|
|
|
|
return { success: true };
|
|
});
|