feat: initial commit — backend API + student cabinet frontend
- Go backend: auth (JWT), points earn/spend, QR token generation, partners, admin grant/stats endpoints with chi router - Next.js 14 frontend: login, student dashboard, transaction history, QR display, partners list - PostgreSQL migrations (4 tables), Redis cache, Docker Compose - CORS middleware, role-based route protection, Zustand auth store Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
// Global client state managed with Zustand.
|
||||
// Only truly global state lives here: auth tokens and the current user profile.
|
||||
// Local UI state (loading flags, form values) stays in component useState.
|
||||
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import type { User } from './types';
|
||||
|
||||
interface AuthState {
|
||||
accessToken: string | null;
|
||||
refreshToken: string | null;
|
||||
user: User | null;
|
||||
setTokens: (access: string, refresh: string) => void;
|
||||
setUser: (user: User) => void;
|
||||
updateBalance: (newBalance: number) => void;
|
||||
logout: () => void;
|
||||
}
|
||||
|
||||
/** Writes the access token to localStorage and to a short-lived cookie so
|
||||
* Next.js middleware (edge runtime) can read it for role-based redirects. */
|
||||
function persistToken(accessToken: string): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
localStorage.setItem('access_token', accessToken);
|
||||
// 15 min lifetime matches the default JWT_ACCESS_TTL
|
||||
document.cookie = `access_token=${accessToken}; path=/; SameSite=Strict; max-age=900`;
|
||||
}
|
||||
|
||||
export const useAuthStore = create<AuthState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
accessToken: null,
|
||||
refreshToken: null,
|
||||
user: null,
|
||||
|
||||
setTokens: (accessToken, refreshToken) => {
|
||||
persistToken(accessToken);
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem('refresh_token', refreshToken);
|
||||
}
|
||||
set({ accessToken, refreshToken });
|
||||
},
|
||||
|
||||
setUser: (user) => set({ user }),
|
||||
|
||||
updateBalance: (newBalance) => {
|
||||
const { user } = get();
|
||||
if (user) set({ user: { ...user, balance: newBalance } });
|
||||
},
|
||||
|
||||
logout: () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.removeItem('access_token');
|
||||
localStorage.removeItem('refresh_token');
|
||||
// Clear the auth cookie so middleware stops treating this session as logged in.
|
||||
document.cookie = 'access_token=; path=/; max-age=0';
|
||||
}
|
||||
set({ accessToken: null, refreshToken: null, user: null });
|
||||
},
|
||||
}),
|
||||
{ name: 'cu-points-auth' }
|
||||
)
|
||||
);
|
||||
Reference in New Issue
Block a user