Skip to content

Commit 208e3a5

Browse files
Merge pull request #41 from Dayz-tech-co/feature/auth-middleware-31
Feature/auth middleware 31
2 parents e401ea7 + b5923ed commit 208e3a5

20 files changed

Lines changed: 1220 additions & 205 deletions

File tree

app/api/auth/logout/route.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { NextRequest, NextResponse } from 'next/server'
2+
import {
3+
clearSessionCookies,
4+
readRefreshToken,
5+
revokeSession,
6+
} from '@/lib/auth/session'
7+
8+
export async function POST(request: NextRequest): Promise<NextResponse> {
9+
try {
10+
const refreshToken = readRefreshToken(request)
11+
if (refreshToken) {
12+
await revokeSession(refreshToken)
13+
}
14+
15+
const response = NextResponse.json({ ok: true }, { status: 200 })
16+
clearSessionCookies(response)
17+
return response
18+
} catch {
19+
return NextResponse.json(
20+
{
21+
error: 'Failed to log out',
22+
code: 'LOGOUT_FAILED',
23+
},
24+
{ status: 500 }
25+
)
26+
}
27+
}

app/api/auth/me/route.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { NextResponse } from 'next/server'
2+
import { withAuth } from '@/lib/auth/middleware'
3+
4+
export const GET = withAuth(async (_request, auth) => {
5+
return NextResponse.json(
6+
{
7+
walletAddress: auth.walletAddress,
8+
authenticated: true,
9+
},
10+
{ status: 200 }
11+
)
12+
})

app/api/auth/nonce/route.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { NextRequest, NextResponse } from 'next/server'
2+
import { NONCE_TTL_SECONDS } from '@/lib/auth/constants'
3+
import { randomNonce, sha256Hex } from '@/lib/auth/crypto'
4+
import { saveNonce } from '@/lib/auth/store'
5+
import {
6+
buildAuthMessage,
7+
isValidStellarAddress,
8+
normalizeWalletAddress,
9+
} from '@/lib/auth/stellar'
10+
11+
interface NonceRequestBody {
12+
walletAddress?: string
13+
}
14+
15+
export async function POST(request: NextRequest): Promise<NextResponse> {
16+
try {
17+
const body: NonceRequestBody = await request.json()
18+
const walletAddress = body.walletAddress?.trim()
19+
20+
if (!walletAddress || !isValidStellarAddress(walletAddress)) {
21+
return NextResponse.json(
22+
{
23+
error: 'Invalid wallet address',
24+
code: 'INVALID_WALLET_ADDRESS',
25+
},
26+
{ status: 400 }
27+
)
28+
}
29+
30+
const normalizedWallet = normalizeWalletAddress(walletAddress)
31+
const nonce = randomNonce()
32+
const expiresAt = new Date(Date.now() + NONCE_TTL_SECONDS * 1000)
33+
await saveNonce({
34+
walletAddress: normalizedWallet,
35+
nonceHash: sha256Hex(nonce),
36+
expiresAt,
37+
})
38+
39+
return NextResponse.json(
40+
{
41+
walletAddress: normalizedWallet,
42+
nonce,
43+
message: buildAuthMessage(normalizedWallet, nonce),
44+
expiresAt: expiresAt.toISOString(),
45+
},
46+
{ status: 200 }
47+
)
48+
} catch {
49+
return NextResponse.json(
50+
{
51+
error: 'Failed to create auth nonce',
52+
code: 'NONCE_ISSUE_FAILED',
53+
},
54+
{ status: 500 }
55+
)
56+
}
57+
}

app/api/auth/refresh/route.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { NextRequest, NextResponse } from 'next/server'
2+
import {
3+
readRefreshToken,
4+
rotateSession,
5+
setSessionCookies,
6+
} from '@/lib/auth/session'
7+
8+
export async function POST(request: NextRequest): Promise<NextResponse> {
9+
try {
10+
const refreshToken = readRefreshToken(request)
11+
if (!refreshToken) {
12+
return NextResponse.json(
13+
{
14+
error: 'Refresh token is required',
15+
code: 'REFRESH_TOKEN_REQUIRED',
16+
},
17+
{ status: 401 }
18+
)
19+
}
20+
21+
const session = await rotateSession(request, refreshToken)
22+
if (!session) {
23+
return NextResponse.json(
24+
{
25+
error: 'Invalid or expired refresh token',
26+
code: 'INVALID_REFRESH_TOKEN',
27+
},
28+
{ status: 401 }
29+
)
30+
}
31+
32+
const response = NextResponse.json(
33+
{
34+
walletAddress: session.walletAddress,
35+
accessTokenExpiresAt: session.accessTokenExpiresAt.toISOString(),
36+
refreshTokenExpiresAt: session.refreshTokenExpiresAt.toISOString(),
37+
},
38+
{ status: 200 }
39+
)
40+
setSessionCookies(response, session)
41+
42+
return response
43+
} catch {
44+
return NextResponse.json(
45+
{
46+
error: 'Failed to refresh session',
47+
code: 'SESSION_REFRESH_FAILED',
48+
},
49+
{ status: 500 }
50+
)
51+
}
52+
}

