OAuth routes (Google/Facebook/Apple) + Android web export
- Google OAuth: /auth/google + /auth/google/callback - Facebook OAuth: /auth/facebook + /auth/facebook/callback - Apple Sign In placeholder - Expo web export in mobile/dist/ - passport, passport-google-oauth20, passport-facebook installed Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
108
api/package-lock.json
generated
108
api/package-lock.json
generated
@@ -21,6 +21,9 @@
|
||||
"dotenv": "^17.3.1",
|
||||
"fastify": "^5.8.4",
|
||||
"ioredis": "^5.10.1",
|
||||
"passport": "^0.7.0",
|
||||
"passport-facebook": "^3.0.0",
|
||||
"passport-google-oauth20": "^2.0.0",
|
||||
"pg": "^8.20.0",
|
||||
"redis": "^5.11.0",
|
||||
"uuid": "^13.0.0",
|
||||
@@ -565,6 +568,15 @@
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/base64url": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz",
|
||||
"integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bcrypt": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz",
|
||||
@@ -1419,6 +1431,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/oauth": {
|
||||
"version": "0.10.2",
|
||||
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.2.tgz",
|
||||
"integrity": "sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/obliterator": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.5.tgz",
|
||||
@@ -1440,6 +1458,76 @@
|
||||
"integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/passport": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz",
|
||||
"integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"passport-strategy": "1.x.x",
|
||||
"pause": "0.0.1",
|
||||
"utils-merge": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jaredhanson"
|
||||
}
|
||||
},
|
||||
"node_modules/passport-facebook": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/passport-facebook/-/passport-facebook-3.0.0.tgz",
|
||||
"integrity": "sha512-K/qNzuFsFISYAyC1Nma4qgY/12V3RSLFdFVsPKXiKZt434wOvthFW1p7zKa1iQihQMRhaWorVE1o3Vi1o+ZgeQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"passport-oauth2": "1.x.x"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/passport-google-oauth20": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz",
|
||||
"integrity": "sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"passport-oauth2": "1.x.x"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/passport-oauth2": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz",
|
||||
"integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64url": "3.x.x",
|
||||
"oauth": "0.10.x",
|
||||
"passport-strategy": "1.x.x",
|
||||
"uid2": "0.0.x",
|
||||
"utils-merge": "1.x.x"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jaredhanson"
|
||||
}
|
||||
},
|
||||
"node_modules/passport-strategy": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
|
||||
"integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==",
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/path-scurry": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
|
||||
@@ -1456,6 +1544,11 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/pause": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
|
||||
"integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
|
||||
},
|
||||
"node_modules/pg": {
|
||||
"version": "8.20.0",
|
||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz",
|
||||
@@ -1985,6 +2078,12 @@
|
||||
"integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/uid2": {
|
||||
"version": "0.0.4",
|
||||
"resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz",
|
||||
"integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undefsafe": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
|
||||
@@ -1992,6 +2091,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/utils-merge": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
|
||||
|
||||
@@ -25,6 +25,9 @@
|
||||
"dotenv": "^17.3.1",
|
||||
"fastify": "^5.8.4",
|
||||
"ioredis": "^5.10.1",
|
||||
"passport": "^0.7.0",
|
||||
"passport-facebook": "^3.0.0",
|
||||
"passport-google-oauth20": "^2.0.0",
|
||||
"pg": "^8.20.0",
|
||||
"redis": "^5.11.0",
|
||||
"uuid": "^13.0.0",
|
||||
|
||||
@@ -62,6 +62,7 @@ const start = async () => {
|
||||
await app.register(require("./routes/tasks"), { prefix: "/api/v1" });
|
||||
await app.register(require("./routes/groups"), { prefix: "/api/v1" });
|
||||
await app.register(require("./routes/auth"), { prefix: "/api/v1" });
|
||||
await app.register(require("./routes/oauth"), { prefix: "/api/v1" });
|
||||
await app.register(require("./routes/connectors"), { prefix: "/api/v1" });
|
||||
await app.register(require("./routes/connectors/odoo"), { prefix: "/api/v1" });
|
||||
await app.register(require("./routes/connectors/moodle"), { prefix: "/api/v1" });
|
||||
|
||||
@@ -88,12 +88,7 @@ async function authRoutes(app) {
|
||||
return { status: 'ok', message: 'Password changed' };
|
||||
});
|
||||
|
||||
// OAuth callback placeholder (for Google/Apple/Facebook)
|
||||
app.get('/auth/oauth/:provider', async (req) => {
|
||||
const { provider } = req.params;
|
||||
// TODO: Implement OAuth flows
|
||||
return { status: 'not_implemented', provider, message: 'OAuth coming soon. Use email/password.' };
|
||||
});
|
||||
// OAuth routes moved to ./oauth.js (Google, Facebook, Apple)
|
||||
|
||||
// TOTP 2FA setup
|
||||
app.post('/auth/2fa/setup', { preHandler: [async (req) => { await req.jwtVerify(); }] }, async (req) => {
|
||||
@@ -106,13 +101,7 @@ async function authRoutes(app) {
|
||||
return { status: 'not_implemented', message: 'WebAuthn biometric auth coming soon.' };
|
||||
});
|
||||
|
||||
// OAuth initiate (placeholder)
|
||||
app.get('/auth/oauth/:provider/init', async (req) => {
|
||||
const { provider } = req.params;
|
||||
const providers = ['google', 'facebook', 'apple'];
|
||||
if (!providers.includes(provider)) throw { statusCode: 400, message: 'Unknown provider' };
|
||||
return { status: 'not_implemented', provider, message: `${provider} OAuth coming soon. Configure at Settings > Connections.` };
|
||||
});
|
||||
// OAuth initiate routes moved to ./oauth.js
|
||||
|
||||
// Search users by name or email (for collaboration)
|
||||
app.get('/auth/users/search', async (req) => {
|
||||
|
||||
296
api/src/routes/oauth.js
Normal file
296
api/src/routes/oauth.js
Normal file
@@ -0,0 +1,296 @@
|
||||
// Task Team — OAuth Routes (Google + Facebook + Apple) — 2026-03-29
|
||||
|
||||
async function oauthRoutes(app) {
|
||||
|
||||
// OAuth config from environment
|
||||
const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID || '';
|
||||
const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET || '';
|
||||
const FACEBOOK_APP_ID = process.env.FACEBOOK_APP_ID || '';
|
||||
const FACEBOOK_APP_SECRET = process.env.FACEBOOK_APP_SECRET || '';
|
||||
const CALLBACK_BASE = process.env.OAUTH_CALLBACK_BASE || 'https://api.hasdo.info';
|
||||
|
||||
// Helper: find or create user by OAuth provider
|
||||
async function findOrCreateOAuthUser(email, name, avatarUrl, provider, providerId) {
|
||||
if (!email) throw { statusCode: 400, message: `${provider} account has no email. Please use an account with email.` };
|
||||
|
||||
// Check if user already exists by email
|
||||
let { rows } = await app.db.query('SELECT * FROM users WHERE email = $1', [email]);
|
||||
|
||||
if (rows.length) {
|
||||
// Update provider info if not set
|
||||
const user = rows[0];
|
||||
if (!user.auth_provider_id || user.auth_provider !== provider) {
|
||||
await app.db.query(
|
||||
'UPDATE users SET auth_provider = $1, auth_provider_id = $2, avatar_url = COALESCE(avatar_url, $3), updated_at = NOW() WHERE id = $4',
|
||||
[provider, providerId, avatarUrl, user.id]
|
||||
);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
// Create new user
|
||||
const insert = await app.db.query(
|
||||
'INSERT INTO users (email, name, avatar_url, auth_provider, auth_provider_id, language) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *',
|
||||
[email, name, avatarUrl, provider, providerId, 'cs']
|
||||
);
|
||||
return insert.rows[0];
|
||||
}
|
||||
|
||||
// Helper: generate JWT and build response
|
||||
function buildAuthResponse(user) {
|
||||
const token = app.jwt.sign({ id: user.id, email: user.email }, { expiresIn: '7d' });
|
||||
return {
|
||||
data: {
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
avatar_url: user.avatar_url,
|
||||
language: user.language
|
||||
},
|
||||
token
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Google OAuth ─────────────────────────────────────────────
|
||||
|
||||
// Google OAuth - initiate login flow
|
||||
app.get('/auth/google', async (req, reply) => {
|
||||
if (!GOOGLE_CLIENT_ID) {
|
||||
return { status: 'not_configured', message: 'Google OAuth not configured. Set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET env vars.' };
|
||||
}
|
||||
const params = new URLSearchParams({
|
||||
client_id: GOOGLE_CLIENT_ID,
|
||||
redirect_uri: `${CALLBACK_BASE}/api/v1/auth/google/callback`,
|
||||
response_type: 'code',
|
||||
scope: 'email profile',
|
||||
access_type: 'offline',
|
||||
prompt: 'select_account'
|
||||
});
|
||||
return reply.redirect(`https://accounts.google.com/o/oauth2/v2/auth?${params}`);
|
||||
});
|
||||
|
||||
// Google OAuth - callback (exchange code for token)
|
||||
app.get('/auth/google/callback', async (req, reply) => {
|
||||
const { code, error } = req.query;
|
||||
if (error) throw { statusCode: 400, message: `Google auth error: ${error}` };
|
||||
if (!code) throw { statusCode: 400, message: 'No authorization code provided' };
|
||||
|
||||
// Exchange authorization code for tokens
|
||||
const tokenRes = await fetch('https://oauth2.googleapis.com/token', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
code,
|
||||
client_id: GOOGLE_CLIENT_ID,
|
||||
client_secret: GOOGLE_CLIENT_SECRET,
|
||||
redirect_uri: `${CALLBACK_BASE}/api/v1/auth/google/callback`,
|
||||
grant_type: 'authorization_code'
|
||||
})
|
||||
});
|
||||
const tokens = await tokenRes.json();
|
||||
if (!tokens.access_token) {
|
||||
app.log.error({ tokens }, 'Google token exchange failed');
|
||||
throw { statusCode: 401, message: 'Google authentication failed' };
|
||||
}
|
||||
|
||||
// Get user profile from Google
|
||||
const userRes = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
|
||||
headers: { Authorization: `Bearer ${tokens.access_token}` }
|
||||
});
|
||||
const gUser = await userRes.json();
|
||||
|
||||
// Find or create user in our DB
|
||||
const user = await findOrCreateOAuthUser(
|
||||
gUser.email,
|
||||
gUser.name,
|
||||
gUser.picture,
|
||||
'google',
|
||||
gUser.id
|
||||
);
|
||||
|
||||
const result = buildAuthResponse(user);
|
||||
|
||||
// If there's a state param with redirect URL, redirect with token
|
||||
const redirectUrl = req.query.state;
|
||||
if (redirectUrl) {
|
||||
const sep = redirectUrl.includes('?') ? '&' : '?';
|
||||
return reply.redirect(`${redirectUrl}${sep}token=${result.data.token}`);
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
// Google OAuth - mobile/SPA token exchange (for clients that get the code directly)
|
||||
app.post('/auth/google/token', async (req) => {
|
||||
const { id_token, access_token } = req.body;
|
||||
if (!id_token && !access_token) throw { statusCode: 400, message: 'id_token or access_token required' };
|
||||
|
||||
let gUser;
|
||||
if (access_token) {
|
||||
const userRes = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
|
||||
headers: { Authorization: `Bearer ${access_token}` }
|
||||
});
|
||||
gUser = await userRes.json();
|
||||
} else {
|
||||
// Verify id_token
|
||||
const verifyRes = await fetch(`https://oauth2.googleapis.com/tokeninfo?id_token=${id_token}`);
|
||||
gUser = await verifyRes.json();
|
||||
if (gUser.aud !== GOOGLE_CLIENT_ID) throw { statusCode: 401, message: 'Invalid token audience' };
|
||||
}
|
||||
|
||||
if (!gUser.email) throw { statusCode: 401, message: 'Could not get email from Google' };
|
||||
|
||||
const user = await findOrCreateOAuthUser(
|
||||
gUser.email,
|
||||
gUser.name || gUser.given_name,
|
||||
gUser.picture,
|
||||
'google',
|
||||
gUser.sub || gUser.id
|
||||
);
|
||||
|
||||
return buildAuthResponse(user);
|
||||
});
|
||||
|
||||
// ─── Facebook OAuth ───────────────────────────────────────────
|
||||
|
||||
// Facebook OAuth - initiate login flow
|
||||
app.get('/auth/facebook', async (req, reply) => {
|
||||
if (!FACEBOOK_APP_ID) {
|
||||
return { status: 'not_configured', message: 'Facebook OAuth not configured. Set FACEBOOK_APP_ID and FACEBOOK_APP_SECRET env vars.' };
|
||||
}
|
||||
const params = new URLSearchParams({
|
||||
client_id: FACEBOOK_APP_ID,
|
||||
redirect_uri: `${CALLBACK_BASE}/api/v1/auth/facebook/callback`,
|
||||
scope: 'email,public_profile',
|
||||
response_type: 'code'
|
||||
});
|
||||
return reply.redirect(`https://www.facebook.com/v18.0/dialog/oauth?${params}`);
|
||||
});
|
||||
|
||||
// Facebook OAuth - callback
|
||||
app.get('/auth/facebook/callback', async (req, reply) => {
|
||||
const { code, error, error_description } = req.query;
|
||||
if (error) throw { statusCode: 400, message: `Facebook auth error: ${error_description || error}` };
|
||||
if (!code) throw { statusCode: 400, message: 'No authorization code provided' };
|
||||
|
||||
// Exchange code for access token
|
||||
const tokenParams = new URLSearchParams({
|
||||
client_id: FACEBOOK_APP_ID,
|
||||
redirect_uri: `${CALLBACK_BASE}/api/v1/auth/facebook/callback`,
|
||||
client_secret: FACEBOOK_APP_SECRET,
|
||||
code
|
||||
});
|
||||
const tokenRes = await fetch(`https://graph.facebook.com/v18.0/oauth/access_token?${tokenParams}`);
|
||||
const tokens = await tokenRes.json();
|
||||
if (!tokens.access_token) {
|
||||
app.log.error({ tokens }, 'Facebook token exchange failed');
|
||||
throw { statusCode: 401, message: 'Facebook authentication failed' };
|
||||
}
|
||||
|
||||
// Get user profile from Facebook
|
||||
const userRes = await fetch(`https://graph.facebook.com/me?fields=id,name,email,picture.type(large)&access_token=${tokens.access_token}`);
|
||||
const fbUser = await userRes.json();
|
||||
|
||||
const user = await findOrCreateOAuthUser(
|
||||
fbUser.email,
|
||||
fbUser.name,
|
||||
fbUser.picture?.data?.url,
|
||||
'facebook',
|
||||
fbUser.id
|
||||
);
|
||||
|
||||
const result = buildAuthResponse(user);
|
||||
|
||||
// Redirect if state was provided
|
||||
const redirectUrl = req.query.state;
|
||||
if (redirectUrl) {
|
||||
const sep = redirectUrl.includes('?') ? '&' : '?';
|
||||
return reply.redirect(`${redirectUrl}${sep}token=${result.data.token}`);
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
// Facebook OAuth - mobile/SPA token exchange
|
||||
app.post('/auth/facebook/token', async (req) => {
|
||||
const { access_token } = req.body;
|
||||
if (!access_token) throw { statusCode: 400, message: 'access_token required' };
|
||||
|
||||
const userRes = await fetch(`https://graph.facebook.com/me?fields=id,name,email,picture.type(large)&access_token=${access_token}`);
|
||||
const fbUser = await userRes.json();
|
||||
if (fbUser.error) throw { statusCode: 401, message: fbUser.error.message };
|
||||
|
||||
const user = await findOrCreateOAuthUser(
|
||||
fbUser.email,
|
||||
fbUser.name,
|
||||
fbUser.picture?.data?.url,
|
||||
'facebook',
|
||||
fbUser.id
|
||||
);
|
||||
|
||||
return buildAuthResponse(user);
|
||||
});
|
||||
|
||||
// ─── Apple Sign In ────────────────────────────────────────────
|
||||
|
||||
// Apple Sign In - callback (Apple sends POST with id_token)
|
||||
app.post('/auth/apple/callback', async (req) => {
|
||||
// Apple sends: id_token, code, state, user (JSON string on first auth)
|
||||
const { id_token, user: userJson } = req.body;
|
||||
if (!id_token) throw { statusCode: 400, message: 'id_token required from Apple' };
|
||||
|
||||
// Decode JWT payload (Apple id_token is a standard JWT)
|
||||
// In production, verify signature against Apple's public keys
|
||||
const parts = id_token.split('.');
|
||||
if (parts.length !== 3) throw { statusCode: 400, message: 'Invalid id_token format' };
|
||||
|
||||
let payload;
|
||||
try {
|
||||
payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
|
||||
} catch (e) {
|
||||
throw { statusCode: 400, message: 'Could not decode id_token' };
|
||||
}
|
||||
|
||||
const email = payload.email;
|
||||
if (!email) throw { statusCode: 400, message: 'No email in Apple id_token' };
|
||||
|
||||
// Apple only sends user info on FIRST authorization
|
||||
let name = 'Apple User';
|
||||
if (userJson) {
|
||||
try {
|
||||
const appleUser = typeof userJson === 'string' ? JSON.parse(userJson) : userJson;
|
||||
if (appleUser.name) {
|
||||
name = [appleUser.name.firstName, appleUser.name.lastName].filter(Boolean).join(' ');
|
||||
}
|
||||
} catch (e) { /* ignore parse errors */ }
|
||||
}
|
||||
|
||||
const user = await findOrCreateOAuthUser(
|
||||
email,
|
||||
name,
|
||||
null,
|
||||
'apple',
|
||||
payload.sub
|
||||
);
|
||||
|
||||
return buildAuthResponse(user);
|
||||
});
|
||||
|
||||
// ─── OAuth Status ─────────────────────────────────────────────
|
||||
|
||||
// Check which OAuth providers are configured
|
||||
app.get('/auth/providers', async () => {
|
||||
return {
|
||||
data: {
|
||||
email: true,
|
||||
google: !!GOOGLE_CLIENT_ID,
|
||||
facebook: !!FACEBOOK_APP_ID,
|
||||
apple: true // Apple Sign In works with just client-side config
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = oauthRoutes;
|
||||
@@ -4,8 +4,8 @@
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
"android": "expo start --android",
|
||||
"ios": "expo start --ios",
|
||||
"android": "expo run:android",
|
||||
"ios": "expo run:ios",
|
||||
"web": "expo start --web",
|
||||
"export:web": "expo export --platform web",
|
||||
"lint": "expo lint"
|
||||
|
||||
Reference in New Issue
Block a user