Skip to content

Commit da3caf1

Browse files
committed
Configure auth
1 parent 9c5d834 commit da3caf1

File tree

8 files changed

+170
-50
lines changed

8 files changed

+170
-50
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,5 @@ tmp
3030
# IDE
3131
.vscode
3232
.idea
33+
34+
api.flexile.dev

frontend/AUTH_SETUP.md

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,8 @@ Add these environment variables to your `.env` file:
1010
# Auth.js secret (server-side only)
1111
AUTH_SECRET=your-auth-secret-here
1212

13-
# API configuration (client-side accessible)
14-
NEXT_PUBLIC_API_SECRET_TOKEN=your-api-secret-token-here
15-
NEXT_PUBLIC_API_URL=http://localhost:3000 # Optional - defaults to localhost:3000 in dev
13+
# API token for backend communication (server-side only)
14+
API_SECRET_TOKEN=your-api-secret-token-here
1615
```
1716

1817
### AUTH_SECRET
@@ -21,24 +20,26 @@ Generate a random secret for Auth.js:
2120
npx auth secret
2221
```
2322

24-
### NEXT_PUBLIC_API_SECRET_TOKEN
25-
This should match the API token configured in your backend for accessing the OTP endpoints. Uses `NEXT_PUBLIC_` prefix to make it available in client-side code.
26-
27-
### NEXT_PUBLIC_API_URL (Optional)
28-
Override the default API URL. If not set, it will auto-detect based on environment:
29-
- Development: `http://localhost:3000`
30-
- Production: `https://api.flexile.com`
31-
- Preview: Auto-generated Heroku URL
23+
### API_SECRET_TOKEN
24+
This should match the API token configured in your Rails backend for accessing the OTP endpoints. This is kept server-side only for security.
3225

3326
## How it Works
3427

35-
1. **OTP Request**: User enters email on `/login2` → calls `/api/v1/email_otp` → sends email with OTP
36-
2. **OTP Verification**: User enters OTP → calls `/api/v1/login` → returns JWT → creates Auth.js session
28+
1. **OTP Request**: User enters email on `/login2` → calls Next.js `/api/auth/send-otp` → calls Rails `/api/v1/email_otp` → sends email with OTP
29+
2. **OTP Verification**: User enters OTP → calls Next.js `/api/auth/login` → calls Rails `/api/v1/login` → returns JWT → creates Auth.js session
30+
31+
## API Architecture
32+
33+
**Frontend****Next.js API Routes****Rails Backend**
3734

38-
## API Endpoints Used
35+
### Next.js API Routes
36+
- `POST /api/auth/send-otp` - Proxy to Rails email_otp endpoint
37+
- `POST /api/auth/login` - Proxy to Rails login endpoint
38+
- `GET/POST /api/auth/[...nextauth]` - Auth.js session management
3939

40-
- `POST /api/v1/email_otp` - Send OTP email
41-
- `POST /api/v1/login` - Verify OTP and login
40+
### Rails Backend API Endpoints
41+
- `POST /api/v1/email_otp` - Send OTP email (called by Next.js)
42+
- `POST /api/v1/login` - Verify OTP and login (called by Next.js)
4243

4344
## Routes
4445

