Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions app/api/accounts/artists/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { safeParseJson } from "@/lib/networking/safeParseJson";
import { validateAddArtistBody, type AddArtistBody } from "@/lib/accounts/validateAddArtistBody";
import { addArtistToAccountHandler } from "@/lib/accounts/addArtistToAccountHandler";

/**
* POST /api/accounts/artists
*
* Add an artist to an account's list of associated artists.
* If the artist is already associated with the account, returns success.
*
* @param req - The incoming request with email and artistId in body
* @returns NextResponse with success status or error
*/
export async function POST(req: NextRequest) {
const body = await safeParseJson(req);

const validated = validateAddArtistBody(body);
if (validated instanceof NextResponse) {
return validated;
}

return addArtistToAccountHandler(validated as AddArtistBody);
}

/**
* OPTIONS handler for CORS preflight requests.
*
* @returns NextResponse with CORS headers
*/
export async function OPTIONS() {
return new NextResponse(null, {
status: 204,
headers: getCorsHeaders(),
});
}

export const dynamic = "force-dynamic";
export const fetchCache = "force-no-store";
export const revalidate = 0;
70 changes: 70 additions & 0 deletions app/api/accounts/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { safeParseJson } from "@/lib/networking/safeParseJson";
import {
validateCreateAccountBody,
type CreateAccountBody,
} from "@/lib/accounts/validateCreateAccountBody";
import {
validateUpdateAccountBody,
type UpdateAccountBody,
} from "@/lib/accounts/validateUpdateAccountBody";
import { createAccountHandler } from "@/lib/accounts/createAccountHandler";
import { updateAccountHandler } from "@/lib/accounts/updateAccountHandler";

/**
* POST /api/accounts
*
* Create a new account or retrieve an existing account by email or wallet.
* If an account with the provided email or wallet exists, returns that account.
* Otherwise creates a new account and initializes credits.
*
* @param req - The incoming request with email and/or wallet in body
* @returns Account data
*/
export async function POST(req: NextRequest) {
const body = await safeParseJson(req);

const validated = validateCreateAccountBody(body);
if (validated instanceof NextResponse) {
return validated;
}

return createAccountHandler(validated as CreateAccountBody);
}

/**
* PATCH /api/accounts
*
* Update an existing account's profile information.
* Requires accountId in the body along with fields to update.
*
* @param req - The incoming request with accountId and update fields
* @returns NextResponse with updated account data or error
*/
export async function PATCH(req: NextRequest) {
const body = await safeParseJson(req);

const validated = validateUpdateAccountBody(body);
if (validated instanceof NextResponse) {
return validated;
}

return updateAccountHandler(validated as UpdateAccountBody);
}

/**
* OPTIONS handler for CORS preflight requests.
*
* @returns NextResponse with CORS headers
*/
export async function OPTIONS() {
return new NextResponse(null, {
status: 204,
headers: getCorsHeaders(),
});
}

export const dynamic = "force-dynamic";
export const fetchCache = "force-no-store";
export const revalidate = 0;
45 changes: 45 additions & 0 deletions lib/accounts/addArtistToAccountHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { selectAccountByEmail } from "@/lib/supabase/account_emails/selectAccountByEmail";
import { getAccountArtistIds } from "@/lib/supabase/account_artist_ids/getAccountArtistIds";
import { insertAccountArtistId } from "@/lib/supabase/account_artist_ids/insertAccountArtistId";
import type { AddArtistBody } from "./validateAddArtistBody";

/**
* Handles POST /api/accounts/artists - Add artist to account's artist list.
*
* @param body - Validated request body with email and artistId
* @returns NextResponse with success status
*/
export async function addArtistToAccountHandler(body: AddArtistBody): Promise<NextResponse> {
const { email, artistId } = body;

try {
// Find account by email
const accountEmail = await selectAccountByEmail(email);
if (!accountEmail?.account_id) {
return NextResponse.json(
{ message: "Not found account." },
{ status: 400, headers: getCorsHeaders() },
);
}

const accountId = accountEmail.account_id;

// Check if artist is already associated with account
const existingArtists = await getAccountArtistIds({ accountIds: [accountId] });
const alreadyExists = existingArtists.some(a => a.artist_id === artistId);

if (alreadyExists) {
return NextResponse.json({ success: true }, { status: 200, headers: getCorsHeaders() });
}

// Add artist to account
await insertAccountArtistId(accountId, artistId);

return NextResponse.json({ success: true }, { status: 200, headers: getCorsHeaders() });
} catch (error) {
const message = error instanceof Error ? error.message : "failed";
return NextResponse.json({ message }, { status: 400, headers: getCorsHeaders() });
}
}
111 changes: 111 additions & 0 deletions lib/accounts/createAccountHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { selectAccountByEmail } from "@/lib/supabase/account_emails/selectAccountByEmail";
import { selectAccountByWallet } from "@/lib/supabase/account_wallets/selectAccountByWallet";
import { getAccountWithDetails } from "@/lib/supabase/accounts/getAccountWithDetails";
import { insertAccount } from "@/lib/supabase/accounts/insertAccount";
import { insertAccountEmail } from "@/lib/supabase/account_emails/insertAccountEmail";
import { insertAccountWallet } from "@/lib/supabase/account_wallets/insertAccountWallet";
import { insertCreditsUsage } from "@/lib/supabase/credits_usage/insertCreditsUsage";
import { assignAccountToOrg } from "@/lib/organizations/assignAccountToOrg";
import type { CreateAccountBody } from "./validateCreateAccountBody";

