Skip to content
Open
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
196 changes: 196 additions & 0 deletions frontend/src/actions/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
'use server';

import { clearAuthCookies, getAuthToken, getAuthUser, setAuthCookies } from '@/lib/cookies';
import { LoginRequest, SignupRequest, User } from '@/types/user';
import { BASE_URL } from '../../constants/constants';

export interface AuthActionResult {
success: boolean;
error?: string;
user?: User;
}

export interface SessionResult {
user: User | null;
token: string | null;
}

/**
* Server action for user signup
*/
export async function signupAction(data: SignupRequest): Promise<AuthActionResult> {
try {
const res = await fetch(`${BASE_URL}/signup`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});

if (!res.ok) {
const error = await res.json();
return {
success: false,
error: error.error || 'Failed to sign up.',
};
}

await res.json();
return { success: true };
} catch (error: unknown) {
if (error instanceof TypeError) {
return {
success: false,
error: `Unable to connect to the server. Please make sure the backend is running on ${BASE_URL}`,
};
}
return {
success: false,
error: error instanceof Error ? error.message : 'An unexpected error occurred',
};
}
}

/**
* Server action for user login
*/
export async function loginAction(data: LoginRequest): Promise<AuthActionResult> {
try {
const res = await fetch(`${BASE_URL}/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});

if (!res.ok) {
const error = await res.json();
return {
success: false,
error: error.error || 'Failed to log in.',
};
}

const response = await res.json();
const { token, user } = response;

if (!token || !user) {
return {
success: false,
error: 'Invalid login response format. Please contact support.',
};
}

// Set auth cookies
await setAuthCookies(token, user);

return {
success: true,
user,
};
} catch (error: unknown) {
if (error instanceof TypeError) {
return {
success: false,
error: `Unable to connect to the server. Please make sure the backend is running on ${BASE_URL}`,
};
}
return {
success: false,
error: error instanceof Error ? error.message : 'An unexpected error occurred',
};
}
}

/**
* Server action for user logout
*/
export async function logoutAction(): Promise<void> {
await clearAuthCookies();
}

/**
* Server action to get current session
*/
export async function getSession(): Promise<SessionResult> {
const token = await getAuthToken();

if (!token) {
return { user: null, token: null };
}

try {
// Validate token by fetching user profile
const res = await fetch(`${BASE_URL}/profile/`, {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
},
cache: 'no-store',
});

if (!res.ok) {
// Token is invalid, clear cookies
await clearAuthCookies();
return { user: null, token: null };
}

const user = await res.json();

// Update user cookie with fresh data
await setAuthCookies(token, user);

return { user, token };
} catch (error) {
console.error('Error validating session:', error);

// On network error, return cached user data if available
if (error instanceof TypeError) {
const cachedUser = await getAuthUser();
if (cachedUser) {
return { user: cachedUser, token };
}
}

// Clear invalid session
await clearAuthCookies();
return { user: null, token: null };
}
}

/**
* Server action to refresh user profile
*/
export async function refreshUserProfile(): Promise<User | null> {
const token = await getAuthToken();

if (!token) {
return null;
}

try {
const res = await fetch(`${BASE_URL}/profile/`, {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
},
cache: 'no-store',
});

if (!res.ok) {
return null;
}

const user = await res.json();

// Update user cookie
await setAuthCookies(token, user);

return user;
} catch (error) {
console.error('Error refreshing user profile:', error);
return null;
}
}
93 changes: 93 additions & 0 deletions frontend/src/app/login/LoginForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
'use client';

import { AuthActionResult } from '@/actions/auth';
import { LoginRequest } from '@/types/user';
import { useRouter } from 'next/navigation';
import { useState, useTransition } from 'react';

interface LoginFormProps {
loginAction: (data: LoginRequest) => Promise<AuthActionResult>;
}

export function LoginForm({ loginAction }: LoginFormProps) {
const router = useRouter();
const [error, setError] = useState('');
const [isPending, startTransition] = useTransition();

async function onSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
setError('');

const formData = new FormData(event.currentTarget);
const email = formData.get('email') as string;
const password = formData.get('password') as string;

startTransition(async () => {
try {
const result = await loginAction({ email, password });

if (result.success) {
router.push('/');
} else {
setError(result.error || 'Failed to log in. Please try again.');
}
} catch {
setError('An unexpected error occurred. Please try again.');
}
});
}

return (
<>
{/* Error Message */}
{error && (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg text-sm">
{error}
</div>
)}

<form onSubmit={onSubmit} className="space-y-5">
{/* Email */}
<div className="space-y-2">
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
Email Address
</label>
<input
id="email"
name="email"
type="email"
required
disabled={isPending}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-black focus:border-transparent transition-all outline-none disabled:bg-gray-50 disabled:cursor-not-allowed"
placeholder="you@example.com"
/>
</div>

{/* Password */}
<div className="space-y-2">
<label htmlFor="password" className="block text-sm font-medium text-gray-700">
Password
</label>
<input
id="password"
name="password"
type="password"
required
disabled={isPending}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-black focus:border-transparent transition-all outline-none disabled:bg-gray-50 disabled:cursor-not-allowed"
placeholder="••••••••"
/>
</div>

{/* Submit Button */}
<button
type="submit"
disabled={isPending}
className="w-full bg-black text-white py-3 rounded-lg font-medium hover:bg-gray-800 transition-colors duration-200 disabled:bg-gray-400 disabled:cursor-not-allowed"
>
{isPending ? 'Signing in...' : 'Sign In'}
</button>
</form>
</>
);
}
Loading