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:
parent
d29d116214
commit
f9c6230101
15 changed files with 190 additions and 148 deletions
|
@ -1,4 +1,9 @@
|
||||||
.env
|
**/dist
|
||||||
node_modules
|
|
||||||
dist
|
dist
|
||||||
.turbo
|
.env
|
||||||
|
.env.local
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
node_modules
|
||||||
|
**/node_modules
|
||||||
|
.turbo
|
|
@ -1,4 +1,8 @@
|
||||||
.env
|
**/dist
|
||||||
node_modules
|
|
||||||
dist
|
dist
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
node_modules
|
||||||
.turbo
|
.turbo
|
||||||
|
|
|
@ -11,6 +11,7 @@ const schema = z
|
||||||
API_HOST: z.string(),
|
API_HOST: z.string(),
|
||||||
API_PORT: z.coerce.number(),
|
API_PORT: z.coerce.number(),
|
||||||
COOKIE_SECRET: z.string(),
|
COOKIE_SECRET: z.string(),
|
||||||
|
DATABASE_URL: z.string(),
|
||||||
MINIO_ENDPOINT: z.string(),
|
MINIO_ENDPOINT: z.string(),
|
||||||
MINIO_PORT: z.coerce.number(),
|
MINIO_PORT: z.coerce.number(),
|
||||||
MINIO_ACCESS_KEY: z.string(),
|
MINIO_ACCESS_KEY: z.string(),
|
||||||
|
@ -34,6 +35,7 @@ const schema = z
|
||||||
allowedOrigins: env.ALLOWED_ORIGINS.split(','),
|
allowedOrigins: env.ALLOWED_ORIGINS.split(','),
|
||||||
port: env.API_PORT,
|
port: env.API_PORT,
|
||||||
host: env.API_HOST,
|
host: env.API_HOST,
|
||||||
|
databaseUrl: env.DATABASE_URL,
|
||||||
},
|
},
|
||||||
minio: {
|
minio: {
|
||||||
endPoint: env.MINIO_ENDPOINT,
|
endPoint: env.MINIO_ENDPOINT,
|
||||||
|
@ -45,8 +47,8 @@ const schema = z
|
||||||
region: env.MINIO_REGION,
|
region: env.MINIO_REGION,
|
||||||
},
|
},
|
||||||
token: {
|
token: {
|
||||||
accessTokenExpiresIn: env.TOKEN_ACCESS_EXPIRES_IN,
|
accessTokenExpiresIn: env.TOKEN_ACCESS_EXPIRES_IN * 1000,
|
||||||
refreshTokenExpiresIn: env.TOKEN_REFRESH_EXPIRES_IN,
|
refreshTokenExpiresIn: env.TOKEN_REFRESH_EXPIRES_IN * 1000,
|
||||||
secret: env.TOKEN_SECRET,
|
secret: env.TOKEN_SECRET,
|
||||||
},
|
},
|
||||||
memc: {
|
memc: {
|
||||||
|
|
|
@ -140,21 +140,21 @@ export type MutationregisterArgs = {
|
||||||
email: Scalars["String"];
|
email: Scalars["String"];
|
||||||
username: Scalars["String"];
|
username: Scalars["String"];
|
||||||
password: Scalars["String"];
|
password: Scalars["String"];
|
||||||
deviceId: Scalars["String"];
|
deviceId?: InputMaybe<Scalars["String"]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MutationloginArgs = {
|
export type MutationloginArgs = {
|
||||||
email: Scalars["String"];
|
email: Scalars["String"];
|
||||||
password: Scalars["String"];
|
password: Scalars["String"];
|
||||||
deviceId: Scalars["String"];
|
deviceId?: InputMaybe<Scalars["String"]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MutationlogoutArgs = {
|
export type MutationlogoutArgs = {
|
||||||
deviceId: Scalars["String"];
|
deviceId?: InputMaybe<Scalars["String"]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MutationrefreshTokenArgs = {
|
export type MutationrefreshTokenArgs = {
|
||||||
deviceId: Scalars["String"];
|
deviceId?: InputMaybe<Scalars["String"]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MutationcreateRoomArgs = {
|
export type MutationcreateRoomArgs = {
|
||||||
|
@ -495,28 +495,25 @@ export type MutationResolvers<
|
||||||
ResolversTypes["AuthPayload"],
|
ResolversTypes["AuthPayload"],
|
||||||
ParentType,
|
ParentType,
|
||||||
ContextType,
|
ContextType,
|
||||||
RequireFields<
|
RequireFields<MutationregisterArgs, "email" | "username" | "password">
|
||||||
MutationregisterArgs,
|
|
||||||
"email" | "username" | "password" | "deviceId"
|
|
||||||
>
|
|
||||||
>;
|
>;
|
||||||
login?: Resolver<
|
login?: Resolver<
|
||||||
ResolversTypes["AuthPayload"],
|
ResolversTypes["AuthPayload"],
|
||||||
ParentType,
|
ParentType,
|
||||||
ContextType,
|
ContextType,
|
||||||
RequireFields<MutationloginArgs, "email" | "password" | "deviceId">
|
RequireFields<MutationloginArgs, "email" | "password">
|
||||||
>;
|
>;
|
||||||
logout?: Resolver<
|
logout?: Resolver<
|
||||||
ResolversTypes["Boolean"],
|
ResolversTypes["Boolean"],
|
||||||
ParentType,
|
ParentType,
|
||||||
ContextType,
|
ContextType,
|
||||||
RequireFields<MutationlogoutArgs, "deviceId">
|
Partial<MutationlogoutArgs>
|
||||||
>;
|
>;
|
||||||
refreshToken?: Resolver<
|
refreshToken?: Resolver<
|
||||||
ResolversTypes["AuthPayload"],
|
ResolversTypes["AuthPayload"],
|
||||||
ParentType,
|
ParentType,
|
||||||
ContextType,
|
ContextType,
|
||||||
RequireFields<MutationrefreshTokenArgs, "deviceId">
|
Partial<MutationrefreshTokenArgs>
|
||||||
>;
|
>;
|
||||||
logoutAllDevices?: Resolver<
|
logoutAllDevices?: Resolver<
|
||||||
ResolversTypes["Boolean"],
|
ResolversTypes["Boolean"],
|
||||||
|
|
|
@ -56,8 +56,9 @@ app.register(fastifyCookie, {
|
||||||
secret: cookieConfig.secret,
|
secret: cookieConfig.secret,
|
||||||
parseOptions: {
|
parseOptions: {
|
||||||
secure: config.isProduction,
|
secure: config.isProduction,
|
||||||
httpOnly: config.isProduction,
|
httpOnly: true,
|
||||||
sameSite: 'lax',
|
sameSite: config.isProduction ? 'none' : 'lax',
|
||||||
|
path: '/',
|
||||||
maxAge: 60 * 60 * 24 * 30, // 30 days
|
maxAge: 60 * 60 * 24 * 30, // 30 days
|
||||||
signed: true,
|
signed: true,
|
||||||
},
|
},
|
||||||
|
@ -69,7 +70,7 @@ app.register(fastifyCors, {
|
||||||
return callback(new Error('Not allowed'), false);
|
return callback(new Error('Not allowed'), false);
|
||||||
},
|
},
|
||||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||||
allowedHeaders: ['Content-Type', 'Authorization'],
|
credentials: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
app.register(mercurius, {
|
app.register(mercurius, {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { GraphQLError } from 'graphql';
|
import { GraphQLError } from 'graphql';
|
||||||
import { IResolvers } from 'mercurius';
|
import { IResolvers } from 'mercurius';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { MutationResolvers, QueryResolvers } from '../generated/graphql';
|
import { MutationResolvers, QueryResolvers } from '../generated/graphql';
|
||||||
import { withAuth, hashPassword, verifyPassword } from '../utils';
|
import { withAuth, hashPassword, verifyPassword } from '../utils';
|
||||||
|
|
||||||
|
@ -66,9 +66,10 @@ export const userResolvers: IResolvers = {
|
||||||
Mutation: {
|
Mutation: {
|
||||||
register: async (
|
register: async (
|
||||||
_,
|
_,
|
||||||
{ email, username, password, deviceId },
|
{ email, username, password, deviceId: reqDeviceId },
|
||||||
{ prisma, token, reply }
|
{ prisma, token, reply, req }
|
||||||
) => {
|
) => {
|
||||||
|
const deviceId = req.cookies?.deviceId ?? reqDeviceId ?? uuidv4();
|
||||||
const existingUser = await prisma.user.findFirst({
|
const existingUser = await prisma.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
OR: [{ email }, { username }],
|
OR: [{ email }, { username }],
|
||||||
|
@ -101,6 +102,7 @@ export const userResolvers: IResolvers = {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
reply.setCookie('refreshToken', refreshToken);
|
reply.setCookie('refreshToken', refreshToken);
|
||||||
|
reply.setCookie('deviceId', deviceId);
|
||||||
return {
|
return {
|
||||||
accessToken,
|
accessToken,
|
||||||
refreshToken,
|
refreshToken,
|
||||||
|
@ -109,9 +111,10 @@ export const userResolvers: IResolvers = {
|
||||||
},
|
},
|
||||||
login: async (
|
login: async (
|
||||||
_,
|
_,
|
||||||
{ email, password, deviceId },
|
{ email, password, deviceId: reqDeviceId },
|
||||||
{ prisma, token, reply }
|
{ prisma, token, reply, req }
|
||||||
) => {
|
) => {
|
||||||
|
const deviceId = req.cookies?.deviceId ?? reqDeviceId ?? uuidv4();
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: { email },
|
where: { email },
|
||||||
});
|
});
|
||||||
|
@ -128,19 +131,27 @@ export const userResolvers: IResolvers = {
|
||||||
role: 'user',
|
role: 'user',
|
||||||
deviceId,
|
deviceId,
|
||||||
});
|
});
|
||||||
|
|
||||||
reply.setCookie('refreshToken', refreshToken);
|
reply.setCookie('refreshToken', refreshToken);
|
||||||
|
reply.setCookie('deviceId', deviceId);
|
||||||
return {
|
return {
|
||||||
user,
|
user,
|
||||||
accessToken,
|
accessToken,
|
||||||
refreshToken,
|
refreshToken,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
logout: async (_, { deviceId }, { token, jwt, reply }) => {
|
logout: async (
|
||||||
jwt &&
|
_,
|
||||||
(await token.revokeTokensByDevice({
|
{ deviceId: reqDeviceId },
|
||||||
|
{ token, jwt, reply, req }
|
||||||
|
) => {
|
||||||
|
const deviceId = req.cookies?.deviceId ?? reqDeviceId ?? uuidv4();
|
||||||
|
if (jwt) {
|
||||||
|
await token.revokeTokensByDevice({
|
||||||
userId: jwt.sub,
|
userId: jwt.sub,
|
||||||
deviceId,
|
deviceId,
|
||||||
}));
|
});
|
||||||
|
}
|
||||||
reply.clearCookie('refreshToken');
|
reply.clearCookie('refreshToken');
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
@ -152,13 +163,15 @@ export const userResolvers: IResolvers = {
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
refreshToken: withAuth<MutationResolvers['refreshToken']>(
|
refreshToken: withAuth<MutationResolvers['refreshToken']>(
|
||||||
async (_, { deviceId }, { token, jwt, reply }) => {
|
async (_, { deviceId: reqDeviceId }, { token, jwt, reply }) => {
|
||||||
|
const deviceId = reply.cookies?.['deviceId'] ?? reqDeviceId ?? uuidv4();
|
||||||
const { accessToken, refreshToken } = await token.rotateRefreshToken({
|
const { accessToken, refreshToken } = await token.rotateRefreshToken({
|
||||||
userId: jwt.sub,
|
userId: jwt.sub,
|
||||||
oldToken: jwt.jti,
|
oldToken: jwt.jti,
|
||||||
deviceId,
|
deviceId,
|
||||||
});
|
});
|
||||||
reply.setCookie('refreshToken', refreshToken);
|
reply.setCookie('refreshToken', refreshToken);
|
||||||
|
reply.setCookie('deviceId', deviceId);
|
||||||
return {
|
return {
|
||||||
accessToken,
|
accessToken,
|
||||||
refreshToken,
|
refreshToken,
|
||||||
|
|
|
@ -70,11 +70,11 @@ export default gql`
|
||||||
email: String!
|
email: String!
|
||||||
username: String!
|
username: String!
|
||||||
password: String!
|
password: String!
|
||||||
deviceId: String!
|
deviceId: String
|
||||||
): AuthPayload!
|
): AuthPayload!
|
||||||
login(email: String!, password: String!, deviceId: String!): AuthPayload!
|
login(email: String!, password: String!, deviceId: String): AuthPayload!
|
||||||
logout(deviceId: String!): Boolean!
|
logout(deviceId: String): Boolean!
|
||||||
refreshToken(deviceId: String!): AuthPayload!
|
refreshToken(deviceId: String): AuthPayload!
|
||||||
logoutAllDevices: Boolean!
|
logoutAllDevices: Boolean!
|
||||||
createRoom(name: String!, description: String, isPrivate: Boolean): Room!
|
createRoom(name: String!, description: String, isPrivate: Boolean): Room!
|
||||||
joinRoom(roomId: ID!): Room!
|
joinRoom(roomId: ID!): Room!
|
||||||
|
|
|
@ -38,12 +38,11 @@ export class MemcService {
|
||||||
* Stores a value in Memcached
|
* Stores a value in Memcached
|
||||||
* @param key - The key to store the value under
|
* @param key - The key to store the value under
|
||||||
* @param value - The value to store
|
* @param value - The value to store
|
||||||
* @param ttl - Optional time-to-live in seconds (overrides default TTL)
|
* @param ttl - Optional time-to-live in milliseconds (overrides default TTL)
|
||||||
*/
|
*/
|
||||||
public async set(key: string, value: string, ttl?: number): Promise<boolean> {
|
public async set(key: string, value: string, ttl?: number): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const ttlMilliseconds = ttl ? ttl * 1000 : undefined;
|
return await this.keyv.set(key, value, ttl);
|
||||||
return await this.keyv.set(key, value, ttlMilliseconds);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error setting value in Memcached:', error);
|
console.error('Error setting value in Memcached:', error);
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { createSignal, Show } from 'solid-js';
|
import { createSignal, Show, onMount } from 'solid-js';
|
||||||
import { Provider } from '@urql/solid';
|
import { gql } from '@urql/core';
|
||||||
import { client } from './lib/graphql-client';
|
import { createMutation } from '@urql/solid';
|
||||||
import { LoginForm } from './components/login-form';
|
import { LoginForm } from './components/login-form';
|
||||||
import { RegisterForm } from './components/register-form';
|
import { RegisterForm } from './components/register-form';
|
||||||
import { RoomList } from './components/room-list';
|
import { RoomList } from './components/room-list';
|
||||||
|
@ -8,11 +8,18 @@ import { ChatRoom } from './components/chat-room';
|
||||||
import { CreateRoom } from './components/create-room';
|
import { CreateRoom } from './components/create-room';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
|
|
||||||
|
const LOGOUT_MUTATION = gql`
|
||||||
|
mutation Logout {
|
||||||
|
logout
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [isAuthenticated, setIsAuthenticated] = createSignal(false);
|
const [isAuthenticated, setIsAuthenticated] = createSignal(false);
|
||||||
const [userId, setUserId] = createSignal('');
|
const [userId, setUserId] = createSignal('');
|
||||||
const [selectedRoomId, setSelectedRoomId] = createSignal<string | null>(null);
|
const [selectedRoomId, setSelectedRoomId] = createSignal<string | null>(null);
|
||||||
const [showRegister, setShowRegister] = createSignal(false);
|
const [showRegister, setShowRegister] = createSignal(false);
|
||||||
|
const [, logout] = createMutation(LOGOUT_MUTATION);
|
||||||
|
|
||||||
// Function to generate avatar URL
|
// Function to generate avatar URL
|
||||||
const getUserAvatarUrl = (userId: string, size: number = 40) =>
|
const getUserAvatarUrl = (userId: string, size: number = 40) =>
|
||||||
|
@ -30,19 +37,27 @@ function App() {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Call checkAuth on component mount
|
// Call checkAuth on component mount
|
||||||
checkAuth();
|
onMount(() => {
|
||||||
|
checkAuth();
|
||||||
|
});
|
||||||
|
|
||||||
const handleLoginSuccess = (_: string, id: string) => {
|
const handleLoginSuccess = (_: string, id: string) => {
|
||||||
setIsAuthenticated(true);
|
setIsAuthenticated(true);
|
||||||
setUserId(id);
|
setUserId(id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = async () => {
|
||||||
localStorage.removeItem('token');
|
try {
|
||||||
localStorage.removeItem('userId');
|
await logout({});
|
||||||
setIsAuthenticated(false);
|
} catch (error) {
|
||||||
setUserId('');
|
console.error('Logout failed:', error);
|
||||||
setSelectedRoomId(null);
|
} finally {
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
localStorage.removeItem('userId');
|
||||||
|
setIsAuthenticated(false);
|
||||||
|
setUserId('');
|
||||||
|
setSelectedRoomId(null);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectRoom = (roomId: string) => {
|
const handleSelectRoom = (roomId: string) => {
|
||||||
|
@ -54,81 +69,79 @@ function App() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Provider value={client}>
|
<div class='app'>
|
||||||
<div class='app'>
|
<header class='app-header'>
|
||||||
<header class='app-header'>
|
<h1>Unreal Chat</h1>
|
||||||
<h1>Unreal Chat</h1>
|
{isAuthenticated() && (
|
||||||
{isAuthenticated() && (
|
<div class='user-profile'>
|
||||||
<div class='user-profile'>
|
<img
|
||||||
<img
|
src={getUserAvatarUrl(userId())}
|
||||||
src={getUserAvatarUrl(userId())}
|
alt='User avatar'
|
||||||
alt='User avatar'
|
class='user-avatar'
|
||||||
class='user-avatar'
|
/>
|
||||||
/>
|
<button class='logout-button' onClick={handleLogout}>
|
||||||
<button class='logout-button' onClick={handleLogout}>
|
Logout
|
||||||
Logout
|
</button>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</header>
|
||||||
</header>
|
|
||||||
|
|
||||||
<main class='app-main'>
|
<main class='app-main'>
|
||||||
<Show
|
<Show
|
||||||
when={isAuthenticated()}
|
when={isAuthenticated()}
|
||||||
fallback={
|
fallback={
|
||||||
<div class='auth-container'>
|
<div class='auth-container'>
|
||||||
<div class='auth-tabs'>
|
<div class='auth-tabs'>
|
||||||
<button
|
<button
|
||||||
class={!showRegister() ? 'active' : ''}
|
class={!showRegister() ? 'active' : ''}
|
||||||
onClick={() => setShowRegister(false)}
|
onClick={() => setShowRegister(false)}
|
||||||
>
|
|
||||||
Login
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class={showRegister() ? 'active' : ''}
|
|
||||||
onClick={() => setShowRegister(true)}
|
|
||||||
>
|
|
||||||
Register
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Show
|
|
||||||
when={showRegister()}
|
|
||||||
fallback={<LoginForm onLoginSuccess={handleLoginSuccess} />}
|
|
||||||
>
|
>
|
||||||
<RegisterForm onRegisterSuccess={handleLoginSuccess} />
|
Login
|
||||||
</Show>
|
</button>
|
||||||
|
<button
|
||||||
|
class={showRegister() ? 'active' : ''}
|
||||||
|
onClick={() => setShowRegister(true)}
|
||||||
|
>
|
||||||
|
Register
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
>
|
|
||||||
<div class='chat-container'>
|
|
||||||
<aside class='sidebar'>
|
|
||||||
<CreateRoom onRoomCreated={handleSelectRoom} />
|
|
||||||
<RoomList
|
|
||||||
onSelectRoom={handleSelectRoom}
|
|
||||||
selectedRoomId={selectedRoomId() || undefined}
|
|
||||||
/>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<main class='chat-main'>
|
<Show
|
||||||
<Show when={selectedRoomId()}>
|
when={showRegister()}
|
||||||
<ChatRoom
|
fallback={<LoginForm onLoginSuccess={handleLoginSuccess} />}
|
||||||
roomId={selectedRoomId() || ''}
|
>
|
||||||
userId={userId() || ''}
|
<RegisterForm onRegisterSuccess={handleLoginSuccess} />
|
||||||
onLeaveRoom={handleLeaveRoom}
|
</Show>
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
<Show when={!selectedRoomId()}>
|
|
||||||
<div class='select-room-message'>
|
|
||||||
Select a room to start chatting or create a new one
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</main>
|
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
}
|
||||||
</main>
|
>
|
||||||
</div>
|
<div class='chat-container'>
|
||||||
</Provider>
|
<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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,8 @@ import { createMutation } from '@urql/solid';
|
||||||
const LOGIN_MUTATION = gql`
|
const LOGIN_MUTATION = gql`
|
||||||
mutation Login($email: String!, $password: String!) {
|
mutation Login($email: String!, $password: String!) {
|
||||||
login(email: $email, password: $password) {
|
login(email: $email, password: $password) {
|
||||||
token
|
accessToken
|
||||||
|
refreshToken
|
||||||
user {
|
user {
|
||||||
id
|
id
|
||||||
username
|
username
|
||||||
|
@ -35,7 +36,10 @@ export function LoginForm(props: LoginFormProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await login({ email: email(), password: password() });
|
const result = await login({
|
||||||
|
email: email(),
|
||||||
|
password: password(),
|
||||||
|
});
|
||||||
|
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
setError(result.error.message);
|
setError(result.error.message);
|
||||||
|
@ -43,10 +47,12 @@ export function LoginForm(props: LoginFormProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.data?.login) {
|
if (result.data?.login) {
|
||||||
const { token, user } = result.data.login;
|
const { accessToken, user } = result.data.login;
|
||||||
localStorage.setItem('token', token);
|
localStorage.setItem('token', accessToken);
|
||||||
localStorage.setItem('userId', user.id);
|
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) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'An error occurred');
|
setError(err instanceof Error ? err.message : 'An error occurred');
|
||||||
|
|
|
@ -5,7 +5,8 @@ import { createMutation } from '@urql/solid';
|
||||||
const REGISTER_MUTATION = gql`
|
const REGISTER_MUTATION = gql`
|
||||||
mutation Register($email: String!, $username: String!, $password: String!) {
|
mutation Register($email: String!, $username: String!, $password: String!) {
|
||||||
register(email: $email, username: $username, password: $password) {
|
register(email: $email, username: $username, password: $password) {
|
||||||
token
|
accessToken
|
||||||
|
refreshToken
|
||||||
user {
|
user {
|
||||||
id
|
id
|
||||||
username
|
username
|
||||||
|
@ -54,10 +55,12 @@ export function RegisterForm(props: RegisterFormProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.data?.register) {
|
if (result.data?.register) {
|
||||||
const { token, user } = result.data.register;
|
const { accessToken, user } = result.data.register;
|
||||||
localStorage.setItem('token', token);
|
localStorage.setItem('token', accessToken);
|
||||||
localStorage.setItem('userId', user.id);
|
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) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'An error occurred');
|
setError(err instanceof Error ? err.message : 'An error occurred');
|
||||||
|
|
|
@ -1,8 +1,17 @@
|
||||||
/* @refresh reload */
|
/* @refresh reload */
|
||||||
import { render } from 'solid-js/web';
|
import { render } from 'solid-js/web';
|
||||||
|
import { Provider } from '@urql/solid';
|
||||||
|
import { client } from './lib/graphql-client';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
import App from './App.tsx';
|
import App from './App.tsx';
|
||||||
|
|
||||||
const root = document.getElementById('root');
|
const root = document.getElementById('root');
|
||||||
|
|
||||||
render(() => <App />, root!);
|
render(
|
||||||
|
() => (
|
||||||
|
<Provider value={client}>
|
||||||
|
<App />
|
||||||
|
</Provider>
|
||||||
|
),
|
||||||
|
root!
|
||||||
|
);
|
||||||
|
|
|
@ -10,8 +10,6 @@ const envSchema = z
|
||||||
WS_URL: env.VITE_WS_URL,
|
WS_URL: env.VITE_WS_URL,
|
||||||
}));
|
}));
|
||||||
const { API_URL, WS_URL } = envSchema.parse(import.meta.env);
|
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
|
// Create a WebSocket client for GraphQL subscriptions
|
||||||
const wsClient = createWsClient({
|
const wsClient = createWsClient({
|
||||||
|
@ -40,10 +38,9 @@ export const client = createClient({
|
||||||
// For development, we'll add a simple header-based authentication
|
// For development, we'll add a simple header-based authentication
|
||||||
fetchOptions: () => {
|
fetchOptions: () => {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
const userId = localStorage.getItem('userId');
|
|
||||||
return {
|
return {
|
||||||
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
'user-id': userId || '',
|
|
||||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,11 +6,11 @@ services:
|
||||||
container_name: unreal-chat-api
|
container_name: unreal-chat-api
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
|
- DATABASE_URL=${DATABASE_URL}
|
||||||
- ALLOWED_ORIGINS=${ALLOWED_ORIGINS}
|
- ALLOWED_ORIGINS=${ALLOWED_ORIGINS}
|
||||||
- API_HOST=${API_HOST}
|
- API_HOST=${API_HOST}
|
||||||
- API_PORT=${API_PORT}
|
- API_PORT=${API_PORT}
|
||||||
- COOKIE_SECRET=${COOKIE_SECRET}
|
- COOKIE_SECRET=${COOKIE_SECRET}
|
||||||
- DATABASE_URL=${DATABASE_URL}
|
|
||||||
- MEMC_HOST=${MEMC_HOST}
|
- MEMC_HOST=${MEMC_HOST}
|
||||||
- MEMC_PORT=${MEMC_PORT}
|
- MEMC_PORT=${MEMC_PORT}
|
||||||
- MINIO_ACCESS_KEY=${MINIO_ACCESS_KEY}
|
- MINIO_ACCESS_KEY=${MINIO_ACCESS_KEY}
|
||||||
|
@ -19,10 +19,8 @@ services:
|
||||||
- MINIO_PORT=${MINIO_PORT}
|
- MINIO_PORT=${MINIO_PORT}
|
||||||
- MINIO_SECRET_KEY=${MINIO_SECRET_KEY}
|
- MINIO_SECRET_KEY=${MINIO_SECRET_KEY}
|
||||||
- MINIO_USE_SSL=${MINIO_USE_SSL}
|
- MINIO_USE_SSL=${MINIO_USE_SSL}
|
||||||
- NODE_ENV=production
|
- NODE_ENV=${NODE_ENV}
|
||||||
- TOKEN_SECRET=${TOKEN_SECRET}
|
- TOKEN_SECRET=${TOKEN_SECRET}
|
||||||
networks:
|
|
||||||
- default-network
|
|
||||||
|
|
||||||
web:
|
web:
|
||||||
build:
|
build:
|
||||||
|
@ -37,8 +35,6 @@ services:
|
||||||
- NODE_ENV=production
|
- NODE_ENV=production
|
||||||
- VITE_API_URL=https://chat-api.jusemon.com/graphql
|
- VITE_API_URL=https://chat-api.jusemon.com/graphql
|
||||||
- VITE_WS_URL=wss://chat-api.jusemon.com/graphql
|
- VITE_WS_URL=wss://chat-api.jusemon.com/graphql
|
||||||
networks:
|
|
||||||
- default-network
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
default:
|
default:
|
||||||
|
|
17
turbo.json
17
turbo.json
|
@ -3,23 +3,20 @@
|
||||||
"ui": "tui",
|
"ui": "tui",
|
||||||
"globalDependencies": [".env"],
|
"globalDependencies": [".env"],
|
||||||
"globalEnv": [
|
"globalEnv": [
|
||||||
|
"DATABASE_URL",
|
||||||
"ALLOWED_ORIGINS",
|
"ALLOWED_ORIGINS",
|
||||||
"API_HOST",
|
"API_HOST",
|
||||||
"API_PORT",
|
"API_PORT",
|
||||||
"COOKIE_SECRET",
|
"COOKIE_SECRET",
|
||||||
"MINIO_ENDPOINT",
|
|
||||||
"MINIO_PORT",
|
|
||||||
"MINIO_ACCESS_KEY",
|
|
||||||
"MINIO_SECRET_KEY",
|
|
||||||
"MINIO_BUCKET_NAME",
|
|
||||||
"MINIO_USE_SSL",
|
|
||||||
"MEMC_HOST",
|
"MEMC_HOST",
|
||||||
"MEMC_PORT",
|
"MEMC_PORT",
|
||||||
"MEMC_TTL",
|
"MINIO_ACCESS_KEY",
|
||||||
"MEMC_NAMESPACE",
|
"MINIO_BUCKET_NAME",
|
||||||
|
"MINIO_ENDPOINT",
|
||||||
|
"MINIO_PORT",
|
||||||
|
"MINIO_SECRET_KEY",
|
||||||
|
"MINIO_USE_SSL",
|
||||||
"NODE_ENV",
|
"NODE_ENV",
|
||||||
"TOKEN_ACCESS_EXPIRES_IN",
|
|
||||||
"TOKEN_REFRESH_EXPIRES_IN",
|
|
||||||
"TOKEN_SECRET",
|
"TOKEN_SECRET",
|
||||||
"VITE_API_URL",
|
"VITE_API_URL",
|
||||||
"VITE_WS_URL"
|
"VITE_WS_URL"
|
||||||
|
|
Loading…
Add table
Reference in a new issue