Files
Codewalkers/apps/server/trpc/routers/account.ts
Lukas May 9c4131c814 feat: add addAccountByToken tRPC mutation with upsert logic and tests
Adds a new mutation that accepts an email + raw OAuth token and upserts
the account — creating it if it doesn't exist, updating credentials if
it does. Covers all four scenarios with unit tests (new, existing,
empty-email, empty-token validation).
2026-03-06 10:58:02 +01:00

101 lines
3.2 KiB
TypeScript

/**
* Account Router — list, add, remove, refresh, update auth, mark exhausted, providers
*/
import { z } from 'zod';
import type { ProcedureBuilder } from '../trpc.js';
import { requireAccountRepository } from './_helpers.js';
import { listProviders as listProviderNames } from '../../agent/providers/registry.js';
export function accountProcedures(publicProcedure: ProcedureBuilder) {
return {
listAccounts: publicProcedure
.query(async ({ ctx }) => {
const repo = requireAccountRepository(ctx);
return repo.findAll();
}),
addAccount: publicProcedure
.input(z.object({
email: z.string().min(1),
provider: z.string().default('claude'),
configJson: z.string().optional(),
credentials: z.string().optional(),
}))
.mutation(async ({ ctx, input }) => {
const repo = requireAccountRepository(ctx);
return repo.create({
email: input.email,
provider: input.provider,
configJson: input.configJson,
credentials: input.credentials,
});
}),
removeAccount: publicProcedure
.input(z.object({ id: z.string().min(1) }))
.mutation(async ({ ctx, input }) => {
const repo = requireAccountRepository(ctx);
await repo.delete(input.id);
return { success: true };
}),
refreshAccounts: publicProcedure
.mutation(async ({ ctx }) => {
const repo = requireAccountRepository(ctx);
const cleared = await repo.clearExpiredExhaustion();
return { cleared };
}),
updateAccountAuth: publicProcedure
.input(z.object({
id: z.string().min(1),
configJson: z.string(),
credentials: z.string(),
}))
.mutation(async ({ ctx, input }) => {
const repo = requireAccountRepository(ctx);
return repo.updateAccountAuth(input.id, input.configJson, input.credentials);
}),
markAccountExhausted: publicProcedure
.input(z.object({
id: z.string().min(1),
until: z.string().datetime(),
}))
.mutation(async ({ ctx, input }) => {
const repo = requireAccountRepository(ctx);
return repo.markExhausted(input.id, new Date(input.until));
}),
listProviderNames: publicProcedure
.query(() => {
return listProviderNames();
}),
addAccountByToken: publicProcedure
.input(z.object({
email: z.string().min(1),
provider: z.string().default('claude'),
token: z.string().min(1),
}))
.mutation(async ({ ctx, input }) => {
const repo = requireAccountRepository(ctx);
const credentials = JSON.stringify({ claudeAiOauth: { accessToken: input.token } });
const configJson = JSON.stringify({ hasCompletedOnboarding: true });
const existing = await repo.findByEmail(input.email);
if (existing) {
const account = await repo.updateAccountAuth(existing.id, configJson, credentials);
return { upserted: true, account };
}
const account = await repo.create({
email: input.email,
provider: input.provider,
configJson,
credentials,
});
return { upserted: false, account };
}),
};
}