Spaces: - Perplexity-like UI with collaboration features - Space detail page with threads/members tabs - Invite members via email, role management - New space creation with icon/color picker Model selector: - Added Ollama client for free Auto model - GooSeek 1.0 via Timeweb (tariff-based) - Frontend model dropdown in ChatInput Auth & Infrastructure: - Fixed auth-svc missing from Dockerfile.all - Removed duplicate ratelimit_tiered.go (conflict) - Added Redis to api-gateway for rate limiting - Fixed Next.js proxy for local development UI improvements: - Redesigned login button in sidebar (gradient) - Settings page with tabs (account/billing/prefs) - Auth pages visual refresh Made-with: Cursor
278 lines
6.6 KiB
TypeScript
278 lines
6.6 KiB
TypeScript
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';
|
|
balance: number;
|
|
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();
|
|
}
|