feat: default locale Russian, geo determines language for other countries
- localization-svc: defaultLocale ru, resolveLocale only by geo - web-svc: DEFAULT_LOCALE ru, layout lang=ru, embeddedTranslations fallback ru - countryToLocale: default ru when no country or unknown country Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
63
services/auth-svc/src/index.ts
Normal file
63
services/auth-svc/src/index.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { config } from 'dotenv';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
config({ path: path.resolve(fileURLToPath(import.meta.url), '../../../../.env') });
|
||||
|
||||
/**
|
||||
* auth-svc — SSO, JWT, Bearer validation для API-to-API
|
||||
* docs/architecture: 01-perplexity-analogue-design.md §3.2, auth-svc
|
||||
* Миграция из apps/auth-mcs в services/
|
||||
*/
|
||||
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import { toNodeHandler, fromNodeHeaders } from 'better-auth/node';
|
||||
import { auth } from './lib/auth.js';
|
||||
|
||||
const PORT = parseInt(process.env.PORT ?? '3014', 10);
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(
|
||||
cors({
|
||||
origin: true,
|
||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
|
||||
credentials: true,
|
||||
})
|
||||
);
|
||||
|
||||
app.get('/api/auth/me', async (req, res) => {
|
||||
try {
|
||||
const session = await auth.api.getSession({
|
||||
headers: fromNodeHeaders(req.headers),
|
||||
});
|
||||
if (!session?.user?.id) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
const { id, name, email, image } = session.user;
|
||||
return res.json({ id, name: name ?? undefined, email: email ?? undefined, image: image ?? undefined });
|
||||
} catch {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
});
|
||||
|
||||
app.all('/api/auth/*', toNodeHandler(auth));
|
||||
|
||||
app.use(express.json({ limit: '1mb' }));
|
||||
|
||||
app.get('/health', (_req, res) => res.json({ status: 'ok', service: 'auth-svc' }));
|
||||
app.get('/ready', (_req, res) => res.json({ status: 'ready' }));
|
||||
|
||||
app.use((err: unknown, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
|
||||
console.error('[auth-svc] Unhandled error:', err);
|
||||
res.status(500).json({
|
||||
error: 'Internal server error',
|
||||
message: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
});
|
||||
|
||||
app.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`auth-svc listening on :${PORT}`);
|
||||
console.log(' GET /api/auth/me — Bearer validation for library-svc, memory-svc, etc.');
|
||||
console.log(' Перед первым запуском: cd services/auth-svc && npm run db:migrate');
|
||||
});
|
||||
103
services/auth-svc/src/lib/auth.ts
Normal file
103
services/auth-svc/src/lib/auth.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { betterAuth } from 'better-auth';
|
||||
import { bearer, oidcProvider } from 'better-auth/plugins';
|
||||
import { sso } from '@better-auth/sso';
|
||||
import { credentials } from 'better-auth-credentials-plugin';
|
||||
import { authenticate } from 'ldap-authentication';
|
||||
import { z } from 'zod';
|
||||
import { db } from './db.js';
|
||||
|
||||
const PORT = parseInt(process.env.PORT ?? '3014', 10);
|
||||
const baseUrl =
|
||||
process.env.BETTER_AUTH_URL || process.env.AUTH_SERVICE_URL || `http://localhost:${PORT}`;
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: db,
|
||||
basePath: '/api/auth',
|
||||
baseURL: baseUrl,
|
||||
telemetry: { enabled: false },
|
||||
trustedOrigins: [
|
||||
baseUrl,
|
||||
'http://localhost:3000',
|
||||
'http://localhost:3014',
|
||||
`http://localhost:${PORT}`,
|
||||
...(process.env.TRUSTED_ORIGINS || '').split(',').filter(Boolean),
|
||||
],
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
},
|
||||
plugins: [
|
||||
bearer(),
|
||||
sso(),
|
||||
oidcProvider({
|
||||
loginPage: '/sign-in',
|
||||
allowDynamicClientRegistration: true,
|
||||
trustedClients: (() => {
|
||||
try {
|
||||
if (process.env.TRUSTED_CLIENTS) {
|
||||
return JSON.parse(process.env.TRUSTED_CLIENTS);
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return [
|
||||
{
|
||||
clientId: process.env.DEFAULT_CLIENT_ID || 'gooseek',
|
||||
clientSecret: process.env.DEFAULT_CLIENT_SECRET || 'gooseek-secret-change-me',
|
||||
name: 'GooSeek',
|
||||
type: 'web' as const,
|
||||
redirectUrls: [
|
||||
'http://localhost:3000/api/auth/callback/better-auth',
|
||||
`${baseUrl}/api/auth/callback/better-auth`,
|
||||
'https://gooseek.ru/api/auth/callback/better-auth',
|
||||
'https://www.gooseek.ru/api/auth/callback/better-auth',
|
||||
],
|
||||
disabled: false,
|
||||
skipConsent: true,
|
||||
},
|
||||
];
|
||||
})(),
|
||||
}),
|
||||
...(process.env.LDAP_URL
|
||||
? [
|
||||
// @ts-expect-error — credentials plugin has deep generic instantiation
|
||||
credentials({
|
||||
autoSignUp: true,
|
||||
linkAccountIfExisting: true,
|
||||
providerId: 'ldap',
|
||||
path: '/sign-in/ldap',
|
||||
inputSchema: z.object({
|
||||
credential: z.string().min(1, 'Username or DN required'),
|
||||
password: z.string().min(1, 'Password required'),
|
||||
}),
|
||||
async callback(_ctx, parsed) {
|
||||
const ldapResult = await authenticate({
|
||||
ldapOpts: {
|
||||
url: process.env.LDAP_URL!,
|
||||
connectTimeout: 5000,
|
||||
...(process.env.LDAP_URL!.startsWith('ldaps://')
|
||||
? { tlsOptions: { minVersion: 'TLSv1.2' } }
|
||||
: {}),
|
||||
},
|
||||
adminDn: process.env.LDAP_BIND_DN || '',
|
||||
adminPassword: process.env.LDAP_PASSWORD || '',
|
||||
userSearchBase: process.env.LDAP_BASE_DN || '',
|
||||
usernameAttribute: process.env.LDAP_USERNAME_ATTR || 'uid',
|
||||
username: parsed.credential,
|
||||
userPassword: parsed.password,
|
||||
});
|
||||
|
||||
const uid = ldapResult[process.env.LDAP_USERNAME_ATTR || 'uid'];
|
||||
const email =
|
||||
(Array.isArray(ldapResult.mail) ? ldapResult.mail[0] : ldapResult.mail) ||
|
||||
`${uid}@local`;
|
||||
|
||||
return {
|
||||
email,
|
||||
name: ldapResult.displayName || ldapResult.cn || String(uid),
|
||||
};
|
||||
},
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
],
|
||||
});
|
||||
15
services/auth-svc/src/lib/db.ts
Normal file
15
services/auth-svc/src/lib/db.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import Database from 'better-sqlite3';
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
|
||||
const defaultPath = path.join(process.cwd(), 'data', 'auth.db');
|
||||
const dbPath = process.env.DATABASE_URL?.startsWith('file:')
|
||||
? process.env.DATABASE_URL.replace(/^file:/, '')
|
||||
: process.env.DATABASE_PATH || defaultPath;
|
||||
|
||||
const dir = path.dirname(dbPath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
export const db = new Database(dbPath);
|
||||
Reference in New Issue
Block a user