11import { NextRequest, NextResponse } from 'next/server';
2- import { Keypair } from '@stellar/stellar-sdk';
3- import { getAndClearNonce } from '@/lib/auth-cache';
4- import {
5- createSession,
6- getSessionCookieHeader,
7- } from '@/lib/session';
8-
92import { Keypair, StrKey } from '@stellar/stellar-sdk';
10- import { getNonce, deleteNonce } from '@/lib/auth/nonce-store';
11- import { getTranslator } from '@/lib/i18n';
12- import {
13- createSession,
14- getSessionCookieHeader,
15- } from '../../../../lib/session';
3+ import { getAndClearNonce } from '@/lib/auth-cache';
4+ import { createSession, getSessionCookieHeader } from '@/lib/session';
5+ import { prisma } from '@/lib/prisma';
166
177// Force dynamic rendering for this route
188export const dynamic = 'force-dynamic';
@@ -21,123 +11,87 @@ export const runtime = 'nodejs';
2111/**
2212 * POST /api/auth/login
2313 * Verify a signature and authenticate user
24- *
25- * Request Body:
26- * - address: Stellar public key
27- * - message: The nonce that was signed
28- * - signature: Base64-encoded signature
29- */
30-
31- export const dynamic = 'force-dynamic';
32-
33- /**
34- * Wallet-based auth flow:
35- * 1. Frontend: user connects wallet (e.g. Freighter), gets address.
36- * 2. Frontend: GET /api/auth/nonce?address={address} to get a random nonce.
37- * 3. Frontend: sign the hex nonce with wallet, encode as base64.
38- * 4. Frontend: POST /api/auth/login with { address, signature }.
39- * 5. Backend: verify signature with Keypair; create encrypted session cookie.
4014 */
4115export async function POST(request: NextRequest) {
4216 try {
4317 const body = await request.json();
4418 const { address, message, signature } = body;
45- const t = getTranslator(request.headers.get('accept-language'));
4619
4720 if (!address || !message || !signature) {
4821 return NextResponse.json(
49- { error: t('errors.address_signature_required') || 'Missing required fields: address, message, signature' },
22+ { error: 'Missing required fields: address, message, signature' },
5023 { status: 400 }
5124 );
5225 }
5326
5427 // Validate Stellar address format
5528 if (!StrKey.isValidEd25519PublicKey(address)) {
5629 return NextResponse.json(
57- { error: t('errors.invalid_address_format') || 'Invalid Stellar address format' },
30+ { error: 'Invalid Stellar address format' },
5831 { status: 400 }
5932 );
6033 }
6134
62- // Verify nonce exists and hasn't expired
63- const storedNonce = getNonce (address);
35+ // Verify nonce exists and matches (atomic read + delete)
36+ const storedNonce = getAndClearNonce (address);
6437 if (!storedNonce || storedNonce !== message) {
6538 return NextResponse.json(
66- { error: t('errors.nonce_expired') || 'Invalid or expired nonce' },
39+ { error: 'Invalid or expired nonce' },
6740 { status: 401 }
6841 );
6942 }
7043
71- try {
72- // Verify the signature
73- const keypair = Keypair.fromPublicKey(address);
74- const messageBuffer = Buffer.from(message, 'utf8');
75- const signatureBuffer = Buffer.from(signature, 'base64');
76-
77- const isValid = keypair.verify(messageBuffer, signatureBuffer);
78-
79- if (!isValid) {
80- return NextResponse.json(
81- { error: t('errors.invalid_signature') || 'Invalid signature' },
82- { status: 401 }
83- );
84- }
44+ // Verify signature
45+ // The client signs Buffer.from(nonce, 'utf8') so we must decode the same way
46+ const keypair = Keypair.fromPublicKey(address);
47+ const messageBuffer = Buffer.from(message, 'utf8');
48+ const signatureBuffer = Buffer.from(signature, 'base64');
8549
86- // Delete used nonce (one-time use)
87- deleteNonce(address);
50+ const isValid = keypair.verify(messageBuffer, signatureBuffer);
8851
89- // Create session cookie like from HEAD
90- const sealed = await createSession(address);
91- const cookieHeader = getSessionCookieHeader(sealed);
92-
93- return new Response(
94- JSON.stringify({
95- success: true,
96- token: `mock-jwt-${address.substring(0, 10)}`, // Keeping this property for compatibility with main branch frontend changes
97- address
98- }),
99- {
100- status: 200,
101- headers: {
102- 'Content-Type': 'application/json',
103- 'Set-Cookie': cookieHeader,
104- },
105- }
106- );
107-
108- } catch (verifyError) {
109- console.error('Signature verification error:', verifyError);
52+ if (!isValid) {
11053 return NextResponse.json(
111- { error: t('errors.signature_verification_failed') || 'Invalid signature' },
54+ { error: 'Invalid signature' },
11255 { status: 401 }
11356 );
11457 }
11558
59+ // Upsert user in database (best-effort — don't fail login if DB is unavailable)
60+ try {
61+ await prisma.user.upsert({
62+ where: { stellar_address: address },
63+ update: {},
64+ create: {
65+ stellar_address: address,
66+ preferences: {
67+ create: {},
68+ },
69+ },
70+ });
71+ } catch (dbErr) {
72+ console.warn('DB upsert skipped (non-fatal):', dbErr);
73+ }
74+
75+ // Create encrypted session
11676 const sealed = await createSession(address);
117- const cookieHeader = getSessionCookieHeader(sealed);
11877
119- return new Response(
120- JSON.stringify({ success: true, address, token: sealed }),
121- {
122- status: 200,
123- headers: {
124- 'Content-Type': 'application/json',
125- 'Set-Cookie': cookieHeader,
126- },
127- }
128- );
129- } catch (err) {
130- console.error('Login error:', err);
131- return NextResponse.json(
132- { error: 'Internal server error' },
133- { status: 500 }
78+ const response = NextResponse.json({
79+ success: true,
80+ address,
81+ });
82+
83+ response.headers.set(
84+ 'Set-Cookie',
85+ getSessionCookieHeader(sealed)
13486 );
87+
88+ return response;
89+
13590 } catch (error) {
136- console.error('Error during login:', error);
137- const t = getTranslator(request.headers.get('accept-language'));
91+ console.error('Login error:', error);
13892 return NextResponse.json(
139- { error: t('errors.internal_server_error') || 'Internal Server Error' },
93+ { error: 'Internal Server Error' },
14094 { status: 500 }
14195 );
14296 }
143- }
97+ }
0 commit comments