Add admin CLI command and auth guard, use oRPC client
CLI changes: - Use official oRPC client instead of manual HTTP requests - Add admin complete-login command for dev workflow - Remove type assertions, use proper ContractRouterClient typing - Add @orpc/client and @orpc/contract dependencies API changes: - Use oRPC cookie helpers from @orpc/server/helpers - Improve admin complete-login error messages (expired, already completed) Dashboard changes: - Add AuthGuard component to redirect unauthenticated users to /auth/login - Update confirm page with correct CLI command and copy button - Remove duplicate auth redirect from dashboard layout Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,24 +9,40 @@ export const adminAuthCompleteLogin = os.admin.auth.completeLogin
|
||||
.use(authMiddleware)
|
||||
.use(superuserMiddleware)
|
||||
.handler(async ({ input, context }) => {
|
||||
const loginRequest = await context.db
|
||||
const email = input.email.toLowerCase();
|
||||
|
||||
// First check if any login request exists for this email
|
||||
const anyRequest = await context.db
|
||||
.selectFrom("login_requests")
|
||||
.where("email", "=", input.email.toLowerCase())
|
||||
.where("completed_at", "is", null)
|
||||
.where("expires_at", ">", new Date())
|
||||
.where("email", "=", email)
|
||||
.orderBy("created_at", "desc")
|
||||
.select(["id"])
|
||||
.select(["id", "completed_at", "expires_at"])
|
||||
.executeTakeFirst();
|
||||
|
||||
if (!loginRequest) {
|
||||
if (!anyRequest) {
|
||||
throw new ORPCError("NOT_FOUND", {
|
||||
message: "No pending login request found",
|
||||
message: `No login request found for ${email}`,
|
||||
});
|
||||
}
|
||||
|
||||
// Check if already completed
|
||||
if (anyRequest.completed_at) {
|
||||
throw new ORPCError("BAD_REQUEST", {
|
||||
message: "Login request already completed",
|
||||
});
|
||||
}
|
||||
|
||||
// Check if expired
|
||||
if (new Date(anyRequest.expires_at) < new Date()) {
|
||||
throw new ORPCError("BAD_REQUEST", {
|
||||
message: "Login request expired (15 min limit). Start a new login flow.",
|
||||
});
|
||||
}
|
||||
|
||||
// Complete the login request
|
||||
await context.db
|
||||
.updateTable("login_requests")
|
||||
.set({ completed_at: new Date() })
|
||||
.where("id", "=", loginRequest.id)
|
||||
.where("id", "=", anyRequest.id)
|
||||
.execute();
|
||||
});
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
/**
|
||||
* Cookie configuration for authentication
|
||||
* All cookies use 'rev.' prefix, HttpOnly, Secure, SameSite=Lax
|
||||
*
|
||||
* Uses oRPC cookie helpers for proper cookie handling
|
||||
*/
|
||||
|
||||
export {
|
||||
deleteCookie,
|
||||
getCookie,
|
||||
setCookie,
|
||||
} from "@orpc/server/helpers";
|
||||
|
||||
export const COOKIE_NAMES = {
|
||||
SESSION_TOKEN: "rev.session_token",
|
||||
DEVICE_FINGERPRINT: "rev.device_fingerprint",
|
||||
@@ -39,71 +47,3 @@ export const COOKIE_OPTIONS = {
|
||||
maxAge: COOKIE_DURATIONS.LOGIN_REQUEST,
|
||||
},
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Cookie options type for setCookie function
|
||||
*/
|
||||
export interface CookieOptions {
|
||||
httpOnly?: boolean;
|
||||
secure?: boolean;
|
||||
sameSite?: "strict" | "lax" | "none";
|
||||
path?: string;
|
||||
maxAge?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse cookie string and get a specific cookie value
|
||||
*/
|
||||
export const getCookie = (
|
||||
headers: Headers,
|
||||
name: string,
|
||||
): string | undefined => {
|
||||
const cookieHeader = headers.get("Cookie");
|
||||
if (!cookieHeader) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const cookies = cookieHeader.split(";").map((c) => c.trim());
|
||||
for (const cookie of cookies) {
|
||||
const [cookieName, ...valueParts] = cookie.split("=");
|
||||
if (cookieName === name) {
|
||||
return valueParts.join("=");
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set a cookie in the response headers
|
||||
*/
|
||||
export const setCookie = (
|
||||
headers: Headers,
|
||||
name: string,
|
||||
value: string,
|
||||
options: CookieOptions,
|
||||
): void => {
|
||||
const parts = [`${name}=${value}`];
|
||||
if (options.httpOnly) {
|
||||
parts.push("HttpOnly");
|
||||
}
|
||||
if (options.secure) {
|
||||
parts.push("Secure");
|
||||
}
|
||||
if (options.sameSite) {
|
||||
parts.push(`SameSite=${options.sameSite}`);
|
||||
}
|
||||
if (options.path) {
|
||||
parts.push(`Path=${options.path}`);
|
||||
}
|
||||
if (options.maxAge) {
|
||||
parts.push(`Max-Age=${String(options.maxAge)}`);
|
||||
}
|
||||
headers.append("Set-Cookie", parts.join("; "));
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete a cookie by setting it to expire immediately
|
||||
*/
|
||||
export const deleteCookie = (headers: Headers, name: string): void => {
|
||||
headers.append("Set-Cookie", `${name}=; Path=/; Max-Age=0`);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user