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:
107
server/lib/auth.js
Normal file
107
server/lib/auth.js
Normal file
@@ -0,0 +1,107 @@
|
||||
const crypto = require('crypto');
|
||||
const { get, run } = require('./sqlite');
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'greenlens-dev-secret-change-in-prod';
|
||||
const TOKEN_EXPIRY_SECONDS = 365 * 24 * 3600; // 1 year
|
||||
|
||||
// ─── Minimal JWT (HS256, no external deps) ─────────────────────────────────
|
||||
|
||||
const b64url = (input) => {
|
||||
const str = typeof input === 'string' ? input : JSON.stringify(input);
|
||||
return Buffer.from(str).toString('base64url');
|
||||
};
|
||||
|
||||
const b64urlDecode = (str) => Buffer.from(str, 'base64url').toString();
|
||||
|
||||
const signJwt = (payload) => {
|
||||
const header = b64url({ alg: 'HS256', typ: 'JWT' });
|
||||
const body = b64url(payload);
|
||||
const sig = crypto.createHmac('sha256', JWT_SECRET).update(`${header}.${body}`).digest('base64url');
|
||||
return `${header}.${body}.${sig}`;
|
||||
};
|
||||
|
||||
const verifyJwt = (token) => {
|
||||
if (!token || typeof token !== 'string') return null;
|
||||
const parts = token.split('.');
|
||||
if (parts.length !== 3) return null;
|
||||
const [header, body, sig] = parts;
|
||||
const expected = crypto.createHmac('sha256', JWT_SECRET).update(`${header}.${body}`).digest('base64url');
|
||||
if (sig !== expected) return null;
|
||||
try {
|
||||
const payload = JSON.parse(b64urlDecode(body));
|
||||
if (payload.exp && Math.floor(Date.now() / 1000) > payload.exp) return null;
|
||||
return payload;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const issueToken = (userId, email, name) =>
|
||||
signJwt({
|
||||
sub: userId,
|
||||
email,
|
||||
name,
|
||||
iat: Math.floor(Date.now() / 1000),
|
||||
exp: Math.floor(Date.now() / 1000) + TOKEN_EXPIRY_SECONDS,
|
||||
});
|
||||
|
||||
// ─── Password hashing ──────────────────────────────────────────────────────
|
||||
|
||||
const hashPassword = (password) =>
|
||||
crypto.createHmac('sha256', JWT_SECRET).update(password).digest('hex');
|
||||
|
||||
// ─── Schema ────────────────────────────────────────────────────────────────
|
||||
|
||||
const ensureAuthSchema = async (db) => {
|
||||
await run(
|
||||
db,
|
||||
`CREATE TABLE IF NOT EXISTS auth_users (
|
||||
id TEXT PRIMARY KEY,
|
||||
email TEXT NOT NULL UNIQUE COLLATE NOCASE,
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
password_hash TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)`,
|
||||
);
|
||||
};
|
||||
|
||||
// ─── Operations ───────────────────────────────────────────────────────────
|
||||
|
||||
const signUp = async (db, email, name, password) => {
|
||||
const normalizedEmail = email.trim().toLowerCase();
|
||||
const existing = await get(db, 'SELECT id FROM auth_users WHERE email = ?', [normalizedEmail]);
|
||||
if (existing) {
|
||||
const err = new Error('Email already in use.');
|
||||
err.code = 'EMAIL_TAKEN';
|
||||
err.status = 409;
|
||||
throw err;
|
||||
}
|
||||
const id = `usr_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
||||
await run(db, 'INSERT INTO auth_users (id, email, name, password_hash) VALUES (?, ?, ?, ?)', [
|
||||
id,
|
||||
normalizedEmail,
|
||||
name.trim(),
|
||||
hashPassword(password),
|
||||
]);
|
||||
return { id, email: normalizedEmail, name: name.trim() };
|
||||
};
|
||||
|
||||
const login = async (db, email, password) => {
|
||||
const normalizedEmail = email.trim().toLowerCase();
|
||||
const user = await get(db, 'SELECT id, email, name, password_hash FROM auth_users WHERE email = ?', [normalizedEmail]);
|
||||
if (!user) {
|
||||
const err = new Error('No account found for this email.');
|
||||
err.code = 'USER_NOT_FOUND';
|
||||
err.status = 401;
|
||||
throw err;
|
||||
}
|
||||
if (user.password_hash !== hashPassword(password)) {
|
||||
const err = new Error('Wrong password.');
|
||||
err.code = 'WRONG_PASSWORD';
|
||||
err.status = 401;
|
||||
throw err;
|
||||
}
|
||||
return { id: user.id, email: user.email, name: user.name };
|
||||
};
|
||||
|
||||
module.exports = { ensureAuthSchema, signUp, login, issueToken, verifyJwt };
|
||||
Reference in New Issue
Block a user