mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-06-04 04:22:57 +00:00
small fix, don't worry about it
This commit is contained in:
Generated
+1246
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "containr-auth",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "node src/server.js",
|
||||
"start": "node src/server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"better-auth": "^1.3.4",
|
||||
"dotenv": "^16.6.1",
|
||||
"express": "^4.21.2",
|
||||
"kysely": "^0.28.15",
|
||||
"pg": "^8.16.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
import { betterAuth } from 'better-auth';
|
||||
import { magicLink } from 'better-auth/plugins/magic-link';
|
||||
import { genericOAuth } from 'better-auth/plugins/generic-oauth';
|
||||
import { Pool } from 'pg';
|
||||
import { PostgresDialect } from 'kysely';
|
||||
import { getMigrations } from 'better-auth/db/migration';
|
||||
|
||||
const env = process.env;
|
||||
const defaultBackendURL = env.BACKEND_URL?.trim() || 'http://localhost:8082';
|
||||
|
||||
const asBool = (value, fallback = false) => {
|
||||
if (value === undefined || value === null || value === '') return fallback;
|
||||
return ['1', 'true', 'yes', 'on'].includes(String(value).toLowerCase());
|
||||
};
|
||||
|
||||
const splitCsv = (value) =>
|
||||
String(value || '')
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const buildPoolConfig = () => {
|
||||
const host = env.DB_HOST?.trim();
|
||||
const user = env.DB_USER?.trim();
|
||||
const password = env.DB_PASSWORD ?? '';
|
||||
const database = env.DB_NAME?.trim();
|
||||
|
||||
if (host && user && database) {
|
||||
return {
|
||||
host,
|
||||
port: Number.parseInt(env.DB_PORT || '5432', 10),
|
||||
user,
|
||||
password,
|
||||
database,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
connectionString: env.DATABASE_URL,
|
||||
};
|
||||
};
|
||||
|
||||
const pool = new Pool({
|
||||
...buildPoolConfig(),
|
||||
});
|
||||
|
||||
const database = new PostgresDialect({
|
||||
pool,
|
||||
});
|
||||
|
||||
const githubClientId = env.GITHUB_CLIENT_ID?.trim() || '';
|
||||
const githubClientSecret = env.GITHUB_CLIENT_SECRET?.trim() || '';
|
||||
|
||||
const gitlabClientId = env.GITLAB_CLIENT_ID?.trim() || 'PLACEHOLDER_GITLAB_CLIENT_ID';
|
||||
const gitlabClientSecret = env.GITLAB_CLIENT_SECRET?.trim() || 'PLACEHOLDER_GITLAB_CLIENT_SECRET';
|
||||
const gitlabAuthorizeUrl =
|
||||
env.GITLAB_OAUTH_AUTHORIZE_URL?.trim() || 'https://gitlab.com/oauth/authorize';
|
||||
const gitlabTokenUrl = env.GITLAB_OAUTH_TOKEN_URL?.trim() || 'https://gitlab.com/oauth/token';
|
||||
const gitlabUserUrl = env.GITLAB_OAUTH_USERINFO_URL?.trim() || 'https://gitlab.com/api/v4/user';
|
||||
|
||||
const giteaClientId = env.GITEA_CLIENT_ID?.trim() || 'PLACEHOLDER_GITEA_CLIENT_ID';
|
||||
const giteaClientSecret = env.GITEA_CLIENT_SECRET?.trim() || 'PLACEHOLDER_GITEA_CLIENT_SECRET';
|
||||
const giteaAuthorizeUrl =
|
||||
env.GITEA_OAUTH_AUTHORIZE_URL?.trim() ||
|
||||
'https://gitea.example.com/login/oauth/authorize';
|
||||
const giteaTokenUrl =
|
||||
env.GITEA_OAUTH_TOKEN_URL?.trim() ||
|
||||
'https://gitea.example.com/login/oauth/access_token';
|
||||
const giteaUserUrl = env.GITEA_OAUTH_USERINFO_URL?.trim() || 'https://gitea.example.com/api/v1/user';
|
||||
|
||||
const bitbucketClientId = env.BITBUCKET_CLIENT_ID?.trim() || 'PLACEHOLDER_BITBUCKET_CLIENT_ID';
|
||||
const bitbucketClientSecret =
|
||||
env.BITBUCKET_CLIENT_SECRET?.trim() || 'PLACEHOLDER_BITBUCKET_CLIENT_SECRET';
|
||||
const bitbucketAuthorizeUrl =
|
||||
env.BITBUCKET_OAUTH_AUTHORIZE_URL?.trim() || 'https://bitbucket.org/site/oauth2/authorize';
|
||||
const bitbucketTokenUrl =
|
||||
env.BITBUCKET_OAUTH_TOKEN_URL?.trim() || 'https://bitbucket.org/site/oauth2/access_token';
|
||||
const bitbucketUserUrl =
|
||||
env.BITBUCKET_OAUTH_USERINFO_URL?.trim() || 'https://api.bitbucket.org/2.0/user';
|
||||
const bitbucketEmailsUrl =
|
||||
env.BITBUCKET_OAUTH_EMAILS_URL?.trim() || 'https://api.bitbucket.org/2.0/user/emails';
|
||||
|
||||
const socialProviders = {};
|
||||
if (githubClientId && githubClientSecret) {
|
||||
socialProviders.github = {
|
||||
clientId: githubClientId,
|
||||
clientSecret: githubClientSecret,
|
||||
};
|
||||
}
|
||||
|
||||
const trustedOrigins = [
|
||||
...splitCsv(env.BETTER_AUTH_TRUSTED_ORIGINS),
|
||||
env.FRONTEND_URL,
|
||||
env.BACKEND_URL,
|
||||
env.BETTER_AUTH_URL,
|
||||
defaultBackendURL,
|
||||
'http://localhost:3000',
|
||||
'http://localhost:8082',
|
||||
'http://localhost:3001',
|
||||
]
|
||||
.map((origin) => (origin || '').trim())
|
||||
.filter(Boolean);
|
||||
|
||||
export const auth = betterAuth({
|
||||
appName: env.BETTER_AUTH_APP_NAME || 'Containr',
|
||||
baseURL: env.BETTER_AUTH_URL || defaultBackendURL,
|
||||
basePath: '/api/auth',
|
||||
secret: env.BETTER_AUTH_SECRET || 'PLACEHOLDER_BETTER_AUTH_SECRET_CHANGE_ME_32CHARS_MIN',
|
||||
trustedOrigins,
|
||||
database,
|
||||
user: {
|
||||
modelName: 'auth_users',
|
||||
},
|
||||
session: {
|
||||
modelName: 'auth_sessions',
|
||||
},
|
||||
account: {
|
||||
modelName: 'auth_accounts',
|
||||
},
|
||||
verification: {
|
||||
modelName: 'auth_verifications',
|
||||
},
|
||||
rateLimit: {
|
||||
modelName: 'auth_rate_limits',
|
||||
enabled: asBool(env.BETTER_AUTH_RATE_LIMIT_ENABLED, false),
|
||||
},
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
minPasswordLength: 8,
|
||||
maxPasswordLength: 128,
|
||||
},
|
||||
socialProviders: Object.keys(socialProviders).length > 0 ? socialProviders : undefined,
|
||||
plugins: [
|
||||
magicLink({
|
||||
sendMagicLink: async ({ email, url, token, metadata }) => {
|
||||
const payload = {
|
||||
email,
|
||||
url,
|
||||
token,
|
||||
metadata,
|
||||
};
|
||||
console.log('[better-auth] magic-link', JSON.stringify(payload));
|
||||
},
|
||||
disableSignUp: false,
|
||||
}),
|
||||
genericOAuth({
|
||||
config: [
|
||||
{
|
||||
providerId: 'gitlab',
|
||||
clientId: gitlabClientId,
|
||||
clientSecret: gitlabClientSecret,
|
||||
authorizationUrl: gitlabAuthorizeUrl,
|
||||
tokenUrl: gitlabTokenUrl,
|
||||
userInfoUrl: gitlabUserUrl,
|
||||
scopes: ['read_user', 'read_api'],
|
||||
getUserInfo: async (tokens) => {
|
||||
if (!tokens.accessToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const response = await fetch(gitlabUserUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${tokens.accessToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const profile = await response.json();
|
||||
if (!profile || (!profile.id && !profile.username)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: profile.id || profile.username,
|
||||
email: profile.email || null,
|
||||
name: profile.name || profile.username || 'GitLab User',
|
||||
image: profile.avatar_url || null,
|
||||
emailVerified: Boolean(profile.email),
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
providerId: 'gitea',
|
||||
clientId: giteaClientId,
|
||||
clientSecret: giteaClientSecret,
|
||||
authorizationUrl: giteaAuthorizeUrl,
|
||||
tokenUrl: giteaTokenUrl,
|
||||
userInfoUrl: giteaUserUrl,
|
||||
scopes: ['read:user', 'read:repository'],
|
||||
getUserInfo: async (tokens) => {
|
||||
if (!tokens.accessToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const response = await fetch(giteaUserUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `token ${tokens.accessToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const profile = await response.json();
|
||||
if (!profile || (!profile.id && !profile.login)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: profile.id || profile.login,
|
||||
email: profile.email || null,
|
||||
name: profile.full_name || profile.login || 'Gitea User',
|
||||
image: profile.avatar_url || null,
|
||||
emailVerified: Boolean(profile.email),
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
providerId: 'bitbucket',
|
||||
clientId: bitbucketClientId,
|
||||
clientSecret: bitbucketClientSecret,
|
||||
authorizationUrl: bitbucketAuthorizeUrl,
|
||||
tokenUrl: bitbucketTokenUrl,
|
||||
userInfoUrl: bitbucketUserUrl,
|
||||
scopes: ['account', 'email', 'repository'],
|
||||
getUserInfo: async (tokens) => {
|
||||
if (!tokens.accessToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const userResponse = await fetch(bitbucketUserUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${tokens.accessToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!userResponse.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const user = await userResponse.json();
|
||||
if (!user || (!user.account_id && !user.username && !user.nickname)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let primaryEmail = null;
|
||||
const emailsResponse = await fetch(bitbucketEmailsUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${tokens.accessToken}`,
|
||||
},
|
||||
});
|
||||
if (emailsResponse.ok) {
|
||||
const emailsPayload = await emailsResponse.json();
|
||||
const values = Array.isArray(emailsPayload?.values) ? emailsPayload.values : [];
|
||||
const primary = values.find((entry) => entry?.is_primary) || values[0];
|
||||
if (primary && typeof primary.email === 'string') {
|
||||
primaryEmail = primary.email;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: user.account_id || user.username || user.nickname,
|
||||
email: primaryEmail,
|
||||
name: user.display_name || user.nickname || 'Bitbucket User',
|
||||
image: user?.links?.avatar?.href || null,
|
||||
emailVerified: Boolean(primaryEmail),
|
||||
};
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
export async function runAuthMigrations() {
|
||||
const { runMigrations } = await getMigrations(auth.options);
|
||||
|
||||
await runMigrations();
|
||||
}
|
||||
|
||||
export async function closeAuthDatabase() {
|
||||
await pool.end();
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import 'dotenv/config';
|
||||
import express from 'express';
|
||||
import { fromNodeHeaders, toNodeHandler } from 'better-auth/node';
|
||||
import { auth, closeAuthDatabase, runAuthMigrations } from './auth.js';
|
||||
|
||||
const app = express();
|
||||
const port = Number.parseInt(process.env.AUTH_PORT || '3001', 10);
|
||||
const internalToken = process.env.BETTER_AUTH_INTERNAL_TOKEN || '';
|
||||
const autoMigrate = ['1', 'true', 'yes', 'on'].includes(
|
||||
String(process.env.BETTER_AUTH_AUTO_MIGRATE || 'true').toLowerCase(),
|
||||
);
|
||||
|
||||
const authHandler = toNodeHandler(auth);
|
||||
|
||||
app.get('/health', (_req, res) => {
|
||||
res.status(200).json({
|
||||
status: 'ok',
|
||||
service: 'containr-auth',
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/internal/session', async (req, res) => {
|
||||
if (!internalToken || req.header('x-containr-auth-internal') !== internalToken) {
|
||||
return res.status(401).json({
|
||||
authenticated: false,
|
||||
error: 'Unauthorized',
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const session = await auth.api.getSession({
|
||||
headers: fromNodeHeaders(req.headers),
|
||||
});
|
||||
|
||||
if (!session?.user || !session?.session) {
|
||||
return res.status(401).json({ authenticated: false });
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
authenticated: true,
|
||||
user: session.user,
|
||||
session: session.session,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[better-auth] failed to get session', error);
|
||||
return res.status(500).json({
|
||||
authenticated: false,
|
||||
error: 'Failed to verify session',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
app.all('/api/auth', (req, res) => authHandler(req, res));
|
||||
app.all('/api/auth/*', (req, res) => authHandler(req, res));
|
||||
|
||||
let shuttingDown = false;
|
||||
|
||||
const server = app.listen(port, async () => {
|
||||
if (autoMigrate) {
|
||||
try {
|
||||
await runAuthMigrations();
|
||||
console.log('[better-auth] migrations completed');
|
||||
} catch (error) {
|
||||
console.warn('[better-auth] migration skipped due to adapter limitations', error);
|
||||
}
|
||||
}
|
||||
console.log(`[better-auth] ready on :${port}`);
|
||||
});
|
||||
|
||||
const shutdown = async () => {
|
||||
if (shuttingDown) {
|
||||
return;
|
||||
}
|
||||
|
||||
shuttingDown = true;
|
||||
server.close(async () => {
|
||||
await closeAuthDatabase();
|
||||
process.exit(0);
|
||||
});
|
||||
};
|
||||
|
||||
process.on('SIGINT', shutdown);
|
||||
process.on('SIGTERM', shutdown);
|
||||
Reference in New Issue
Block a user