feat: auth service + security audit fixes + cleanup legacy services
Major changes:
- Add auth-svc: JWT auth, register/login/refresh, password reset
- Add auth UI: modals, pages (/login, /register, /forgot-password)
- Add usage tracking (usage_metrics table, daily limits)
- Add tiered rate limiting (free/pro/business)
- Add LLM usage limits per tier
Security fixes:
- All repos now require userID for Update/Delete operations
- JWT middleware in chat-svc, llm-svc, agent-svc, discover-svc
- ErrNotFound/ErrForbidden errors for proper access control
Cleanup:
- Remove legacy TypeScript services/ directory
- Remove computer-svc (to be reimplemented)
- Remove old deploy/docker configs
New files:
- backend/cmd/auth-svc/main.go
- backend/internal/auth/{types,repository}.go
- backend/internal/usage/{types,repository}.go
- backend/pkg/middleware/{llm_limits,ratelimit_tiered}.go
- backend/webui/src/components/auth/*
- backend/webui/src/app/(auth)/*
Made-with: Cursor
This commit is contained in:
276
backend/webui/src/lib/auth.ts
Normal file
276
backend/webui/src/lib/auth.ts
Normal file
@@ -0,0 +1,276 @@
|
||||
const API_BASE = process.env.NEXT_PUBLIC_API_URL || '';
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
avatar?: string;
|
||||
role: 'user' | 'admin';
|
||||
tier: 'free' | 'pro' | 'business';
|
||||
emailVerified: boolean;
|
||||
provider: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface AuthTokens {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
expiresIn: number;
|
||||
tokenType: string;
|
||||
user: User;
|
||||
}
|
||||
|
||||
export interface RegisterRequest {
|
||||
email: string;
|
||||
password: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface LoginRequest {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface ChangePasswordRequest {
|
||||
currentPassword: string;
|
||||
newPassword: string;
|
||||
}
|
||||
|
||||
export interface ResetPasswordRequest {
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface ResetPasswordConfirm {
|
||||
token: string;
|
||||
newPassword: string;
|
||||
}
|
||||
|
||||
export interface UpdateProfileRequest {
|
||||
name: string;
|
||||
avatar?: string;
|
||||
}
|
||||
|
||||
const TOKEN_KEY = 'token';
|
||||
const REFRESH_TOKEN_KEY = 'refreshToken';
|
||||
const USER_KEY = 'user';
|
||||
|
||||
export function getStoredToken(): string | null {
|
||||
if (typeof window === 'undefined') return null;
|
||||
return localStorage.getItem(TOKEN_KEY);
|
||||
}
|
||||
|
||||
export function getStoredRefreshToken(): string | null {
|
||||
if (typeof window === 'undefined') return null;
|
||||
return localStorage.getItem(REFRESH_TOKEN_KEY);
|
||||
}
|
||||
|
||||
export function getStoredUser(): User | null {
|
||||
if (typeof window === 'undefined') return null;
|
||||
const data = localStorage.getItem(USER_KEY);
|
||||
if (!data) return null;
|
||||
try {
|
||||
return JSON.parse(data);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function storeAuth(tokens: AuthTokens): void {
|
||||
localStorage.setItem(TOKEN_KEY, tokens.accessToken);
|
||||
localStorage.setItem(REFRESH_TOKEN_KEY, tokens.refreshToken);
|
||||
localStorage.setItem(USER_KEY, JSON.stringify(tokens.user));
|
||||
}
|
||||
|
||||
export function clearAuth(): void {
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
localStorage.removeItem(REFRESH_TOKEN_KEY);
|
||||
localStorage.removeItem(USER_KEY);
|
||||
}
|
||||
|
||||
async function handleResponse<T>(response: Response): Promise<T> {
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}));
|
||||
throw new Error(data.error || `Request failed: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function register(data: RegisterRequest): Promise<AuthTokens> {
|
||||
const response = await fetch(`${API_BASE}/api/v1/auth/register`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
const tokens = await handleResponse<AuthTokens>(response);
|
||||
storeAuth(tokens);
|
||||
return tokens;
|
||||
}
|
||||
|
||||
export async function login(data: LoginRequest): Promise<AuthTokens> {
|
||||
const response = await fetch(`${API_BASE}/api/v1/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
const tokens = await handleResponse<AuthTokens>(response);
|
||||
storeAuth(tokens);
|
||||
return tokens;
|
||||
}
|
||||
|
||||
export async function refreshTokens(): Promise<AuthTokens | null> {
|
||||
const refreshToken = getStoredRefreshToken();
|
||||
if (!refreshToken) return null;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/v1/auth/refresh`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ refreshToken }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
clearAuth();
|
||||
return null;
|
||||
}
|
||||
|
||||
const tokens = await response.json();
|
||||
storeAuth(tokens);
|
||||
return tokens;
|
||||
} catch {
|
||||
clearAuth();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function logout(): Promise<void> {
|
||||
const token = getStoredToken();
|
||||
const refreshToken = getStoredRefreshToken();
|
||||
|
||||
if (token) {
|
||||
try {
|
||||
await fetch(`${API_BASE}/api/v1/auth/logout`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ refreshToken }),
|
||||
});
|
||||
} catch {
|
||||
// Ignore errors during logout
|
||||
}
|
||||
}
|
||||
|
||||
clearAuth();
|
||||
}
|
||||
|
||||
export async function logoutAll(): Promise<void> {
|
||||
const token = getStoredToken();
|
||||
|
||||
if (token) {
|
||||
try {
|
||||
await fetch(`${API_BASE}/api/v1/auth/logout-all`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
}
|
||||
|
||||
clearAuth();
|
||||
}
|
||||
|
||||
export async function getMe(): Promise<User | null> {
|
||||
const token = getStoredToken();
|
||||
if (!token) return null;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/v1/auth/me`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
const refreshed = await refreshTokens();
|
||||
if (refreshed) {
|
||||
return getMe();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return response.json();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateProfile(data: UpdateProfileRequest): Promise<User> {
|
||||
const token = getStoredToken();
|
||||
if (!token) throw new Error('Not authenticated');
|
||||
|
||||
const response = await fetch(`${API_BASE}/api/v1/auth/me`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
const user = await handleResponse<User>(response);
|
||||
|
||||
const storedUser = getStoredUser();
|
||||
if (storedUser) {
|
||||
localStorage.setItem(USER_KEY, JSON.stringify({ ...storedUser, ...user }));
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
export async function changePassword(data: ChangePasswordRequest): Promise<void> {
|
||||
const token = getStoredToken();
|
||||
if (!token) throw new Error('Not authenticated');
|
||||
|
||||
const response = await fetch(`${API_BASE}/api/v1/auth/change-password`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
await handleResponse<{ message: string }>(response);
|
||||
}
|
||||
|
||||
export async function forgotPassword(email: string): Promise<void> {
|
||||
const response = await fetch(`${API_BASE}/api/v1/auth/forgot-password`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email }),
|
||||
});
|
||||
|
||||
await handleResponse<{ message: string }>(response);
|
||||
}
|
||||
|
||||
export async function resetPassword(data: ResetPasswordConfirm): Promise<void> {
|
||||
const response = await fetch(`${API_BASE}/api/v1/auth/reset-password`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
await handleResponse<{ message: string }>(response);
|
||||
}
|
||||
|
||||
export function isAuthenticated(): boolean {
|
||||
return !!getStoredToken();
|
||||
}
|
||||
Reference in New Issue
Block a user