diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb index 31fbe6d..a497db0 100644 --- a/app/controllers/users/sessions_controller.rb +++ b/app/controllers/users/sessions_controller.rb @@ -11,28 +11,18 @@ class SessionsController < Devise::SessionsController def respond_with(_resource, _options = {}) render json: { status: { code: 200, message: 'User signed in successfully', - data: { user: UserSerializer.new(current_user).serializable_hash[:data][:attributes] } } + data: { user: UserSerializer.new(current_user).serializable_hash.dig(:data, :attributes) } } }, status: :ok end def respond_to_on_destroy - if request.headers['Authorization'].present? - jwt_payload = JWT.decode(request.headers['Authorization'].split.last, - Rails.application.credentials.fetch(:secret_key_base)).first - current_user = User.find(jwt_payload['sub']) - end - if current_user - render json: { - status: 200, - message: 'Logged out successfully.' - }, status: :ok - else - render json: { - status: 401, - message: "Couldn't find an active session." - }, status: :unauthorized + current_user.update(jti: SecureRandom.uuid) end + render json: { + status: 200, + message: 'Logged out successfully (or already logged out).' + }, status: :ok end end end diff --git a/client/src/App.css b/client/src/App.css index 5202d7f..0701c9c 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -395,6 +395,9 @@ font-size: 0.75rem; margin-top: 0.25rem; display: block; + display: inline-flex; + align-items: center; + gap: 0.25rem; } /* Cards */ diff --git a/client/src/components/Login.js b/client/src/components/Login.js deleted file mode 100644 index 3e87c9c..0000000 --- a/client/src/components/Login.js +++ /dev/null @@ -1,61 +0,0 @@ -import React, { useState } from "react"; -import baseAuthApi from "../services/api"; - -const Login = () => { - const [email, setEmail] = useState(""); - const [password, setPassword] = useState(""); - const [error, setError] = useState(""); - - const handleSubmit = async (e) => { - e.preventDefault(); - setError(""); - try { - await baseAuthApi.post("/login", { user: { email, password } }); - alert("Login successful!"); - // Redirect or update UI here - } catch (err) { - setError("Invalid email or password"); - } - }; - - return ( -
-
-

Login

- {error &&
{error}
} -
- - setEmail(e.target.value)} - className="w-full px-3 py-2 border rounded focus:outline-none focus:ring" - required - autoFocus - /> -
-
- - setPassword(e.target.value)} - className="w-full px-3 py-2 border rounded focus:outline-none focus:ring" - required - /> -
- -
-
- ); -}; - -export default Login; diff --git a/client/src/components/Signup.js b/client/src/components/Signup.js deleted file mode 100644 index 823f7f4..0000000 --- a/client/src/components/Signup.js +++ /dev/null @@ -1,91 +0,0 @@ -import React, { useState } from 'react'; -import baseAuthApi from '../services/api'; - -const Signup = () => { - const [name, setName] = useState(''); - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [passwordConfirmation, setPasswordConfirmation] = useState(''); - const [error, setError] = useState(''); - const [success, setSuccess] = useState(''); - - const handleSubmit = async e => { - e.preventDefault(); - setError(''); - setSuccess(''); - if (password !== passwordConfirmation) { - setError('Passwords do not match'); - return; - } - try { - const res = await baseAuthApi.post('/signup', { - user: { name, email, password, password_confirmation: passwordConfirmation } - }); - setSuccess('Signup successful! You can now log in.'); - } catch (err) { - setError('Signup failed. Please check your details.'); - } - }; - - return ( -
-
-

Signup

- {error &&
{error}
} - {success &&
{success}
} -
- - setName(e.target.value)} - required - autoFocus - /> -
-
- - setEmail(e.target.value)} - required - /> -
-
- - setPassword(e.target.value)} - required - /> -
-
- - setPasswordConfirmation(e.target.value)} - required - /> -
- -
-
- ); -}; - -export default Signup; diff --git a/client/src/components/auth/Login.js b/client/src/components/auth/Login.js index 83af8ed..314eaa7 100644 --- a/client/src/components/auth/Login.js +++ b/client/src/components/auth/Login.js @@ -21,10 +21,6 @@ const Login = () => { if (isAuthenticated) navigate('/'); }, [isAuthenticated, navigate]); - useEffect(() => { - clearError(); - }, [clearError]); - const onSubmit = async (data) => { setIsSubmitting(true); const result = await login(data); diff --git a/client/src/components/auth/Signup.js b/client/src/components/auth/Signup.js index 1f80622..395efdd 100644 --- a/client/src/components/auth/Signup.js +++ b/client/src/components/auth/Signup.js @@ -22,10 +22,6 @@ const Signup = () => { if (isAuthenticated) navigate('/'); }, [isAuthenticated, navigate]); - useEffect(() => { - clearError(); - }, [clearError]); - const onSubmit = async (data) => { setIsSubmitting(true); const result = await registerUser({ @@ -116,7 +112,13 @@ const Signup = () => { id="password" {...register('password', { required: 'Password is required', - minLength: { value: 6, message: 'Password must be at least 6 characters' } + minLength: { + value: 6, + message: 'Password must be at least 6 characters' + }, + validate: value => + /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_\-+=\[\]{};:'",.<>?/`~\\|])[A-Za-z\d!@#$%^&*()_\-+=\[\]{};:'",.<>?/`~\\|]{6,}$/.test(value) + || 'Password must contain at least 1 uppercase letter, 1 lowercase letter, 1 digit, and 1 special character' })} placeholder="Create a password" autoComplete="new-password" diff --git a/client/src/contexts/AuthContext.js b/client/src/contexts/AuthContext.js index 25876d6..e3e3ea6 100644 --- a/client/src/contexts/AuthContext.js +++ b/client/src/contexts/AuthContext.js @@ -1,5 +1,5 @@ import React, { createContext, useContext, useState, useEffect, useCallback } from 'react'; -import { api, baseAuthApi } from '../services/api' +import api, { authAPI } from '../services/api'; const AuthContext = createContext(); @@ -16,19 +16,37 @@ export const AuthProvider = ({ children }) => { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const clearAuthData = () => { + localStorage.removeItem('user'); + localStorage.removeItem('authToken'); + delete api.defaults.headers.common.Authorization; + setUser(null); + setError(null); + }; + + const updateUser = (updatedUser) => { + const newUserData = { ...user, ...updatedUser }; + localStorage.setItem('user', JSON.stringify(newUserData)); + setUser(newUserData); + }; + + const clearError = () => setError(null); + const checkAuthStatus = useCallback(async () => { try { const savedUser = localStorage.getItem('user'); const savedToken = localStorage.getItem('authToken'); - + if (savedUser && savedToken) { - // Verify token is still valid + api.defaults.headers.common.Authorization = `Bearer ${savedToken}`; + + // verify token by calling a lightweight endpoint try { - // In a real app, you'd verify the token with the server - const userData = JSON.parse(savedUser); - setUser(userData); + const res = await authAPI.memberInfo(); + const remoteUser = res.data || JSON.parse(savedUser); + localStorage.setItem('user', JSON.stringify(remoteUser)); + setUser(remoteUser); } catch (err) { - // Token is invalid, clear storage clearAuthData(); } } @@ -50,21 +68,18 @@ export const AuthProvider = ({ children }) => { setError(null); try { - // Call Rails Devise sessions endpoint - const res = await baseAuthApi.post('/login', { user: credentials }); + const res = await authAPI.login({ user: credentials }); - // Extract token from Authorization header if present - const authHeader = res.headers?.authorization || res.headers?.Authorization; + const authHeader = res.headers?.authorization; const token = authHeader ? authHeader.split(' ').pop() : null; - // Extract user from response body (matches Users::SessionsController response) const userData = res.data?.status?.data?.user || - res.data?.data?.attributes || null; if (token) { localStorage.setItem('authToken', token); + api.defaults.headers.common.Authorization = `Bearer ${token}`; } if (userData) { localStorage.setItem('user', JSON.stringify(userData)); @@ -74,7 +89,7 @@ export const AuthProvider = ({ children }) => { return { success: true, user: userData, token }; } catch (err) { - const errorMessage = err.response?.data?.message || 'Login failed. Please try again.'; + const errorMessage = err.response?.data || 'Login failed. Please try again.'; setError(errorMessage); return { success: false, error: errorMessage }; } finally { @@ -87,32 +102,28 @@ export const AuthProvider = ({ children }) => { setError(null); try { - // For now, we'll use mock registration - // In production, this would be: const response = await authAPI.register(userData); - - // Simulate API call - await new Promise(resolve => setTimeout(resolve, 1500)); - - // Mock successful registration - const newUser = { - id: Date.now(), - email: userData.email, - name: userData.name, - role: 'user', - created_at: new Date().toISOString() - }; - - const token = 'mock-jwt-token-' + Date.now(); - - // Store auth data - localStorage.setItem('user', JSON.stringify(newUser)); - localStorage.setItem('authToken', token); - - setUser(newUser); - return { success: true, user: newUser }; + const res = await authAPI.register({ user: userData }); + + const authHeader = res.headers?.authorization; + const token = authHeader ? authHeader.split(' ').pop() : null; + + const returnedUser = + res.data?.data || + null; + + if (token) { + localStorage.setItem('authToken', token); + api.defaults.headers.common.Authorization = `Bearer ${token}`; + } + if (returnedUser) { + localStorage.setItem('user', JSON.stringify(returnedUser)); + setUser(returnedUser); + } + + return { success: true, user: returnedUser, token }; } catch (err) { - const errorMessage = err.response?.data?.message || 'Registration failed. Please try again.'; + const errorMessage = err.response?.data?.status?.message || 'Registration failed. Please try again.'; setError(errorMessage); return { success: false, error: errorMessage }; } finally { @@ -124,41 +135,24 @@ export const AuthProvider = ({ children }) => { setLoading(true); try { - // In production, you might want to call the logout API - // await authAPI.logout(); + const token = localStorage.getItem('authToken'); + + if (token) { + await authAPI.logout(); + } clearAuthData(); - setUser(null); return { success: true }; } catch (err) { console.error('Logout error:', err); - // Even if API call fails, clear local data clearAuthData(); - setUser(null); return { success: true }; } finally { setLoading(false); } }; - const clearAuthData = () => { - localStorage.removeItem('user'); - localStorage.removeItem('authToken'); - setUser(null); - setError(null); - }; - - const updateUser = (updatedUser) => { - const newUserData = { ...user, ...updatedUser }; - localStorage.setItem('user', JSON.stringify(newUserData)); - setUser(newUserData); - }; - - const clearError = () => { - setError(null); - }; - const value = { user, loading, diff --git a/client/src/index.css b/client/src/index.css index 6dced73..06629b3 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -187,7 +187,7 @@ code { .input-group input { width: 100%; - padding: 1rem 3rem 1rem 3rem; /* extra right padding for toggle */ + padding: 1rem 0rem 1rem 3rem!important; /* extra right padding for toggle */ border: 1px solid var(--border-color); border-radius: 0.5rem; background-color: var(--input-bg); @@ -216,7 +216,7 @@ code { border: none; color: var(--text-muted); cursor: pointer; - padding: 0.5rem; + padding: 1rem; border-radius: 0.25rem; transition: color 0.2s ease; } @@ -262,6 +262,9 @@ code { border-radius: 0.5rem; font-size: 0.875rem; margin-bottom: 1rem; + display: inline-flex; + align-items: center; + gap: 0.25rem; } .auth-footer { diff --git a/client/src/services/api.js b/client/src/services/api.js index ca81ea9..e7a6cf9 100644 --- a/client/src/services/api.js +++ b/client/src/services/api.js @@ -9,7 +9,7 @@ const api = axios.create({ withCredentials: true }); -export const baseAuthApi = axios.create({ +const baseAuthApi = axios.create({ baseURL: process.env.REACT_BASE_APP_API_URL || 'http://localhost:3000', headers: { 'Content-Type': 'application/json', @@ -47,10 +47,15 @@ api.interceptors.response.use( // Auth API export const authAPI = { - login: (credentials) => api.post('/auth/login', credentials), - register: (userData) => api.post('/auth/register', userData), - logout: () => api.post('/auth/logout'), - refreshToken: () => api.post('/auth/refresh'), + login: (credentials) => baseAuthApi.post('/login', credentials), + register: (userData) => baseAuthApi.post('/signup', userData), + logout: () => baseAuthApi.delete('/logout', { + headers: { + 'Authorization': `Bearer ${localStorage.getItem('authToken')}` + } + }), + refreshToken: () => baseAuthApi.post('/refresh'), + memberInfo: () => baseAuthApi.get('/member_details'), }; // Budgets API