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:
21
services/auth-svc/Dockerfile
Normal file
21
services/auth-svc/Dockerfile
Normal file
@@ -0,0 +1,21 @@
|
||||
# Сборка из корня: docker build -t gooseek/auth-svc:latest -f services/auth-svc/Dockerfile .
|
||||
FROM node:22-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json ./
|
||||
COPY services/auth-svc/package.json ./services/auth-svc/
|
||||
RUN npm ci -w auth-svc
|
||||
COPY services/auth-svc/tsconfig.json ./services/auth-svc/
|
||||
COPY services/auth-svc/src ./services/auth-svc/src
|
||||
WORKDIR /app/services/auth-svc
|
||||
RUN npm run build
|
||||
|
||||
FROM node:22-alpine
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json ./
|
||||
COPY services/auth-svc/package.json ./services/auth-svc/
|
||||
RUN npm ci -w auth-svc --omit=dev
|
||||
COPY --from=builder /app/services/auth-svc/dist ./services/auth-svc/dist
|
||||
WORKDIR /app/services/auth-svc
|
||||
EXPOSE 3014
|
||||
ENV PORT=3014
|
||||
CMD ["node", "dist/index.js"]
|
||||
BIN
services/auth-svc/data/auth.db
Normal file
BIN
services/auth-svc/data/auth.db
Normal file
Binary file not shown.
33
services/auth-svc/package.json
Normal file
33
services/auth-svc/package.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "auth-svc",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Auth service: SSO, JWT, Bearer validation for API-to-API",
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js",
|
||||
"db:generate": "npx @better-auth/cli generate",
|
||||
"db:migrate": "npx @better-auth/cli migrate --yes"
|
||||
},
|
||||
"dependencies": {
|
||||
"dotenv": "^16.4.5",
|
||||
"@better-auth/sso": "^1.3.0",
|
||||
"better-auth": "^1.4.0",
|
||||
"better-auth-credentials-plugin": "^0.4.0",
|
||||
"better-sqlite3": "^11.9.1",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.21.0",
|
||||
"ldap-authentication": "^3.3.6",
|
||||
"zod": "^3.23.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.6.12",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^22.10.0",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
}
|
||||
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);
|
||||
13
services/auth-svc/tsconfig.json
Normal file
13
services/auth-svc/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
Reference in New Issue
Block a user