Add claude init file + refactor some security issues

This commit is contained in:
2026-05-02 12:22:03 +02:00
parent dc65b49219
commit da85d1487c
8 changed files with 177 additions and 49 deletions
+4 -4
View File
@@ -329,9 +329,9 @@ usersRouter.post('/login', async (req: Request, res: Response) => {
}
// Create a session
const session: Session = await UserService.login(email, password, ip);
const session: Session | null = await UserService.login(email, password, ip);
if (!session.sessionId) {
if (!session || !session.sessionId) {
// Error logging in, probably wrong username / password
res.status(401).send(JSON.stringify({message: 'Wrong username and / or password', sessionId: -1, sessionKey: ''}));
return;
@@ -426,9 +426,9 @@ usersRouter.post('/checkSessionValid', async (req: Request, res: Response) => {
return;
}
const user: User = await UserService.checkSession(session_id, session_key, ip);
const user: User | null = await UserService.checkSession(session_id, session_key, ip);
if (!user.userId) {
if (!user || !user.userId) {
// Error logging in, probably wrong username / password
res.status(401).send(JSON.stringify({messages: ['Invalid session']}));
return;
+34 -25
View File
@@ -23,16 +23,19 @@ dotenv.config();
export const createUser = async (email: string, password: string, fullName: string, ip: string): Promise<Session> => {
let conn = await NachklangCalendarDB.getConnection();
try {
await conn.beginTransaction();
// Hash password and generate + hash session key
const pwHash = bcrypt.hashSync(password, 10);
const sessionKey = Guid.create().toString();
const sessionKeyHash = bcrypt.hashSync(sessionKey, 10);
const activationToken = Guid.create().toString();
const activationTokenHash = bcrypt.hashSync(activationToken, 10);
// Create user entry in SQL
const userQuery = 'INSERT INTO users (email, password_hash, full_name, activation_token) VALUES (?, ?, ?, ?) RETURNING user_id';
const userIdRes = await conn.query(userQuery, [email, pwHash, fullName, activationToken]);
const userIdRes = await conn.query(userQuery, [email, pwHash, fullName, activationTokenHash]);
// Get user id of the created user
let userId: number = -1;
@@ -40,9 +43,6 @@ export const createUser = async (email: string, password: string, fullName: stri
userId = row.user_id;
}
// Send email with activation link
await MailService.sendMail(email, 'Activate your Nachklang account', `Hi ${fullName},\n\nPlease click on the following link to activate your account:\n\nhttps://api.nachklang.art/calendar/users/activate?id=${userId}&token=${activationToken}`);
// Create session
const sessionQuery = 'INSERT INTO sessions (user_id, session_key_hash, created_date, valid_until, last_ip) VALUES (?,?,NOW(),DATE_ADD(NOW(), INTERVAL 30 DAY),?) RETURNING session_id';
const sessionIdRes = await conn.query(sessionQuery, [userId, sessionKeyHash, ip]);
@@ -54,6 +54,9 @@ export const createUser = async (email: string, password: string, fullName: stri
sessionId = row.session_id;
}
// Send email with activation link (after commit so we don't block on email delivery)
await MailService.sendMail(email, 'Activate your Nachklang account', `Hi ${fullName},\n\nPlease click on the following link to activate your account:\n\nhttps://api.nachklang.art/calendar/users/activate?id=${userId}&token=${activationToken}`);
return {
sessionId: sessionId,
userId: userId,
@@ -62,9 +65,9 @@ export const createUser = async (email: string, password: string, fullName: stri
lastIP: ip
};
} catch (err) {
await conn.rollback();
throw err;
} finally {
// Return connection
await conn.end();
}
};
@@ -72,22 +75,25 @@ export const createUser = async (email: string, password: string, fullName: stri
export const activateUser = async (userId: number, token: string): Promise<boolean> => {
let conn = await NachklangCalendarDB.getConnection();
try {
await conn.beginTransaction();
const checkTokenQuery = 'SELECT user_id, activation_token FROM users WHERE user_id = ? AND is_active = 0';
const userNameRes = await conn.query(checkTokenQuery, [userId]);
let storedToken = '';
let storedTokenHash = '';
for (const row of userNameRes) {
storedToken = row.activation_token;
storedTokenHash = row.activation_token;
}
if (storedToken!== token) {
if (!storedTokenHash || !bcrypt.compareSync(token, storedTokenHash)) {
return false;
}
const activateQuery = 'UPDATE users SET is_active = 1, activation_token = null WHERE user_id =?';
const activateQuery = 'UPDATE users SET is_active = 1, activation_token = null WHERE user_id = ?';
const activateRes = await conn.execute(activateQuery, [userId]);
await conn.commit();
return activateRes.affectedRows !== 0;
} catch (err) {
await conn.rollback();
throw err;
} finally {
// Return connection
await conn.end();
}
}
@@ -96,9 +102,11 @@ export const activateUser = async (userId: number, token: string): Promise<boole
* Checks if the given credentials are valid and creates a new session if they are.
* Returns the session information in case of a successful login
*/
export const login = async (email: string, password: string, ip: string): Promise<Session> => {
export const login = async (email: string, password: string, ip: string): Promise<Session | null> => {
let conn = await NachklangCalendarDB.getConnection();
try {
await conn.beginTransaction();
// Get saved password hash
const query = 'SELECT user_id, password_hash FROM users WHERE email = ?';
const userRows = await conn.query(query, email);
@@ -111,8 +119,7 @@ export const login = async (email: string, password: string, ip: string): Promis
// Check for correct password
if (!bcrypt.compareSync(password, savedHash)) {
// Wrong password, return invalid
return {} as Session;
return null;
}
// Generate + hash session key
@@ -138,9 +145,9 @@ export const login = async (email: string, password: string, ip: string): Promis
lastIP: ip
};
} catch (err) {
await conn.rollback();
throw err;
} finally {
// Return connection
await conn.end();
}
};
@@ -148,9 +155,11 @@ export const login = async (email: string, password: string, ip: string): Promis
/**
* Checks if the given session information are valid and returns the user information if they are
*/
export const checkSession = async (sessionId: string, sessionKey: string, ip: string): Promise<User> => {
export const checkSession = async (sessionId: string, sessionKey: string, ip: string): Promise<User | null> => {
let conn = await NachklangCalendarDB.getConnection();
try {
await conn.beginTransaction();
// Get saved session key hash
const query = 'SELECT user_id, session_key_hash, valid_until FROM sessions WHERE session_id = ?';
const sessionRows = await conn.query(query, sessionId);
@@ -165,30 +174,26 @@ export const checkSession = async (sessionId: string, sessionKey: string, ip: st
// Check for correct key
if (!bcrypt.compareSync(sessionKey, savedHash)) {
// Wrong key, return invalid
return {} as User;
return null;
}
// Check if the session is still valid
if (validUntil <= new Date()) {
// Session expired, return invalid
return {} as User;
return null;
}
// Update session entry in SQL
const updateSessionsQuery = 'UPDATE sessions SET last_IP = ? WHERE session_id = ?';
const userIdRes = await conn.query(updateSessionsQuery, [ip, sessionId]);
await conn.query(updateSessionsQuery, [ip, sessionId]);
await conn.commit();
// Get the other required user information
const userQuery = 'SELECT user_id, email, full_name, is_active FROM users WHERE user_id = ?';
const userRows = await conn.query(userQuery, userId);
let username = '';
let email = '';
let fullName = '';
let is_active = false;
for (const row of userRows) {
username = row.username;
email = row.email;
fullName = row.full_name;
is_active = row.is_active;
@@ -203,9 +208,9 @@ export const checkSession = async (sessionId: string, sessionKey: string, ip: st
isActive: is_active
};
} catch (err) {
await conn.rollback();
throw err;
} finally {
// Return connection
await conn.end();
}
};
@@ -213,6 +218,8 @@ export const checkSession = async (sessionId: string, sessionKey: string, ip: st
export const initiatePasswordReset = async (email: string): Promise<boolean> => {
let conn = await NachklangCalendarDB.getConnection();
try {
await conn.beginTransaction();
const checkUsernameQuery = 'SELECT user_id, full_Name FROM users WHERE email = ?';
const userNameRes = await conn.query(checkUsernameQuery, [email]);
if (userNameRes.length === 0) {
@@ -239,9 +246,9 @@ export const initiatePasswordReset = async (email: string): Promise<boolean> =>
await MailService.sendMail(email, 'Password Reset', `Hello ${fullName},\n\nYou requested a password reset for your BonkApp account. If you did not request this, please ignore this email.\n\nTo reset your password, please use the following reset token:\n\n${resetToken}`);
return true;
} catch (err) {
await conn.rollback();
throw err;
} finally {
// Return connection
await conn.end();
}
}
@@ -249,6 +256,8 @@ export const initiatePasswordReset = async (email: string): Promise<boolean> =>
export const finalizePasswordReset = async (email: string, token: string, newPassword: string): Promise<boolean> => {
let conn = await NachklangCalendarDB.getConnection();
try {
await conn.beginTransaction();
const checkTokenQuery = 'SELECT user_id, pw_reset_token_hash FROM users WHERE email = ?';
const userNameRes = await conn.query(checkTokenQuery, [email]);
if (userNameRes.length === 0) {
@@ -276,9 +285,9 @@ export const finalizePasswordReset = async (email: string, token: string, newPas
return false;
} catch (err) {
await conn.rollback();
throw err;
} finally {
// Return connection
await conn.end();
}
}