Skip to content

Commit 26d2104

Browse files
sweetmantechclaude
andauthored
feat: add /api/accounts endpoints for account management (#152)
* feat: add /api/accounts endpoints for account management Migrate account endpoints from Recoup-Chat to Recoup-API: - POST /api/accounts - Create or retrieve account by email/wallet - PATCH /api/accounts - Update account profile information - POST /api/accounts/artists - Add artist to account Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: use safeParseJson for request body parsing Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 144ed9d commit 26d2104

File tree

16 files changed

+693
-0
lines changed

16 files changed

+693
-0
lines changed

app/api/accounts/artists/route.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
3+
import { safeParseJson } from "@/lib/networking/safeParseJson";
4+
import { validateAddArtistBody, type AddArtistBody } from "@/lib/accounts/validateAddArtistBody";
5+
import { addArtistToAccountHandler } from "@/lib/accounts/addArtistToAccountHandler";
6+
7+
/**
8+
* POST /api/accounts/artists
9+
*
10+
* Add an artist to an account's list of associated artists.
11+
* If the artist is already associated with the account, returns success.
12+
*
13+
* @param req - The incoming request with email and artistId in body
14+
* @returns NextResponse with success status or error
15+
*/
16+
export async function POST(req: NextRequest) {
17+
const body = await safeParseJson(req);
18+
19+
const validated = validateAddArtistBody(body);
20+
if (validated instanceof NextResponse) {
21+
return validated;
22+
}
23+
24+
return addArtistToAccountHandler(validated as AddArtistBody);
25+
}
26+
27+
/**
28+
* OPTIONS handler for CORS preflight requests.
29+
*
30+
* @returns NextResponse with CORS headers
31+
*/
32+
export async function OPTIONS() {
33+
return new NextResponse(null, {
34+
status: 204,
35+
headers: getCorsHeaders(),
36+
});
37+
}
38+
39+
export const dynamic = "force-dynamic";
40+
export const fetchCache = "force-no-store";
41+
export const revalidate = 0;

app/api/accounts/route.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
3+
import { safeParseJson } from "@/lib/networking/safeParseJson";
4+
import {
5+
validateCreateAccountBody,
6+
type CreateAccountBody,
7+
} from "@/lib/accounts/validateCreateAccountBody";
8+
import {
9+
validateUpdateAccountBody,
10+
type UpdateAccountBody,
11+
} from "@/lib/accounts/validateUpdateAccountBody";
12+
import { createAccountHandler } from "@/lib/accounts/createAccountHandler";
13+
import { updateAccountHandler } from "@/lib/accounts/updateAccountHandler";
14+
15+
/**
16+
* POST /api/accounts
17+
*
18+
* Create a new account or retrieve an existing account by email or wallet.
19+
* If an account with the provided email or wallet exists, returns that account.
20+
* Otherwise creates a new account and initializes credits.
21+
*
22+
* @param req - The incoming request with email and/or wallet in body
23+
* @returns Account data
24+
*/
25+
export async function POST(req: NextRequest) {
26+
const body = await safeParseJson(req);
27+
28+
const validated = validateCreateAccountBody(body);
29+
if (validated instanceof NextResponse) {
30+
return validated;
31+
}
32+
33+
return createAccountHandler(validated as CreateAccountBody);
34+
}
35+
36+
/**
37+
* PATCH /api/accounts
38+
*
39+
* Update an existing account's profile information.
40+
* Requires accountId in the body along with fields to update.
41+
*
42+
* @param req - The incoming request with accountId and update fields
43+
* @returns NextResponse with updated account data or error
44+
*/
45+
export async function PATCH(req: NextRequest) {
46+
const body = await safeParseJson(req);
47+
48+
const validated = validateUpdateAccountBody(body);
49+
if (validated instanceof NextResponse) {
50+
return validated;
51+
}
52+
53+
return updateAccountHandler(validated as UpdateAccountBody);
54+
}
55+
56+
/**
57+
* OPTIONS handler for CORS preflight requests.
58+
*
59+
* @returns NextResponse with CORS headers
60+
*/
61+
export async function OPTIONS() {
62+
return new NextResponse(null, {
63+
status: 204,
64+
headers: getCorsHeaders(),
65+
});
66+
}
67+
68+
export const dynamic = "force-dynamic";
69+
export const fetchCache = "force-no-store";
70+
export const revalidate = 0;
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { NextResponse } from "next/server";
2+
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
3+
import { selectAccountByEmail } from "@/lib/supabase/account_emails/selectAccountByEmail";
4+
import { getAccountArtistIds } from "@/lib/supabase/account_artist_ids/getAccountArtistIds";
5+
import { insertAccountArtistId } from "@/lib/supabase/account_artist_ids/insertAccountArtistId";
6+
import type { AddArtistBody } from "./validateAddArtistBody";
7+
8+
/**
9+
* Handles POST /api/accounts/artists - Add artist to account's artist list.
10+
*
11+
* @param body - Validated request body with email and artistId
12+
* @returns NextResponse with success status
13+
*/
14+
export async function addArtistToAccountHandler(body: AddArtistBody): Promise<NextResponse> {
15+
const { email, artistId } = body;
16+
17+
try {
18+
// Find account by email
19+
const accountEmail = await selectAccountByEmail(email);
20+
if (!accountEmail?.account_id) {
21+
return NextResponse.json(
22+
{ message: "Not found account." },
23+
{ status: 400, headers: getCorsHeaders() },
24+
);
25+
}
26+
27+
const accountId = accountEmail.account_id;
28+
29+
// Check if artist is already associated with account
30+
const existingArtists = await getAccountArtistIds({ accountIds: [accountId] });
31+
const alreadyExists = existingArtists.some(a => a.artist_id === artistId);
32+
33+
if (alreadyExists) {
34+
return NextResponse.json({ success: true }, { status: 200, headers: getCorsHeaders() });
35+
}
36+
37+
// Add artist to account
38+
await insertAccountArtistId(accountId, artistId);
39+
40+
return NextResponse.json({ success: true }, { status: 200, headers: getCorsHeaders() });
41+
} catch (error) {
42+
const message = error instanceof Error ? error.message : "failed";
43+
return NextResponse.json({ message }, { status: 400, headers: getCorsHeaders() });
44+
}
45+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { NextResponse } from "next/server";
2+
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
3+
import { selectAccountByEmail } from "@/lib/supabase/account_emails/selectAccountByEmail";
4+
import { selectAccountByWallet } from "@/lib/supabase/account_wallets/selectAccountByWallet";
5+
import { getAccountWithDetails } from "@/lib/supabase/accounts/getAccountWithDetails";
6+
import { insertAccount } from "@/lib/supabase/accounts/insertAccount";
7+
import { insertAccountEmail } from "@/lib/supabase/account_emails/insertAccountEmail";
8+
import { insertAccountWallet } from "@/lib/supabase/account_wallets/insertAccountWallet";
9+
import { insertCreditsUsage } from "@/lib/supabase/credits_usage/insertCreditsUsage";
10+
import { assignAccountToOrg } from "@/lib/organizations/assignAccountToOrg";
11+
import type { CreateAccountBody } from "./validateCreateAccountBody";
12+
13+
/**
14+
* Account data response shape.
15+
*/
16+
export type AccountDataResponse = {
17+
id: string;
18+
account_id: string;
19+
name?: string;
20+
email?: string;
21+
wallet?: string;
22+
image?: string;
23+
instruction?: string;
24+
organization?: string;
25+
};
26+
27+
/**
28+
* Handles POST /api/accounts - Create or retrieve account by email/wallet.
29+
*
30+
* @param body - Validated request body with email and/or wallet
31+
* @returns NextResponse with account data
32+
*/
33+
export async function createAccountHandler(body: CreateAccountBody): Promise<NextResponse> {
34+
const { email, wallet } = body;
35+
36+
try {
37+
// If email is provided, check account_emails
38+
if (email) {
39+
const emailRecord = await selectAccountByEmail(email);
40+
if (emailRecord?.account_id) {
41+
// Assign to org based on email domain (idempotent)
42+
await assignAccountToOrg(emailRecord.account_id, email);
43+
44+
const accountData = await getAccountWithDetails(emailRecord.account_id);
45+
if (accountData) {
46+
return NextResponse.json(
47+
{ data: accountData },
48+
{ status: 200, headers: getCorsHeaders() },
49+
);
50+
}
51+
}
52+
}
53+
54+
// If wallet is provided, check account_wallets
55+
if (wallet) {
56+
try {
57+
const account = await selectAccountByWallet(wallet);
58+
59+
// Flatten the nested relations into a single object
60+
const accountInfo = account.account_info?.[0];
61+
const accountEmail = account.account_emails?.[0];
62+
const accountWallet = account.account_wallets?.[0];
63+
64+
const accountData: AccountDataResponse = {
65+
id: account.id,
66+
account_id: account.id,
67+
name: account.name || undefined,
68+
image: accountInfo?.image || undefined,
69+
instruction: accountInfo?.instruction || undefined,
70+
organization: accountInfo?.organization || undefined,
71+
email: accountEmail?.email || undefined,
72+
wallet: accountWallet?.wallet || undefined,
73+
};
74+
75+
return NextResponse.json({ data: accountData }, { status: 200, headers: getCorsHeaders() });
76+
} catch {
77+
// Wallet not found, continue to create new account
78+
}
79+
}
80+
81+
// Create new account
82+
const newAccount = await insertAccount({ name: "" });
83+
84+
if (email) {
85+
await insertAccountEmail(newAccount.id, email);
86+
// Assign new account to org based on email domain
87+
await assignAccountToOrg(newAccount.id, email);
88+
}
89+
90+
if (wallet) {
91+
await insertAccountWallet(newAccount.id, wallet);
92+
}
93+
94+
await insertCreditsUsage(newAccount.id);
95+
96+
const newAccountData: AccountDataResponse = {
97+
id: newAccount.id,
98+
account_id: newAccount.id,
99+
email: email || "",
100+
wallet: wallet || "",
101+
image: "",
102+
instruction: "",
103+
organization: "",
104+
};
105+
106+
return NextResponse.json({ data: newAccountData }, { status: 200, headers: getCorsHeaders() });
107+
} catch (error) {
108+
const message = error instanceof Error ? error.message : "failed";
109+
return NextResponse.json({ message }, { status: 400, headers: getCorsHeaders() });
110+
}
111+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { NextResponse } from "next/server";
2+
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
3+
import { getAccountWithDetails } from "@/lib/supabase/accounts/getAccountWithDetails";
4+
import { updateAccount } from "@/lib/supabase/accounts/updateAccount";
5+
import { insertAccountInfo } from "@/lib/supabase/account_info/insertAccountInfo";
6+
import { updateAccountInfo } from "@/lib/supabase/account_info/updateAccountInfo";
7+
import { selectAccountInfo } from "@/lib/supabase/account_info/selectAccountInfo";
8+
import type { UpdateAccountBody } from "./validateUpdateAccountBody";
9+
10+
/**
11+
* Handles PATCH /api/accounts - Update account profile information.
12+
*
13+
* @param body - Validated request body with accountId and fields to update
14+
* @returns NextResponse with updated account data
15+
*/
16+
export async function updateAccountHandler(body: UpdateAccountBody): Promise<NextResponse> {
17+
const {
18+
accountId,
19+
name,
20+
instruction,
21+
organization,
22+
image,
23+
jobTitle,
24+
roleType,
25+
companyName,
26+
knowledges,
27+
} = body;
28+
29+
try {
30+
// Verify account exists
31+
const found = await getAccountWithDetails(accountId);
32+
if (!found) {
33+
return NextResponse.json(
34+
{ data: null, message: "Account not found" },
35+
{ status: 400, headers: getCorsHeaders() },
36+
);
37+
}
38+
39+
// Update account name if provided
40+
if (name !== undefined) {
41+
await updateAccount(accountId, { name });
42+
}
43+
44+
// Check if account_info exists
45+
const existingInfo = await selectAccountInfo(accountId);
46+
47+
if (!existingInfo) {
48+
// Create new account_info record
49+
await insertAccountInfo({
50+
account_id: accountId,
51+
organization,
52+
image,
53+
instruction,
54+
job_title: jobTitle,
55+
role_type: roleType,
56+
company_name: companyName,
57+
knowledges,
58+
});
59+
} else {
60+
// Update existing account_info
61+
await updateAccountInfo(accountId, {
62+
organization,
63+
image,
64+
instruction,
65+
job_title: jobTitle,
66+
role_type: roleType,
67+
company_name: companyName,
68+
knowledges,
69+
});
70+
}
71+
72+
// Fetch the updated account with all joined info
73+
const updated = await getAccountWithDetails(accountId);
74+
75+
return NextResponse.json({ data: updated }, { status: 200, headers: getCorsHeaders() });
76+
} catch (error) {
77+
const message = error instanceof Error ? error.message : "failed";
78+
return NextResponse.json({ message }, { status: 400, headers: getCorsHeaders() });
79+
}
80+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { NextResponse } from "next/server";
2+
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
3+
import { z } from "zod";
4+
5+
export const addArtistBodySchema = z.object({
6+
email: z.string().email("email must be a valid email address"),
7+
artistId: z.string().uuid("artistId must be a valid UUID"),
8+
});
9+
10+
export type AddArtistBody = z.infer<typeof addArtistBodySchema>;
11+
12+
/**
13+
* Validates request body for POST /api/accounts/artists.
14+
*
15+
* @param body - The request body
16+
* @returns A NextResponse with an error if validation fails, or the validated body if validation passes.
17+
*/
18+
export function validateAddArtistBody(body: unknown): NextResponse | AddArtistBody {
19+
const result = addArtistBodySchema.safeParse(body);
20+
21+
if (!result.success) {
22+
const firstError = result.error.issues[0];
23+
return NextResponse.json(
24+
{
25+
status: "error",
26+
missing_fields: firstError.path,
27+
error: firstError.message,
28+
},
29+
{
30+
status: 400,
31+
headers: getCorsHeaders(),
32+
},
33+
);
34+
}
35+
36+
return result.data;
37+
}

0 commit comments

Comments
 (0)