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
This commit is contained in:
83
src/repositories/organization.ts
Normal file
83
src/repositories/organization.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { query, queryOne } from "../db/connection.ts";
|
||||
import type { Organization } from "../types/index.ts";
|
||||
|
||||
// ============================================
|
||||
// ORGANIZATION REPOSITORY
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Find organization by ID
|
||||
*/
|
||||
export async function findById(id: string): Promise<Organization | null> {
|
||||
return await queryOne<Organization>(
|
||||
`SELECT * FROM organizations WHERE id = $1 AND deleted_at IS NULL`,
|
||||
[id]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find organization by slug
|
||||
*/
|
||||
export async function findBySlug(slug: string): Promise<Organization | null> {
|
||||
return await queryOne<Organization>(
|
||||
`SELECT * FROM organizations WHERE slug = $1 AND deleted_at IS NULL`,
|
||||
[slug]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user count for organization
|
||||
*/
|
||||
export async function getUserCount(orgId: string): Promise<number> {
|
||||
const result = await queryOne<{ count: string }>(
|
||||
`SELECT COUNT(*) as count FROM users WHERE org_id = $1 AND deleted_at IS NULL`,
|
||||
[orgId]
|
||||
);
|
||||
return parseInt(result?.count || "0");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if organization can add more users
|
||||
*/
|
||||
export async function canAddUser(orgId: string): Promise<boolean> {
|
||||
const result = await queryOne<{ can_add: boolean }>(
|
||||
`SELECT (
|
||||
SELECT COUNT(*) FROM users WHERE org_id = $1 AND deleted_at IS NULL
|
||||
) < o.max_users as can_add
|
||||
FROM organizations o
|
||||
WHERE o.id = $1`,
|
||||
[orgId]
|
||||
);
|
||||
return result?.can_add ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all users in organization
|
||||
*/
|
||||
export async function getUsers(orgId: string) {
|
||||
return await query(
|
||||
`SELECT id, email, first_name, last_name, role, is_verified,
|
||||
is_active, last_login_at, created_at
|
||||
FROM users
|
||||
WHERE org_id = $1 AND deleted_at IS NULL
|
||||
ORDER BY created_at DESC`,
|
||||
[orgId]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update organization settings
|
||||
*/
|
||||
export async function updateSettings(
|
||||
orgId: string,
|
||||
settings: Record<string, unknown>
|
||||
): Promise<Organization | null> {
|
||||
const rows = await query<Organization>(
|
||||
`UPDATE organizations
|
||||
SET settings = settings || $1::jsonb, updated_at = NOW()
|
||||
WHERE id = $2
|
||||
RETURNING *`,
|
||||
[JSON.stringify(settings), orgId]
|
||||
);
|
||||
return rows[0] || null;
|
||||
}
|
||||
246
src/repositories/user.ts
Normal file
246
src/repositories/user.ts
Normal file
@@ -0,0 +1,246 @@
|
||||
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()`
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user