feat: improve authentication and cookie management

- Updated Docker and Turbo configuration to include more environment variables
- Modified API configuration to support dynamic cookie and CORS settings
- Enhanced user authentication flow with optional device ID and automatic generation
- Refactored login, register, and logout resolvers to handle device management
- Updated GraphQL schema to make device ID optional
- Improved web application logout and authentication handling
- Simplified client-side GraphQL mutations for login and registration
This commit is contained in:
Juan Sebastián Montoya 2025-03-10 00:41:39 -05:00
parent d29d116214
commit f9c6230101
15 changed files with 190 additions and 148 deletions

View file

@ -1,6 +1,6 @@
import { createSignal, Show } from 'solid-js';
import { Provider } from '@urql/solid';
import { client } from './lib/graphql-client';
import { createSignal, Show, onMount } from 'solid-js';
import { gql } from '@urql/core';
import { createMutation } from '@urql/solid';
import { LoginForm } from './components/login-form';
import { RegisterForm } from './components/register-form';
import { RoomList } from './components/room-list';
@ -8,11 +8,18 @@ import { ChatRoom } from './components/chat-room';
import { CreateRoom } from './components/create-room';
import './App.css';
const LOGOUT_MUTATION = gql`
mutation Logout {
logout
}
`;
function App() {
const [isAuthenticated, setIsAuthenticated] = createSignal(false);
const [userId, setUserId] = createSignal('');
const [selectedRoomId, setSelectedRoomId] = createSignal<string | null>(null);
const [showRegister, setShowRegister] = createSignal(false);
const [, logout] = createMutation(LOGOUT_MUTATION);
// Function to generate avatar URL
const getUserAvatarUrl = (userId: string, size: number = 40) =>
@ -30,19 +37,27 @@ function App() {
};
// Call checkAuth on component mount
checkAuth();
onMount(() => {
checkAuth();
});
const handleLoginSuccess = (_: string, id: string) => {
setIsAuthenticated(true);
setUserId(id);
};
const handleLogout = () => {
localStorage.removeItem('token');
localStorage.removeItem('userId');
setIsAuthenticated(false);
setUserId('');
setSelectedRoomId(null);
const handleLogout = async () => {
try {
await logout({});
} catch (error) {
console.error('Logout failed:', error);
} finally {
localStorage.removeItem('token');
localStorage.removeItem('userId');
setIsAuthenticated(false);
setUserId('');
setSelectedRoomId(null);
}
};
const handleSelectRoom = (roomId: string) => {
@ -54,81 +69,79 @@ function App() {
};
return (
<Provider value={client}>
<div class='app'>
<header class='app-header'>
<h1>Unreal Chat</h1>
{isAuthenticated() && (
<div class='user-profile'>
<img
src={getUserAvatarUrl(userId())}
alt='User avatar'
class='user-avatar'
/>
<button class='logout-button' onClick={handleLogout}>
Logout
</button>
</div>
)}
</header>
<div class='app'>
<header class='app-header'>
<h1>Unreal Chat</h1>
{isAuthenticated() && (
<div class='user-profile'>
<img
src={getUserAvatarUrl(userId())}
alt='User avatar'
class='user-avatar'
/>
<button class='logout-button' onClick={handleLogout}>
Logout
</button>
</div>
)}
</header>
<main class='app-main'>
<Show
when={isAuthenticated()}
fallback={
<div class='auth-container'>
<div class='auth-tabs'>
<button
class={!showRegister() ? 'active' : ''}
onClick={() => setShowRegister(false)}
>
Login
</button>
<button
class={showRegister() ? 'active' : ''}
onClick={() => setShowRegister(true)}
>
Register
</button>
</div>
<Show
when={showRegister()}
fallback={<LoginForm onLoginSuccess={handleLoginSuccess} />}
<main class='app-main'>
<Show
when={isAuthenticated()}
fallback={
<div class='auth-container'>
<div class='auth-tabs'>
<button
class={!showRegister() ? 'active' : ''}
onClick={() => setShowRegister(false)}
>
<RegisterForm onRegisterSuccess={handleLoginSuccess} />
</Show>
Login
</button>
<button
class={showRegister() ? 'active' : ''}
onClick={() => setShowRegister(true)}
>
Register
</button>
</div>
}
>
<div class='chat-container'>
<aside class='sidebar'>
<CreateRoom onRoomCreated={handleSelectRoom} />
<RoomList
onSelectRoom={handleSelectRoom}
selectedRoomId={selectedRoomId() || undefined}
/>
</aside>
<main class='chat-main'>
<Show when={selectedRoomId()}>
<ChatRoom
roomId={selectedRoomId() || ''}
userId={userId() || ''}
onLeaveRoom={handleLeaveRoom}
/>
</Show>
<Show when={!selectedRoomId()}>
<div class='select-room-message'>
Select a room to start chatting or create a new one
</div>
</Show>
</main>
<Show
when={showRegister()}
fallback={<LoginForm onLoginSuccess={handleLoginSuccess} />}
>
<RegisterForm onRegisterSuccess={handleLoginSuccess} />
</Show>
</div>
</Show>
</main>
</div>
</Provider>
}
>
<div class='chat-container'>
<aside class='sidebar'>
<CreateRoom onRoomCreated={handleSelectRoom} />
<RoomList
onSelectRoom={handleSelectRoom}
selectedRoomId={selectedRoomId() || undefined}
/>
</aside>
<main class='chat-main'>
<Show when={selectedRoomId()}>
<ChatRoom
roomId={selectedRoomId() || ''}
userId={userId() || ''}
onLeaveRoom={handleLeaveRoom}
/>
</Show>
<Show when={!selectedRoomId()}>
<div class='select-room-message'>
Select a room to start chatting or create a new one
</div>
</Show>
</main>
</div>
</Show>
</main>
</div>
);
}

View file

@ -5,7 +5,8 @@ import { createMutation } from '@urql/solid';
const LOGIN_MUTATION = gql`
mutation Login($email: String!, $password: String!) {
login(email: $email, password: $password) {
token
accessToken
refreshToken
user {
id
username
@ -35,7 +36,10 @@ export function LoginForm(props: LoginFormProps) {
}
try {
const result = await login({ email: email(), password: password() });
const result = await login({
email: email(),
password: password(),
});
if (result.error) {
setError(result.error.message);
@ -43,10 +47,12 @@ export function LoginForm(props: LoginFormProps) {
}
if (result.data?.login) {
const { token, user } = result.data.login;
localStorage.setItem('token', token);
const { accessToken, user } = result.data.login;
localStorage.setItem('token', accessToken);
localStorage.setItem('userId', user.id);
props.onLoginSuccess(token, user.id);
props.onLoginSuccess(accessToken, user.id);
} else {
setError('Login failed: No data received from server');
}
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');

View file

@ -5,7 +5,8 @@ import { createMutation } from '@urql/solid';
const REGISTER_MUTATION = gql`
mutation Register($email: String!, $username: String!, $password: String!) {
register(email: $email, username: $username, password: $password) {
token
accessToken
refreshToken
user {
id
username
@ -54,10 +55,12 @@ export function RegisterForm(props: RegisterFormProps) {
}
if (result.data?.register) {
const { token, user } = result.data.register;
localStorage.setItem('token', token);
const { accessToken, user } = result.data.register;
localStorage.setItem('token', accessToken);
localStorage.setItem('userId', user.id);
props.onRegisterSuccess(token, user.id);
props.onRegisterSuccess(accessToken, user.id);
} else {
setError('Registration failed: No data received from server');
}
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');

View file

@ -1,8 +1,17 @@
/* @refresh reload */
import { render } from 'solid-js/web';
import { Provider } from '@urql/solid';
import { client } from './lib/graphql-client';
import './index.css';
import App from './App.tsx';
const root = document.getElementById('root');
render(() => <App />, root!);
render(
() => (
<Provider value={client}>
<App />
</Provider>
),
root!
);

View file

@ -10,8 +10,6 @@ const envSchema = z
WS_URL: env.VITE_WS_URL,
}));
const { API_URL, WS_URL } = envSchema.parse(import.meta.env);
console.log('Current API_URL', API_URL);
console.log('Current WS_URL', WS_URL);
// Create a WebSocket client for GraphQL subscriptions
const wsClient = createWsClient({
@ -40,10 +38,9 @@ export const client = createClient({
// For development, we'll add a simple header-based authentication
fetchOptions: () => {
const token = localStorage.getItem('token');
const userId = localStorage.getItem('userId');
return {
credentials: 'include',
headers: {
'user-id': userId || '',
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
};