mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-22 06:41:46 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0af15fdc88 | |||
| 177f004740 |
@@ -38,6 +38,7 @@
|
|||||||
"helmet": "^8.1.0",
|
"helmet": "^8.1.0",
|
||||||
"jimp": "^1.6.1",
|
"jimp": "^1.6.1",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"ldapts": "^8.1.7",
|
||||||
"multer": "^2.1.1",
|
"multer": "^2.1.1",
|
||||||
"node-cron": "^4.2.1",
|
"node-cron": "^4.2.1",
|
||||||
"nodemailer": "^8.0.5",
|
"nodemailer": "^8.0.5",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
validateInviteToken,
|
validateInviteToken,
|
||||||
registerUser,
|
registerUser,
|
||||||
loginUser,
|
loginUser,
|
||||||
|
ldapLoginUser,
|
||||||
getCurrentUser,
|
getCurrentUser,
|
||||||
changePassword,
|
changePassword,
|
||||||
deleteAccount,
|
deleteAccount,
|
||||||
@@ -150,7 +151,7 @@ router.post('/register', authLimiter, (req: Request, res: Response) => {
|
|||||||
|
|
||||||
router.post('/login', authLimiter, async (req: Request, res: Response) => {
|
router.post('/login', authLimiter, async (req: Request, res: Response) => {
|
||||||
const started = Date.now();
|
const started = Date.now();
|
||||||
const result = loginUser(req.body);
|
const result = await ldapLoginUser(req.body);
|
||||||
if (result.auditAction) {
|
if (result.auditAction) {
|
||||||
writeAudit({ userId: result.auditUserId ?? null, action: result.auditAction, ip: getClientIp(req), details: result.auditDetails });
|
writeAudit({ userId: result.auditUserId ?? null, action: result.auditAction, ip: getClientIp(req), details: result.auditDetails });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -432,7 +432,7 @@ export function loginUser(body: {
|
|||||||
auditAction?: string;
|
auditAction?: string;
|
||||||
auditDetails?: Record<string, unknown>;
|
auditDetails?: Record<string, unknown>;
|
||||||
} {
|
} {
|
||||||
if (isOidcOnlyMode()) {
|
if (isOidcOnlyMode() && !process.env.LDAP_URL) {
|
||||||
return { error: 'Password authentication is disabled. Please sign in with SSO.', status: 403 };
|
return { error: 'Password authentication is disabled. Please sign in with SSO.', status: 403 };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -489,6 +489,82 @@ export function loginUser(body: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// LDAP login — async wrapper (FreeIPA / OpenLDAP)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
export async function ldapLoginUser(body: {
|
||||||
|
email?: string;
|
||||||
|
password?: string;
|
||||||
|
}): Promise<ReturnType<typeof loginUser>> {
|
||||||
|
const { getLdapConfig, ldapAuthenticate } = await import('./ldapService');
|
||||||
|
|
||||||
|
if (!getLdapConfig()) {
|
||||||
|
return loginUser(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { email: usernameOrEmail, password } = body;
|
||||||
|
if (!usernameOrEmail || !password) {
|
||||||
|
return { error: 'Email and password are required', status: 400 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const username = usernameOrEmail.includes('@')
|
||||||
|
? usernameOrEmail.split('@')[0]
|
||||||
|
: usernameOrEmail;
|
||||||
|
|
||||||
|
let ldapUser;
|
||||||
|
try {
|
||||||
|
ldapUser = await ldapAuthenticate(username, password);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[LDAP] Authentication error:', err);
|
||||||
|
return { error: 'LDAP authentication failed', status: 502 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ldapUser) {
|
||||||
|
// User nicht in LDAP — lokalen Login versuchen (z.B. lokaler Admin)
|
||||||
|
return loginUser(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
const role: 'admin' | 'user' = ldapUser.isAdmin ? 'admin' : 'user';
|
||||||
|
let user = db.prepare(
|
||||||
|
'SELECT * FROM users WHERE LOWER(email) = LOWER(?)'
|
||||||
|
).get(ldapUser.email) as User | undefined;
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
if (user.role !== role) {
|
||||||
|
db.prepare('UPDATE users SET role = ? WHERE id = ?').run(role, user.id);
|
||||||
|
user = { ...user, role } as User;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let uname = ldapUser.uid.replace(/[^a-zA-Z0-9_-]/g, '').substring(0, 30) || 'user';
|
||||||
|
const conflict = db.prepare(
|
||||||
|
'SELECT id FROM users WHERE LOWER(username) = LOWER(?)'
|
||||||
|
).get(uname);
|
||||||
|
if (conflict) uname = uname + '_' + String(Date.now() % 10000);
|
||||||
|
|
||||||
|
const result = db.prepare(
|
||||||
|
'INSERT INTO users (username, email, password_hash, role, first_seen_version, login_count) VALUES (?, ?, ?, ?, ?, 0)'
|
||||||
|
).run(uname, ldapUser.email, '!ldap', role, process.env.APP_VERSION || '0.0.0');
|
||||||
|
|
||||||
|
user = db.prepare('SELECT * FROM users WHERE id = ?').get(
|
||||||
|
Number(result.lastInsertRowid)
|
||||||
|
) as User;
|
||||||
|
}
|
||||||
|
|
||||||
|
db.prepare(
|
||||||
|
'UPDATE users SET login_count = login_count + 1, last_login = CURRENT_TIMESTAMP WHERE id = ?'
|
||||||
|
).run(user.id);
|
||||||
|
|
||||||
|
const token = generateToken(user);
|
||||||
|
const userSafe = stripUserForClient(user) as Record<string, unknown>;
|
||||||
|
return {
|
||||||
|
token,
|
||||||
|
user: { ...userSafe, avatar_url: avatarUrl(user) },
|
||||||
|
auditUserId: Number(user.id),
|
||||||
|
auditAction: 'user.login',
|
||||||
|
auditDetails: { method: 'ldap', username },
|
||||||
|
};
|
||||||
|
}
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Session
|
// Session
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import { Client, InvalidCredentialsError } from 'ldapts';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
|
||||||
|
export interface LdapConfig {
|
||||||
|
url: string;
|
||||||
|
bindDn: string;
|
||||||
|
bindPassword: string;
|
||||||
|
searchBase: string;
|
||||||
|
searchFilter: string;
|
||||||
|
adminGroup: string;
|
||||||
|
allowGroup: string;
|
||||||
|
tlsCa?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LdapUser {
|
||||||
|
dn: string;
|
||||||
|
uid: string;
|
||||||
|
email: string;
|
||||||
|
displayName: string;
|
||||||
|
isAdmin: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLdapConfig(): LdapConfig | null {
|
||||||
|
if (!process.env.LDAP_URL) return null;
|
||||||
|
return {
|
||||||
|
url: process.env.LDAP_URL,
|
||||||
|
bindDn: process.env.LDAP_BIND_DN || '',
|
||||||
|
bindPassword: process.env.LDAP_BIND_PW || '',
|
||||||
|
searchBase: process.env.LDAP_BASE || '',
|
||||||
|
searchFilter: process.env.LDAP_FILTER || '(uid={{username}})',
|
||||||
|
adminGroup: process.env.LDAP_ADMIN_GROUP || '',
|
||||||
|
allowGroup: process.env.LDAP_ALLOWED_GROUP,
|
||||||
|
tlsCa: process.env.LDAP_TLS_CA,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ldapAuthenticate(
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
): Promise<LdapUser | null> {
|
||||||
|
const config = getLdapConfig();
|
||||||
|
if (!config) return null;
|
||||||
|
|
||||||
|
const tlsOptions = config.tlsCa
|
||||||
|
? { ca: [fs.readFileSync(config.tlsCa)] }
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const client = new Client({ url: config.url, tlsOptions });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.bind(config.bindDn, config.bindPassword);
|
||||||
|
|
||||||
|
const filter = config.searchFilter.replace('{{username}}', username);
|
||||||
|
const { searchEntries } = await client.search(config.searchBase, {
|
||||||
|
scope: 'sub',
|
||||||
|
filter,
|
||||||
|
attributes: ['uid', 'mail', 'cn', 'memberOf'],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!searchEntries.length) return null;
|
||||||
|
|
||||||
|
const entry = searchEntries[0];
|
||||||
|
const dn = entry.dn;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.bind(dn, password);
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof InvalidCredentialsError) return null;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
const raw = entry['memberOf'];
|
||||||
|
const groups: string[] = Array.isArray(raw)
|
||||||
|
? raw.map(String)
|
||||||
|
: raw ? [String(raw)] : [];
|
||||||
|
|
||||||
|
const isAdmin = config.adminGroup
|
||||||
|
? groups.some(g => g.toLowerCase() === config.adminGroup.toLowerCase())
|
||||||
|
: false;
|
||||||
|
|
||||||
|
// Access control: Only members of the allowed group are permitted to enter
|
||||||
|
// Exception: Admins are always allowed in
|
||||||
|
const allowedGroup = process.config.allowGroup;
|
||||||
|
if (allowedGroup && !isAdmin) {
|
||||||
|
const allowed = groups.some(g => g.toLowerCase() === allowedGroup.toLowerCase());
|
||||||
|
if (!allowed) return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
dn,
|
||||||
|
uid: String(entry['uid'] || username),
|
||||||
|
email: String(entry['mail'] || ''),
|
||||||
|
displayName: String(entry['cn'] || username),
|
||||||
|
isAdmin,
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
await client.unbind();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user