@@ -64,4 +65,6 @@ Visit `/login2/test` to see the authentication status and session data.
6465
- Custom credentials provider for OTP authentication
6566
- Parallel to existing Clerk authentication
6667
- Stores JWT token in session for backend API calls
67-
- No frontend data storage - all authentication handled by backend APIs
68+
- No frontend data storage - all authentication handled by backend APIs
69+
- **Security**: API tokens kept server-side, frontend only calls Next.js API routes
70+
- **Architecture**: Frontend → Next.js API → Rails Backend (no direct Rails API calls from frontend)
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { NextRequest, NextResponse } from "next/server"
2+
import { z } from "zod"
3+
import env from "@/env"
4+
5+
const loginSchema = z.object({
6+
email: z.string().email(),
7+
otp_code: z.string().min(6).max(6),
8+
})
9+
10+
// Define the backend API URL based on environment
11+
const getBackendApiUrl = () => {
12+
switch (env.VERCEL_ENV) {
13+
case "production":
14+
return "https://api.flexile.com"
15+
case "preview":
16+
return `https://flexile-pipeline-pr-${process.env.VERCEL_GIT_PULL_REQUEST_ID}.herokuapp.com`
17+
default:
18+
return "https://flexile.dev"
19+
}
20+
}
21+
22+
export async function POST(request: NextRequest) {
23+
try {
24+
const body = await request.json()
25+
const validatedFields = loginSchema.safeParse(body)
26+
27+
if (!validatedFields.success) {
28+
return NextResponse.json(
29+
{ error: "Invalid email or OTP code" },
30+
{ status: 400 }
31+
)
32+
}
33+
34+
const { email, otp_code } = validatedFields.data
35+
const backendUrl = getBackendApiUrl()
36+
37+
// Call the backend login API
38+
const response = await fetch(`${backendUrl}/api/v1/login`, {
39+
method: "POST",
40+
headers: {
41+
"Content-Type": "application/json",
42+
},
43+
body: JSON.stringify({
44+
email,
45+
otp_code,
46+
token: env.API_SECRET_TOKEN,
47+
}),
48+
})
49+
50+
if (!response.ok) {
51+
const errorData = await response.json()
52+
return NextResponse.json(
53+
{ error: errorData.error || "Authentication failed" },
54+
{ status: response.status }
55+
)
56+
}
57+
58+
const data = await response.json()
59+
return NextResponse.json(data)
60+
} catch (error) {
61+
console.error("Login API error:", error)
62+
return NextResponse.json(
63+
{ error: "Internal server error" },
64+
{ status: 500 }
65+
)
66+
}
67+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { NextRequest, NextResponse } from "next/server"
2+
import { z } from "zod"
3+
import env from "@/env"
4+
5+
const sendOtpSchema = z.object({
6+
email: z.string().email(),
7+
})
8+
9+
// Define the backend API URL based on environment
10+
const getBackendApiUrl = () => {
11+
switch (env.VERCEL_ENV) {
12+
case "production":
13+
return "https://api.flexile.com"
14+
case "preview":
15+
return `https://flexile-pipeline-pr-${process.env.VERCEL_GIT_PULL_REQUEST_ID}.herokuapp.com`
16+
default:
17+
return "https://flexile.dev"
18+
}
19+
}
20+
21+
export async function POST(request: NextRequest) {
22+
try {
23+
const body = await request.json()
24+
const validatedFields = sendOtpSchema.safeParse(body)
25+
26+
if (!validatedFields.success) {
27+
return NextResponse.json(
28+
{ error: "Invalid email" },
29+
{ status: 400 }
30+
)
31+
}
32+
33+
const { email } = validatedFields.data
34+
const backendUrl = getBackendApiUrl()
35+
36+
// Call the backend email_otp API
37+
const response = await fetch(`${backendUrl}/api/v1/email_otp`, {
38+
method: "POST",
39+
headers: {
40+
"Content-Type": "application/json",
41+
},
42+
body: JSON.stringify({
43+
email,
44+
token: env.API_SECRET_TOKEN,
45+
}),
46+
})
47+
48+
if (!response.ok) {
49+
const errorData = await response.json()
50+
return NextResponse.json(
51+
{ error: errorData.error || "Failed to send OTP" },
52+
{ status: response.status }
53+
)
54+
}
55+
56+
const data = await response.json()
57+
return NextResponse.json(data)
58+
} catch (error) {
59+
console.error("Send OTP API error:", error)
60+
return NextResponse.json(
61+
{ error: "Internal server error" },
62+
{ status: 500 }
63+
)
64+
}
65+
}

frontend/env/client.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,12 @@ const env = {
55
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY,
66
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY,
77
NEXT_PUBLIC_EQUITY_EXERCISE_DOCUSEAL_ID: process.env.NEXT_PUBLIC_EQUITY_EXERCISE_DOCUSEAL_ID,
8-
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
9-
NEXT_PUBLIC_API_SECRET_TOKEN: process.env.NEXT_PUBLIC_API_SECRET_TOKEN,
10-
VERCEL_ENV: process.env.VERCEL_ENV,
11-
VERCEL_GIT_PULL_REQUEST_ID: process.env.VERCEL_GIT_PULL_REQUEST_ID,
128
};
139

1410
export default z
1511
.object({
1612
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: z.string(),
1713
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: z.string(),
1814
NEXT_PUBLIC_EQUITY_EXERCISE_DOCUSEAL_ID: z.string(),
19-
NEXT_PUBLIC_API_URL: z.string().optional(),
20-
NEXT_PUBLIC_API_SECRET_TOKEN: z.string().optional(),
21-
VERCEL_ENV: z.enum(["production", "preview", "development"]).optional(),
22-
VERCEL_GIT_PULL_REQUEST_ID: z.string().optional(),
2315
})
2416
.parse(env);

frontend/lib/auth.ts

Lines changed: 14 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,18 @@
11
import NextAuth from "next-auth"
22
import Credentials from "next-auth/providers/credentials"
33
import { z } from "zod"
4-
import clientEnv from "@/env/client"
54

6-
// Define the API base URL based on environment
7-
const API_BASE_URL = (() => {
8-
// If NEXT_PUBLIC_API_URL is set, use it
9-
if (clientEnv.NEXT_PUBLIC_API_URL) {
10-
return clientEnv.NEXT_PUBLIC_API_URL
5+
// Get the Next.js application URL
6+
const getNextJsUrl = () => {
7+
if (process.env.VERCEL_URL) {
8+
return `https://${process.env.VERCEL_URL}`
119
}
12-
13-
// Otherwise, determine based on environment
14-
switch (clientEnv.VERCEL_ENV) {
15-
case "production":
16-
return "https://api.flexile.com"
17-
case "preview":
18-
return `https://flexile-pipeline-pr-${clientEnv.VERCEL_GIT_PULL_REQUEST_ID || 'unknown'}.herokuapp.com`
19-
default:
20-
return "http://localhost:3000"
10+
if (process.env.NEXTAUTH_URL) {
11+
return process.env.NEXTAUTH_URL
2112
}
22-
})()
23-
24-
const API_TOKEN = clientEnv.NEXT_PUBLIC_API_SECRET_TOKEN || process.env.API_SECRET_TOKEN || "your-api-token"
13+
// Fallback for local development
14+
return "https://flexile.dev"
15+
}
2516

2617
// Schema for OTP login
2718
const otpLoginSchema = z.object({
@@ -54,16 +45,16 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
5445

5546
const { email, otp_code } = validatedFields.data
5647

57-
// Call the backend login API
58-
const response = await fetch(`${API_BASE_URL}/api/v1/login`, {
48+
// Call the Next.js login API
49+
const nextJsUrl = getNextJsUrl()
50+
const response = await fetch(`${nextJsUrl}/api/auth/login`, {
5951
method: "POST",
6052
headers: {
6153
"Content-Type": "application/json",
6254
},
6355
body: JSON.stringify({
6456
email,
6557
otp_code,
66-
token: API_TOKEN,
6758
}),
6859
})
6960

@@ -118,14 +109,14 @@ export async function sendOTP(email: string) {
118109
throw new Error("Invalid email")
119110
}
120111

121-
const response = await fetch(`${API_BASE_URL}/api/v1/email_otp`, {
112+
const nextJsUrl = getNextJsUrl()
113+
const response = await fetch(`${nextJsUrl}/api/auth/send-otp`, {
122114
method: "POST",
123115
headers: {
124116
"Content-Type": "application/json",
125117
},
126118
body: JSON.stringify({
127119
email,
128-
token: API_TOKEN,
129120
}),
130121
})
131122

frontend/utils/routes.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
* @file Generated by js-routes 2.3.5. Based on Rails 8.0.2 routes of Flexile::Application.
3-
* @version cdd6cd9b0cf9e8a6d8a1288759313facca4b367a53d6c658c9f66222b02b3768
3+
* @version 972cb976202f3a460677e00ec956ca226c20502dbe03363c9d03f98551a22389
44
* @see https://github.com/railsware/js-routes
55
*/
66
declare type Optional<T> = {

frontend/utils/routes.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
* @file Generated by js-routes 2.3.5. Based on Rails 8.0.2 routes of Flexile::Application.
3-
* @version cdd6cd9b0cf9e8a6d8a1288759313facca4b367a53d6c658c9f66222b02b3768
3+
* @version 972cb976202f3a460677e00ec956ca226c20502dbe03363c9d03f98551a22389
44
* @see https://github.com/railsware/js-routes
55
*/
66
// eslint-disable-next-line

0 commit comments

Comments
 (0)