Skip to content

Commit 6cbfffd

Browse files
authored
Merge pull request #242 from codewithzubair07/feature/user-onboarding-profile-api-155
feat: implement user onboarding and profile API with Prisma and sessi…
2 parents b88eef3 + 4927e0c commit 6cbfffd

File tree

21 files changed

+14098
-26958
lines changed

21 files changed

+14098
-26958
lines changed

.env.example

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,47 @@
1+
# ============================================
12
# Anchor API variables
3+
# ============================================
24
ANCHOR_API_BASE_URL=https://api.example.com/anchor
3-
STELLAR_NETWORK="testnet"
4-
STELLAR_RPC_URL="https://soroban-testnet.stellar.org"
5-
INSURANCE_CONTRACT_ID="CACDYF3CYMJEJTIVFESQYZTN67GO2R5D5IUABTCUG3HXQSRXCSOROBAN"
6-
REMITTANCE_CONTRACT_ID="CACDYF3CYMJEJTIVFESQYZTN67GO2R5D5IUABTCUG3HXQSRXCSOROBAN"
7-
SAVINGS_CONTRACT_ID="CACDYF3CYMJEJTIVFESQYZTN67GO2R5D5IUABTCUG3HXQSRXCSOROBAN"
8-
BILLS_CONTRACT_ID="CACDYF3CYMJEJTIVFESQYZTN67GO2R5D5IUABTCUG3HXQSRXCSOROBAN"
95

10-
# Session encryption for wallet-based auth (required for /api/auth/*).
11-
# Generate with: openssl rand -base64 32
12-
SESSION_PASSWORD=your-32-char-minimum-secret-here
13-
14-
# Shared secret used to verify Anchor webhooks
15-
ANCHOR_WEBHOOK_SECRET=your_shared_anchor_secret_here
16-
17-
# Soroban/Stellar Configuration
6+
# ============================================
7+
# Stellar / Soroban Network
8+
# ============================================
9+
STELLAR_NETWORK=testnet
10+
STELLAR_RPC_URL=https://soroban-testnet.stellar.org
1811
SOROBAN_RPC_URL=https://soroban-testnet.stellar.org
1912
SOROBAN_NETWORK_PASSPHRASE=Test SDF Network ; September 2015
2013

21-
# USDC Token Configuration (optional, defaults to testnet USDC issuer)
14+
# ============================================
15+
# Contract IDs (Testnet placeholders)
16+
# ============================================
17+
INSURANCE_CONTRACT_ID=CACDYF3CYMJEJTIVFESQYZTN67GO2R5D5IUABTCUG3HXQSRXCSOROBAN
18+
REMITTANCE_CONTRACT_ID=CACDYF3CYMJEJTIVFESQYZTN67GO2R5D5IUABTCUG3HXQSRXCSOROBAN
19+
SAVINGS_CONTRACT_ID=CACDYF3CYMJEJTIVFESQYZTN67GO2R5D5IUABTCUG3HXQSRXCSOROBAN
20+
BILLS_CONTRACT_ID=CACDYF3CYMJEJTIVFESQYZTN67GO2R5D5IUABTCUG3HXQSRXCSOROBAN
21+
22+
# ============================================
23+
# Session Encryption (MUST be 32+ characters)
24+
# ============================================
25+
SESSION_PASSWORD=supersecurelongsessionpasswordatleast32characters!!
26+
27+
# ============================================
28+
# Auth Secret (used in tests / API)
29+
# ============================================
30+
AUTH_SECRET=test-secret-for-local-dev-only
31+
32+
# ============================================
33+
# Anchor Webhook Secret
34+
# ============================================
35+
ANCHOR_WEBHOOK_SECRET=local-anchor-webhook-secret
36+
37+
# ============================================
38+
# USDC Token (Testnet)
39+
# ============================================
2240
USDC_ISSUER_ADDRESS=GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5
23-
# API Middleware Configuration
24-
# Frontend application URL for CORS policy (required for /api routes)
25-
# Requests from this origin will be allowed by CORS middleware
26-
NEXT_PUBLIC_APP_URL=http://localhost:3000
2741

28-
# Maximum request body size for POST/PUT/PATCH in bytes (default 1MB)
29-
# Requests exceeding this size will receive a 413 Payload Too Large response
42+
# ============================================
43+
# Frontend / API Config
44+
# ============================================
45+
NEXT_PUBLIC_APP_URL=http://localhost:3000
3046
API_MAX_BODY_SIZE=1048576
47+

.env.local.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ NEXT_PUBLIC_STELLAR_RPC_URL=https://soroban-testnet.stellar.org
44

55
# Soroban Contract Addresses
66
NEXT_PUBLIC_SAVINGS_GOALS_CONTRACT_ID=your_contract_id_here
7+

.eslintrc.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
11
{
2-
"extends": "next/core-web-vitals"
3-
}
4-
"extends": "next/core-web-vitals"
2+
"extends": ["next/core-web-vitals"]
53
}

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,7 @@ yarn-error.log*
3535
*.tsbuildinfo
3636
next-env.d.ts
3737

38+
# Prisma / SQLite
39+
prisma/dev.db
40+
prisma/dev.db-journal
41+
*.db

app/api/auth/login/route.ts

Lines changed: 47 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,8 @@
11
import { 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-
92
import { 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
188
export 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
*/
4115
export 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

Comments
 (0)