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:
150
middlelayer/auth/README.md
Normal file
150
middlelayer/auth/README.md
Normal 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
|
||||
|
||||
59
middlelayer/auth/authorization.ts
Normal file
59
middlelayer/auth/authorization.ts
Normal 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
63
middlelayer/auth/jwt.ts
Normal 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;
|
||||
}
|
||||
31
middlelayer/auth/password.ts
Normal file
31
middlelayer/auth/password.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
140
middlelayer/auth/userService.ts
Normal file
140
middlelayer/auth/userService.ts
Normal 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();
|
||||
Reference in New Issue
Block a user