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
136 changes: 136 additions & 0 deletions src/app/actions/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
"use server";

import { createSession, destroySession, getSession } from "@/lib/session";
import { getTokenStore } from "@/lib/token-store";
import { sonarConfig } from "@/lib/config";
import { generatePKCEParams, buildAuthorizationUrl } from "@echoxyz/sonar-core";
import { setPKCEVerifier, getPKCEVerifier, clearPKCEVerifier } from "@/lib/pkce-store";
import { createSonarClient } from "@/lib/sonar";
import { SonarTokens } from "@/lib/token-store";

/**
* Create a new session (login).
* Given this is an example app - no authentication is required.
*/
export async function login(): Promise<{ userId: string }> {
const session = await createSession();
return { userId: session.userId };
}

/**
* Destroy the current session (logout).
* Clears session cookie and any associated Sonar tokens.
*/
export async function logout(): Promise<void> {
await destroySession();
}

/**
* Get current session status.
* Returns session info and Sonar connection status.
*/
export async function getSessionStatus(): Promise<{ authenticated: boolean; sonarConnected: boolean }> {
const session = await getSession();

if (!session) {
return {
authenticated: false,
sonarConnected: false,
};
}

const tokens = getTokenStore().getTokens(session.userId);

return {
authenticated: true,
sonarConnected: !!tokens,
};
}

/**
* Generate Sonar OAuth authorization URL with PKCE
*/
export async function getSonarAuthorizationUrl(): Promise<string> {
const session = await getSession();

if (!session) {
throw new Error("Unauthorized");
}

// Generate PKCE parameters (includes state token from sonar-core)
const { codeVerifier, codeChallenge, state } = await generatePKCEParams();

// Store code verifier and user ID linked to state token (will be retrieved in callback)
setPKCEVerifier(state, session.userId, codeVerifier);

// Build authorization URL with PKCE
const authorizationUrl = buildAuthorizationUrl({
clientUUID: sonarConfig.clientUUID,
redirectURI: sonarConfig.redirectURI,
state,
codeChallenge,
frontendURL: sonarConfig.frontendURL,
});

return authorizationUrl.toString();
}

/**
* Handle OAuth callback from Sonar
* Exchange authorization code for access/refresh tokens using PKCE
*/
export async function handleSonarCallback(code: string, state: string): Promise<void> {
// Verify session exists
const session = await getSession();
if (!session) {
throw new Error("Unauthorized: No active session");
}

// Retrieve code verifier and session ID from cookie store using state token
const stateData = getPKCEVerifier(state);
if (!stateData) {
throw new Error("Invalid state: OAuth state token not found or expired");
}

// Verify the state token belongs to the current session
if (stateData.userId !== session.userId) {
throw new Error("Invalid session: State token does not match current session");
}

const { codeVerifier } = stateData;

// Create a temporary client to exchange the authorization code
const client = createSonarClient(session.userId);
const tokenData = await client.exchangeAuthorizationCode({
code,
codeVerifier,
redirectURI: sonarConfig.redirectURI,
});

const expiresAt = Math.floor(Date.now() / 1000) + tokenData.expires_in;

const sonarTokens: SonarTokens = {
accessToken: tokenData.access_token,
refreshToken: tokenData.refresh_token,
expiresAt,
};

// Store tokens in token store
getTokenStore().setTokens(session.userId, sonarTokens);

// Clear the code verifier (no longer needed)
clearPKCEVerifier(state);
}

/**
* Disconnect Sonar account (remove stored tokens)
*/
export async function disconnectSonar(): Promise<void> {
const session = await getSession();

if (!session) {
throw new Error("Unauthorized");
}

getTokenStore().clearTokens(session.userId);
}
76 changes: 76 additions & 0 deletions src/app/actions/sonar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"use server";

import { createSonarServerAction } from "@/lib/sonar";
import {
ListAvailableEntitiesResponse,
ReadEntityResponse,
PrePurchaseCheckResponse,
GeneratePurchasePermitResponse,
APIError,
} from "@echoxyz/sonar-core";

type ListAvailableEntitiesInput = { saleUUID: string };

/**
* Fetch all entities for the authenticated user
*/
export const getEntities = createSonarServerAction<ListAvailableEntitiesInput, ListAvailableEntitiesResponse>(
async (client, { saleUUID }) => {
if (!saleUUID) {
throw new Error("Missing saleUUID");
}
return client.listAvailableEntities({ saleUUID });
}
);

type ReadEntityInput = { saleUUID: string; walletAddress: string };

/**
* Fetch a specific entity by wallet address
*/
export const getEntity = createSonarServerAction<ReadEntityInput, ReadEntityResponse>(
async (client, { saleUUID, walletAddress }) => {
if (!saleUUID || !walletAddress) {
throw new Error("Missing saleUUID or walletAddress");
}

try {
return await client.readEntity({ saleUUID, walletAddress });
} catch (error) {
// Special handling: 404 returns null entity instead of error
if (error instanceof APIError && error.status === 404) {
return { Entity: null } as unknown as ReadEntityResponse;
}
throw error;
}
}
);

type PrePurchaseCheckInput = { saleUUID: string; entityID: string; walletAddress: string };

/**
* Perform pre-purchase check for an entity
*/
export const prePurchaseCheck = createSonarServerAction<PrePurchaseCheckInput, PrePurchaseCheckResponse>(
async (client, { saleUUID, entityID, walletAddress }) => {
if (!saleUUID || !entityID || !walletAddress) {
throw new Error("Missing required parameters");
}
return client.prePurchaseCheck({ saleUUID, entityID, walletAddress });
}
);

type GeneratePurchasePermitInput = { saleUUID: string; entityID: string; walletAddress: string };

/**
* Generate a purchase permit for an entity
*/
export const generatePurchasePermit = createSonarServerAction<
GeneratePurchasePermitInput,
GeneratePurchasePermitResponse
>(async (client, { saleUUID, entityID, walletAddress }) => {
if (!saleUUID || !entityID || !walletAddress) {
throw new Error("Missing required parameters");
}
return client.generatePurchasePermit({ saleUUID, entityID, walletAddress });
});
15 changes: 0 additions & 15 deletions src/app/api/auth/login/route.ts

This file was deleted.

12 changes: 0 additions & 12 deletions src/app/api/auth/logout/route.ts

This file was deleted.

25 changes: 0 additions & 25 deletions src/app/api/auth/session/route.ts

This file was deleted.

33 changes: 0 additions & 33 deletions src/app/api/auth/sonar/authorize/route.ts

This file was deleted.

88 changes: 0 additions & 88 deletions src/app/api/auth/sonar/callback/route.ts

This file was deleted.

Loading