fix: add auth endpoints to server, fix auth bypass and registration
- server/: commit server code for the first time (was untracked)
- POST /auth/signup and /auth/login endpoints now deployed
- GET /v1/billing/summary now verifies user exists in auth_users
(prevents stale JWTs from bypassing auth → fixes empty dashboard)
- app/_layout.tsx: dual-marker install check (SQLite + SecureStore)
to detect fresh installs reliably on Android
- app/auth/login.tsx, signup.tsx: replace Ionicons leaf logo with
actual app icon image (assets/icon.png)
- services/authService.ts: log HTTP status + server message on auth errors
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
121
services/authService.ts
Normal file
121
services/authService.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
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';
|
||||
}
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user