project setup with core files including configuration, package management, and basic structure. Added .gitignore, README, and various TypeScript types for CMS components. Implemented initial components and layouts for the application.

This commit is contained in:
Peter Meier
2025-12-13 23:26:13 +01:00
parent ea288a5bbc
commit b1a556dc6d
167 changed files with 19057 additions and 131 deletions

150
middlelayer/auth/README.md Normal file
View File

@@ -0,0 +1,150 @@
# Authentication & Authorization
## Übersicht
Der Middlelayer unterstützt JWT-basierte Authentication und Role-Based Access Control (RBAC).
## Features
- ✅ JWT-basierte Authentication
- ✅ Passwort-Hashing mit bcrypt
- ✅ Role-Based Access Control (Admin, Customer, Guest)
- ✅ Protected Resolvers
- ✅ User-Context in GraphQL Requests
## User-Rollen
- **ADMIN**: Vollzugriff auf alle Ressourcen
- **CUSTOMER**: Zugriff auf Kunden-spezifische Ressourcen
- **GUEST**: Nur öffentliche Ressourcen
## GraphQL Mutations
### Register
```graphql
mutation Register {
register(email: "user@example.com", password: "secure123", name: "Max Mustermann") {
user {
id
email
name
role
}
token
}
}
```
### Login
```graphql
mutation Login {
login(email: "user@example.com", password: "secure123") {
user {
id
email
name
role
}
token
}
}
```
## GraphQL Queries
### Aktueller User
```graphql
query Me {
me {
id
email
name
role
}
}
```
## Authorization in Resolvers
### Beispiel: Protected Resolver
```typescript
import { requireAuth, requireAdmin } from "./auth/authorization.js";
export const resolvers = {
Query: {
adminOnlyData: async (_: unknown, __: unknown, context: GraphQLContext) => {
// Prüft ob User Admin ist
requireAdmin(context.user);
// Resolver-Logik...
},
},
};
```
### Verfügbare Authorization-Helper
- `requireAuth(user)` - Prüft ob User authentifiziert ist
- `requireRole(user, roles)` - Prüft ob User eine bestimmte Rolle hat
- `requireAdmin(user)` - Prüft ob User Admin ist
- `requireCustomer(user)` - Prüft ob User Customer oder Admin ist
## Verwendung im Frontend
### Token speichern
```typescript
// Nach Login/Register
const { token } = await login(email, password);
localStorage.setItem('authToken', token);
```
### Token in Requests verwenden
```typescript
const response = await fetch('http://localhost:4000', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('authToken')}`,
},
body: JSON.stringify({ query, variables }),
});
```
## Konfiguration
### Environment Variables
```bash
JWT_SECRET=your-secret-key-change-in-production
JWT_EXPIRES_IN=7d # Token-Gültigkeitsdauer
```
**Wichtig:** In Production muss `JWT_SECRET` sicher gesetzt werden!
## Security Best Practices
1. **JWT Secret**: Verwende einen starken, zufälligen Secret
2. **HTTPS**: Immer HTTPS in Production verwenden
3. **Token Expiration**: Setze angemessene Expiration-Zeiten
4. **Password Hashing**: Passwörter werden automatisch mit bcrypt gehasht
5. **Rate Limiting**: (Noch zu implementieren) Verhindere Brute-Force-Angriffe
## Mock User Store
Aktuell werden User in einem In-Memory Store gespeichert. Für Production sollte dies durch eine Datenbank ersetzt werden.
## Nächste Schritte
- [ ] Database-Integration für User-Speicherung
- [ ] Refresh Tokens
- [ ] Password Reset
- [ ] Email Verification
- [ ] Rate Limiting für Login/Register
- [ ] Session Management

View File

@@ -0,0 +1,59 @@
import { GraphQLError } from "graphql";
import type { User, UserRole } from "../types/user.js";
import { UserRole as UserRoleEnum } from "../types/user.js";
/**
* Authorization-Fehler
*/
export class AuthorizationError extends GraphQLError {
constructor(message: string) {
super(message, {
extensions: {
code: "UNAUTHORIZED",
},
});
}
}
/**
* Prüft ob User authentifiziert ist
*/
export function requireAuth(user: User | null): User {
if (!user) {
throw new AuthorizationError("Authentifizierung erforderlich");
}
return user;
}
/**
* Prüft ob User eine bestimmte Rolle hat
*/
export function requireRole(
user: User | null,
requiredRoles: UserRole[]
): User {
const authenticatedUser = requireAuth(user);
if (!requiredRoles.includes(authenticatedUser.role)) {
throw new AuthorizationError(
`Zugriff verweigert. Erforderliche Rollen: ${requiredRoles.join(", ")}`
);
}
return authenticatedUser;
}
/**
* Prüft ob User Admin ist
*/
export function requireAdmin(user: User | null): User {
return requireRole(user, [UserRoleEnum.ADMIN]);
}
/**
* Prüft ob User Customer oder Admin ist
*/
export function requireCustomer(user: User | null): User {
return requireRole(user, [UserRoleEnum.CUSTOMER, UserRoleEnum.ADMIN]);
}

63
middlelayer/auth/jwt.ts Normal file
View File

@@ -0,0 +1,63 @@
import jwt from "jsonwebtoken";
import type { JWTPayload, UserRole } from "../types/user.js";
import { logger } from "../monitoring/logger.js";
const JWT_SECRET =
process.env.JWT_SECRET || "your-secret-key-change-in-production";
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || "7d";
/**
* Erstellt ein JWT Token für einen User
*/
export function createToken(payload: JWTPayload): string {
return jwt.sign(payload, JWT_SECRET, {
expiresIn: JWT_EXPIRES_IN,
});
}
/**
* Verifiziert ein JWT Token
*/
export function verifyToken(token: string): JWTPayload | null {
try {
const decoded = jwt.verify(token, JWT_SECRET) as JWTPayload;
return decoded;
} catch (error) {
logger.warn("JWT verification failed", { error });
return null;
}
}
/**
* Extrahiert Token aus Authorization Header
*/
export function extractTokenFromHeader(
authHeader: string | null
): string | null {
if (!authHeader) return null;
// Format: "Bearer <token>"
const parts = authHeader.split(" ");
if (parts.length !== 2 || parts[0] !== "Bearer") {
return null;
}
return parts[1];
}
/**
* Prüft ob User eine bestimmte Rolle hat
*/
export function hasRole(
userRole: UserRole,
requiredRoles: UserRole[]
): boolean {
return requiredRoles.includes(userRole);
}
/**
* Prüft ob User Admin ist
*/
export function isAdmin(userRole: UserRole): boolean {
return userRole === UserRole.ADMIN;
}

View File

@@ -0,0 +1,31 @@
import bcrypt from "bcryptjs";
import { logger } from "../monitoring/logger.js";
const SALT_ROUNDS = 10;
/**
* Hasht ein Passwort
*/
export async function hashPassword(password: string): Promise<string> {
try {
return await bcrypt.hash(password, SALT_ROUNDS);
} catch (error) {
logger.error("Password hashing failed", { error });
throw new Error("Fehler beim Hashen des Passworts");
}
}
/**
* Vergleicht ein Passwort mit einem Hash
*/
export async function comparePassword(
password: string,
hash: string
): Promise<boolean> {
try {
return await bcrypt.compare(password, hash);
} catch (error) {
logger.error("Password comparison failed", { error });
return false;
}
}

View File

@@ -0,0 +1,140 @@
import type {
User,
UserRole,
LoginCredentials,
RegisterData,
} from "../types/user.js";
import { hashPassword, comparePassword } from "./password.js";
import { createToken, verifyToken } from "./jwt.js";
import { logger } from "../monitoring/logger.js";
/**
* Mock User Store (später durch Datenbank ersetzen)
*/
const users = new Map<string, User & { passwordHash: string }>();
/**
* User Service für Authentication
*/
export class UserService {
/**
* Registriert einen neuen User
*/
async register(
data: RegisterData,
role: UserRole = "customer"
): Promise<{
user: User;
token: string;
}> {
// Prüfe ob User bereits existiert
const existingUser = Array.from(users.values()).find(
(u) => u.email === data.email
);
if (existingUser) {
throw new Error("User mit dieser E-Mail existiert bereits");
}
// Hashe Passwort
const passwordHash = await hashPassword(data.password);
// Erstelle User
const user: User = {
id: `user-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
email: data.email,
name: data.name,
role,
createdAt: new Date(),
};
// Speichere User
users.set(user.id, { ...user, passwordHash });
// Erstelle Token
const token = createToken({
userId: user.id,
email: user.email,
role: user.role,
});
logger.info("User registered", { userId: user.id, email: user.email });
return { user, token };
}
/**
* Login eines Users
*/
async login(credentials: LoginCredentials): Promise<{
user: User;
token: string;
}> {
// Finde User
const userEntry = Array.from(users.values()).find(
(u) => u.email === credentials.email
);
if (!userEntry) {
throw new Error("Ungültige E-Mail oder Passwort");
}
// Vergleiche Passwort
const isValid = await comparePassword(
credentials.password,
userEntry.passwordHash
);
if (!isValid) {
throw new Error("Ungültige E-Mail oder Passwort");
}
// Erstelle User-Objekt ohne Passwort
const user: User = {
id: userEntry.id,
email: userEntry.email,
name: userEntry.name,
role: userEntry.role,
createdAt: userEntry.createdAt,
};
// Erstelle Token
const token = createToken({
userId: user.id,
email: user.email,
role: user.role,
});
logger.info("User logged in", { userId: user.id, email: user.email });
return { user, token };
}
/**
* Holt User anhand der ID
*/
async getUserById(userId: string): Promise<User | null> {
const userEntry = users.get(userId);
if (!userEntry) return null;
return {
id: userEntry.id,
email: userEntry.email,
name: userEntry.name,
role: userEntry.role,
createdAt: userEntry.createdAt,
};
}
/**
* Holt User anhand des Tokens
*/
async getUserFromToken(token: string): Promise<User | null> {
const payload = verifyToken(token);
if (!payload) return null;
return this.getUserById(payload.userId);
}
}
// Singleton-Instanz
export const userService = new UserService();