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 (
-
- );
-};
-
-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 (
-
- );
-};
-
-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