122 lines
4.0 KiB
TypeScript
122 lines
4.0 KiB
TypeScript
import * as SecureStore from 'expo-secure-store';
|
|
import { AuthDb } from './database';
|
|
|
|
const SESSION_KEY = 'greenlens_session_v3';
|
|
const BACKEND_URL = (
|
|
process.env.EXPO_PUBLIC_BACKEND_URL ||
|
|
process.env.EXPO_PUBLIC_PAYMENT_SERVER_URL ||
|
|
''
|
|
).trim();
|
|
|
|
export interface AuthSession {
|
|
userId: number; // local SQLite id (for plants/settings queries)
|
|
serverUserId: string; // server-side user id (in JWT)
|
|
email: string;
|
|
name: string;
|
|
token: string; // JWT from server
|
|
loggedInAt: string;
|
|
}
|
|
|
|
// ─── Internal helpers ──────────────────────────────────────────────────────
|
|
|
|
const clearStoredSession = async (): Promise<void> => {
|
|
await SecureStore.deleteItemAsync(SESSION_KEY);
|
|
};
|
|
|
|
const authPost = async (path: string, body: object): Promise<{ userId: string; email: string; name: string; token: string }> => {
|
|
const hasBackendUrl = Boolean(BACKEND_URL);
|
|
const url = hasBackendUrl ? `${BACKEND_URL}${path}` : path;
|
|
let response: Response;
|
|
try {
|
|
response = await fetch(url, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body),
|
|
});
|
|
} catch (e) {
|
|
if (!hasBackendUrl) {
|
|
throw new Error('BACKEND_URL_MISSING');
|
|
}
|
|
throw new Error('NETWORK_ERROR');
|
|
}
|
|
const data = await response.json().catch(() => ({}));
|
|
if (!response.ok) {
|
|
const code = (data as any).code || 'AUTH_ERROR';
|
|
const msg = (data as any).message || '';
|
|
console.warn(`[Auth] ${path} failed:`, response.status, code, msg);
|
|
throw new Error(code);
|
|
}
|
|
return data as any;
|
|
};
|
|
|
|
const buildSession = (data: { userId: string; email: string; name: string; token: string }): AuthSession => {
|
|
const localUser = AuthDb.ensureLocalUser(data.email, data.name);
|
|
return {
|
|
userId: localUser.id,
|
|
serverUserId: data.userId,
|
|
email: data.email,
|
|
name: data.name,
|
|
token: data.token,
|
|
loggedInAt: new Date().toISOString(),
|
|
};
|
|
};
|
|
|
|
// ─── AuthService ───────────────────────────────────────────────────────────
|
|
|
|
export const AuthService = {
|
|
async getSession(): Promise<AuthSession | null> {
|
|
try {
|
|
const raw = await SecureStore.getItemAsync(SESSION_KEY);
|
|
if (!raw) return null;
|
|
const parsed = JSON.parse(raw) as Partial<AuthSession>;
|
|
if (!parsed.token || !parsed.serverUserId || !parsed.userId) {
|
|
await clearStoredSession();
|
|
return null;
|
|
}
|
|
return parsed as AuthSession;
|
|
} catch {
|
|
await clearStoredSession();
|
|
return null;
|
|
}
|
|
},
|
|
|
|
async signUp(email: string, name: string, password: string): Promise<AuthSession> {
|
|
const data = await authPost('/auth/signup', { email, name, password });
|
|
const session = buildSession(data);
|
|
await SecureStore.setItemAsync(SESSION_KEY, JSON.stringify(session));
|
|
return session;
|
|
},
|
|
|
|
async login(email: string, password: string): Promise<AuthSession> {
|
|
const data = await authPost('/auth/login', { email, password });
|
|
const session = buildSession(data);
|
|
await SecureStore.setItemAsync(SESSION_KEY, JSON.stringify(session));
|
|
return session;
|
|
},
|
|
|
|
async logout(): Promise<void> {
|
|
await clearStoredSession();
|
|
},
|
|
|
|
async updateSessionName(name: string): Promise<void> {
|
|
const session = await this.getSession();
|
|
if (!session) return;
|
|
await SecureStore.setItemAsync(SESSION_KEY, JSON.stringify({ ...session, name }));
|
|
},
|
|
|
|
async validateWithServer(): Promise<'valid' | 'invalid' | 'unreachable'> {
|
|
const session = await this.getSession();
|
|
if (!session) return 'invalid';
|
|
if (!BACKEND_URL) return 'unreachable';
|
|
try {
|
|
const response = await fetch(`${BACKEND_URL}/v1/billing/summary`, {
|
|
headers: { Authorization: `Bearer ${session.token}` },
|
|
});
|
|
if (response.status === 401 || response.status === 403) return 'invalid';
|
|
return 'valid';
|
|
} catch {
|
|
return 'unreachable';
|
|
}
|
|
},
|
|
};
|