Files
pulse-crm-backend/src/repositories/user.ts
Flux_bot d0f1c242a3 feat(auth): Implementiere vollständiges Auth-System
- 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
2026-02-11 10:30:37 +00:00

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()`
);
}