- JWT Access + Refresh Tokens mit djwt - Argon2 Password Hashing (OWASP konfig) - Rate Limiting für Auth-Endpoints - Rollen-basierte Zugriffskontrolle (owner, admin, manager, user) - DSGVO Audit Logging - Email-Verifizierung (Struktur) - Passwort-Reset Flow - Multi-Device Logout Neue Dateien: - src/types/index.ts - TypeScript Interfaces - src/db/connection.ts - PostgreSQL Pool - src/services/password.ts - Argon2 Hashing - src/services/jwt.ts - Token Generation - src/services/audit.ts - DSGVO Audit Log - src/middleware/auth.ts - Auth Middleware - src/repositories/user.ts - User DB Queries - src/repositories/organization.ts - Org DB Queries - src/utils/response.ts - API Response Helpers Task: #8 Authentifizierung & Benutzerverwaltung
247 lines
5.8 KiB
TypeScript
247 lines
5.8 KiB
TypeScript
import { query, queryOne, execute, transaction } from "../db/connection.ts";
|
|
import type { User, Organization, RefreshTokenRecord } from "../types/index.ts";
|
|
|
|
// ============================================
|
|
// USER REPOSITORY
|
|
// ============================================
|
|
|
|
/**
|
|
* Find user by email
|
|
*/
|
|
export async function findByEmail(email: string): Promise<User | null> {
|
|
return await queryOne<User>(
|
|
`SELECT * FROM users WHERE email = $1 AND deleted_at IS NULL`,
|
|
[email.toLowerCase()]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Find user by ID
|
|
*/
|
|
export async function findById(id: string): Promise<User | null> {
|
|
return await queryOne<User>(
|
|
`SELECT * FROM users WHERE id = $1 AND deleted_at IS NULL`,
|
|
[id]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Find user by verification token
|
|
*/
|
|
export async function findByVerificationToken(token: string): Promise<User | null> {
|
|
return await queryOne<User>(
|
|
`SELECT * FROM users WHERE verification_token = $1 AND deleted_at IS NULL`,
|
|
[token]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Find user by password reset token
|
|
*/
|
|
export async function findByResetToken(token: string): Promise<User | null> {
|
|
return await queryOne<User>(
|
|
`SELECT * FROM users
|
|
WHERE reset_token = $1
|
|
AND reset_token_expires > NOW()
|
|
AND deleted_at IS NULL`,
|
|
[token]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Create a new user with organization
|
|
*/
|
|
export async function createUserWithOrg(data: {
|
|
email: string;
|
|
passwordHash: string;
|
|
firstName: string;
|
|
lastName: string;
|
|
orgName: string;
|
|
verificationToken: string;
|
|
}): Promise<{ user: User; organization: Organization }> {
|
|
return await transaction(async (conn) => {
|
|
// Generate slug from org name
|
|
const slug = data.orgName
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, "-")
|
|
.replace(/^-|-$/g, "")
|
|
.substring(0, 50);
|
|
|
|
// Create organization
|
|
const orgResult = await conn.queryObject<Organization>(
|
|
`INSERT INTO organizations (name, slug, plan, max_users, settings)
|
|
VALUES ($1, $2, 'free', 3, '{}')
|
|
RETURNING *`,
|
|
[data.orgName, slug + "-" + Date.now().toString(36)]
|
|
);
|
|
const organization = orgResult.rows[0];
|
|
|
|
// Create user as owner
|
|
const userResult = await conn.queryObject<User>(
|
|
`INSERT INTO users (
|
|
org_id, email, password_hash, first_name, last_name,
|
|
role, is_verified, verification_token
|
|
)
|
|
VALUES ($1, $2, $3, $4, $5, 'owner', false, $6)
|
|
RETURNING *`,
|
|
[
|
|
organization.id,
|
|
data.email.toLowerCase(),
|
|
data.passwordHash,
|
|
data.firstName,
|
|
data.lastName,
|
|
data.verificationToken,
|
|
]
|
|
);
|
|
const user = userResult.rows[0];
|
|
|
|
return { user, organization };
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create a user in an existing organization (invited user)
|
|
*/
|
|
export async function createUser(data: {
|
|
orgId: string;
|
|
email: string;
|
|
passwordHash: string;
|
|
firstName: string;
|
|
lastName: string;
|
|
role: "admin" | "manager" | "user";
|
|
isVerified?: boolean;
|
|
}): Promise<User> {
|
|
const rows = await query<User>(
|
|
`INSERT INTO users (
|
|
org_id, email, password_hash, first_name, last_name,
|
|
role, is_verified
|
|
)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
RETURNING *`,
|
|
[
|
|
data.orgId,
|
|
data.email.toLowerCase(),
|
|
data.passwordHash,
|
|
data.firstName,
|
|
data.lastName,
|
|
data.role,
|
|
data.isVerified ?? false,
|
|
]
|
|
);
|
|
return rows[0];
|
|
}
|
|
|
|
/**
|
|
* Update user's email verification status
|
|
*/
|
|
export async function verifyEmail(userId: string): Promise<void> {
|
|
await execute(
|
|
`UPDATE users
|
|
SET is_verified = true, verification_token = NULL, updated_at = NOW()
|
|
WHERE id = $1`,
|
|
[userId]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Set password reset token
|
|
*/
|
|
export async function setResetToken(
|
|
userId: string,
|
|
token: string,
|
|
expiresAt: Date
|
|
): Promise<void> {
|
|
await execute(
|
|
`UPDATE users
|
|
SET reset_token = $1, reset_token_expires = $2, updated_at = NOW()
|
|
WHERE id = $3`,
|
|
[token, expiresAt, userId]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Update password and clear reset token
|
|
*/
|
|
export async function updatePassword(
|
|
userId: string,
|
|
passwordHash: string
|
|
): Promise<void> {
|
|
await execute(
|
|
`UPDATE users
|
|
SET password_hash = $1, reset_token = NULL, reset_token_expires = NULL, updated_at = NOW()
|
|
WHERE id = $2`,
|
|
[passwordHash, userId]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Update last login timestamp
|
|
*/
|
|
export async function updateLastLogin(userId: string): Promise<void> {
|
|
await execute(
|
|
`UPDATE users SET last_login_at = NOW() WHERE id = $1`,
|
|
[userId]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Store refresh token hash
|
|
*/
|
|
export async function storeRefreshToken(
|
|
userId: string,
|
|
tokenHash: string,
|
|
expiresAt: Date
|
|
): Promise<void> {
|
|
await execute(
|
|
`INSERT INTO refresh_tokens (user_id, token_hash, expires_at)
|
|
VALUES ($1, $2, $3)`,
|
|
[userId, tokenHash, expiresAt]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Find refresh token by hash
|
|
*/
|
|
export async function findRefreshToken(tokenHash: string): Promise<RefreshTokenRecord | null> {
|
|
return await queryOne<RefreshTokenRecord>(
|
|
`SELECT * FROM refresh_tokens
|
|
WHERE token_hash = $1
|
|
AND revoked = false
|
|
AND expires_at > NOW()`,
|
|
[tokenHash]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Revoke a refresh token
|
|
*/
|
|
export async function revokeRefreshToken(tokenHash: string): Promise<void> {
|
|
await execute(
|
|
`UPDATE refresh_tokens
|
|
SET revoked = true, revoked_at = NOW()
|
|
WHERE token_hash = $1`,
|
|
[tokenHash]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Revoke all refresh tokens for a user (logout everywhere)
|
|
*/
|
|
export async function revokeAllUserTokens(userId: string): Promise<void> {
|
|
await execute(
|
|
`UPDATE refresh_tokens
|
|
SET revoked = true, revoked_at = NOW()
|
|
WHERE user_id = $1 AND revoked = false`,
|
|
[userId]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Clean up expired refresh tokens
|
|
*/
|
|
export async function cleanupExpiredTokens(): Promise<number> {
|
|
return await execute(
|
|
`DELETE FROM refresh_tokens WHERE expires_at < NOW()`
|
|
);
|
|
}
|