app/api/auth/verify/route.ts

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { NextRequest, NextResponse } from 'next/server'
2+
import { createSession, setSessionCookies } from '@/lib/auth/session'
3+
import { consumeNonce, hasActiveNonce } from '@/lib/auth/store'
4+
import { sha256Hex } from '@/lib/auth/crypto'
5+
import {
6+
buildAuthMessage,
7+
isValidStellarAddress,
8+
normalizeWalletAddress,
9+
verifyStellarSignature,
10+
} from '@/lib/auth/stellar'
11+
12+
interface VerifyRequestBody {
13+
walletAddress?: string
14+
nonce?: string
15+
signature?: string
16+
message?: string
17+
}
18+
19+
export async function POST(request: NextRequest): Promise<NextResponse> {
20+
try {
21+
const body: VerifyRequestBody = await request.json()
22+
23+
const walletAddress = body.walletAddress?.trim()
24+
const nonce = body.nonce?.trim()
25+
const signature = body.signature?.trim()
26+
27+
if (!walletAddress || !nonce || !signature) {
28+
return NextResponse.json(
29+
{
30+
error: 'Missing walletAddress, nonce, or signature',
31+
code: 'INVALID_AUTH_PAYLOAD',
32+
},
33+
{ status: 400 }
34+
)
35+
}
36+
37+
if (!isValidStellarAddress(walletAddress)) {
38+
return NextResponse.json(
39+
{
40+
error: 'Invalid wallet address',
41+
code: 'INVALID_WALLET_ADDRESS',
42+
},
43+
{ status: 400 }
44+
)
45+
}
46+
47+
const normalizedWallet = normalizeWalletAddress(walletAddress)
48+
const nonceHash = sha256Hex(nonce)
49+
const expectedMessage = buildAuthMessage(normalizedWallet, nonce)
50+
51+
if (body.message && body.message !== expectedMessage) {
52+
return NextResponse.json(
53+
{
54+
error: 'Signed message does not match expected format',
55+
code: 'MESSAGE_MISMATCH',
56+
},
57+
{ status: 400 }
58+
)
59+
}
60+
61+
const nonceIsValid = await hasActiveNonce({
62+
walletAddress: normalizedWallet,
63+
nonceHash,
64+
})
65+
if (!nonceIsValid) {
66+
return NextResponse.json(
67+
{
68+
error: 'Nonce is invalid, expired, or already used',
69+
code: 'INVALID_NONCE',
70+
},
71+
{ status: 401 }
72+
)
73+
}
74+
75+
const signatureIsValid = verifyStellarSignature({
76+
walletAddress: normalizedWallet,
77+
message: expectedMessage,
78+
signature,
79+
})
80+
if (!signatureIsValid) {
81+
return NextResponse.json(
82+
{
83+
error: 'Invalid wallet signature',
84+
code: 'INVALID_SIGNATURE',
85+
},
86+
{ status: 401 }
87+
)
88+
}
89+
90+
const consumed = await consumeNonce({
91+
walletAddress: normalizedWallet,
92+
nonceHash,
93+
})
94+
if (!consumed) {
95+
return NextResponse.json(
96+
{
97+
error: 'Nonce is invalid, expired, or already used',
98+
code: 'INVALID_NONCE',
99+
},
100+
{ status: 401 }
101+
)
102+
}
103+
104+
const session = await createSession(request, normalizedWallet)
105+
const response = NextResponse.json(
106+
{
107+
walletAddress: normalizedWallet,
108+
accessTokenExpiresAt: session.accessTokenExpiresAt.toISOString(),
109+
refreshTokenExpiresAt: session.refreshTokenExpiresAt.toISOString(),
110+
},
111+
{ status: 200 }
112+
)
113+
setSessionCookies(response, session)
114+
115+
return response
116+
} catch {
117+
return NextResponse.json(
118+
{
119+
error: 'Authentication failed',
120+
code: 'AUTH_VERIFICATION_FAILED',
121+
},
122+
{ status: 500 }
123+
)
124+
}
125+
}

app/layout.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
import React from "react"
22
import type { Metadata } from 'next'
3-
import { Geist, Geist_Mono } from 'next/font/google'
43
import { Analytics } from '@vercel/analytics/next'
54
import './globals.css'
65

7-
const _geist = Geist({ subsets: ["latin"] });
8-
const _geistMono = Geist_Mono({ subsets: ["latin"] });
9-
106
export const metadata: Metadata = {
117
title: 'TaskChain',
128
description: 'Web3-powered freelance marketplace with escrow-based payments on Stellar blockchain. Protect your work and payments with smart contract security.',

docs/pr-evidence/build.txt

4.21 KB
Binary file not shown.

docs/pr-evidence/lint.txt

1.09 KB
Binary file not shown.

env.example

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
DATABASE_URL=your db url
1+
DATABASE_URL=your db url
2+
JWT_SECRET=replace_with_a_long_random_secret_min_32_chars

lib/auth/constants.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export const ACCESS_TOKEN_COOKIE = 'tc_access_token'
2+
export const REFRESH_TOKEN_COOKIE = 'tc_refresh_token'
3+
4+
export const ACCESS_TOKEN_TTL_SECONDS = 15 * 60
5+
export const REFRESH_TOKEN_TTL_SECONDS = 7 * 24 * 60 * 60
6+
export const NONCE_TTL_SECONDS = 5 * 60

0 commit comments

Comments
 (0)