/**
* Account data response shape.
*/
export type AccountDataResponse = {
id: string;
account_id: string;
name?: string;
email?: string;
wallet?: string;
image?: string;
instruction?: string;
organization?: string;
};

/**
* Handles POST /api/accounts - Create or retrieve account by email/wallet.
*
* @param body - Validated request body with email and/or wallet
* @returns NextResponse with account data
*/
export async function createAccountHandler(body: CreateAccountBody): Promise<NextResponse> {
const { email, wallet } = body;

try {
// If email is provided, check account_emails
if (email) {
const emailRecord = await selectAccountByEmail(email);
if (emailRecord?.account_id) {
// Assign to org based on email domain (idempotent)
await assignAccountToOrg(emailRecord.account_id, email);

const accountData = await getAccountWithDetails(emailRecord.account_id);
if (accountData) {
return NextResponse.json(
{ data: accountData },
{ status: 200, headers: getCorsHeaders() },
);
}
}
}

// If wallet is provided, check account_wallets
if (wallet) {
try {
const account = await selectAccountByWallet(wallet);

// Flatten the nested relations into a single object
const accountInfo = account.account_info?.[0];
const accountEmail = account.account_emails?.[0];
const accountWallet = account.account_wallets?.[0];

const accountData: AccountDataResponse = {
id: account.id,
account_id: account.id,
name: account.name || undefined,
image: accountInfo?.image || undefined,
instruction: accountInfo?.instruction || undefined,
organization: accountInfo?.organization || undefined,
email: accountEmail?.email || undefined,
wallet: accountWallet?.wallet || undefined,
};

return NextResponse.json({ data: accountData }, { status: 200, headers: getCorsHeaders() });
} catch {
// Wallet not found, continue to create new account
}
}

// Create new account
const newAccount = await insertAccount({ name: "" });

if (email) {
await insertAccountEmail(newAccount.id, email);
// Assign new account to org based on email domain
await assignAccountToOrg(newAccount.id, email);
}

if (wallet) {
await insertAccountWallet(newAccount.id, wallet);
}

await insertCreditsUsage(newAccount.id);

const newAccountData: AccountDataResponse = {
id: newAccount.id,
account_id: newAccount.id,
email: email || "",
wallet: wallet || "",
image: "",
instruction: "",
organization: "",
};

return NextResponse.json({ data: newAccountData }, { status: 200, headers: getCorsHeaders() });
} catch (error) {
const message = error instanceof Error ? error.message : "failed";
return NextResponse.json({ message }, { status: 400, headers: getCorsHeaders() });
}
}
80 changes: 80 additions & 0 deletions lib/accounts/updateAccountHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { getAccountWithDetails } from "@/lib/supabase/accounts/getAccountWithDetails";
import { updateAccount } from "@/lib/supabase/accounts/updateAccount";
import { insertAccountInfo } from "@/lib/supabase/account_info/insertAccountInfo";
import { updateAccountInfo } from "@/lib/supabase/account_info/updateAccountInfo";
import { selectAccountInfo } from "@/lib/supabase/account_info/selectAccountInfo";
import type { UpdateAccountBody } from "./validateUpdateAccountBody";

/**
* Handles PATCH /api/accounts - Update account profile information.
*
* @param body - Validated request body with accountId and fields to update
* @returns NextResponse with updated account data
*/
export async function updateAccountHandler(body: UpdateAccountBody): Promise<NextResponse> {
const {
accountId,
name,
instruction,
organization,
image,
jobTitle,
roleType,
companyName,
knowledges,
} = body;

try {
// Verify account exists
const found = await getAccountWithDetails(accountId);
if (!found) {
return NextResponse.json(
{ data: null, message: "Account not found" },
{ status: 400, headers: getCorsHeaders() },
);
}

// Update account name if provided
if (name !== undefined) {
await updateAccount(accountId, { name });
}

// Check if account_info exists
const existingInfo = await selectAccountInfo(accountId);

if (!existingInfo) {
// Create new account_info record
await insertAccountInfo({
account_id: accountId,
organization,
image,
instruction,
job_title: jobTitle,
role_type: roleType,
company_name: companyName,
knowledges,
});
} else {
// Update existing account_info
await updateAccountInfo(accountId, {
organization,
image,
instruction,
job_title: jobTitle,
role_type: roleType,
company_name: companyName,
knowledges,
});
}

// Fetch the updated account with all joined info
const updated = await getAccountWithDetails(accountId);

return NextResponse.json({ data: updated }, { status: 200, headers: getCorsHeaders() });
} catch (error) {
const message = error instanceof Error ? error.message : "failed";
return NextResponse.json({ message }, { status: 400, headers: getCorsHeaders() });
}
}
37 changes: 37 additions & 0 deletions lib/accounts/validateAddArtistBody.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { z } from "zod";

export const addArtistBodySchema = z.object({
email: z.string().email("email must be a valid email address"),
artistId: z.string().uuid("artistId must be a valid UUID"),
});

export type AddArtistBody = z.infer<typeof addArtistBodySchema>;

/**
* Validates request body for POST /api/accounts/artists.
*
* @param body - The request body
* @returns A NextResponse with an error if validation fails, or the validated body if validation passes.
*/
export function validateAddArtistBody(body: unknown): NextResponse | AddArtistBody {
const result = addArtistBodySchema.safeParse(body);

if (!result.success) {
const firstError = result.error.issues[0];
return NextResponse.json(
{
status: "error",
missing_fields: firstError.path,
error: firstError.message,
},
{
status: 400,
headers: getCorsHeaders(),
},
);
}

return result.data;
}
Loading
Loading