implement auth login and register with MongoDB
- Add AuthController login/register endpoints with error handling - Implement AuthService with password validation and user creation - Add MongoUserRepository with findByEmail and create methods - Implement password hashing with bcrypt - Add dotenv for environment variable support - Add Docker Compose setup for MongoDB + Mongo Express - Stub AuthMiddleware with fake user for testing - Update CLAUDE.md with implementation status
This commit is contained in:
38
CLAUDE.md
38
CLAUDE.md
@@ -198,9 +198,44 @@ The repository pattern allows swapping databases:
|
|||||||
- Multiple calendars
|
- Multiple calendars
|
||||||
- CalDAV synchronization with external services
|
- CalDAV synchronization with external services
|
||||||
|
|
||||||
|
## Development Environment
|
||||||
|
|
||||||
|
### MongoDB (Docker)
|
||||||
|
```bash
|
||||||
|
cd apps/server/docker/mongo
|
||||||
|
docker compose up -d # Start MongoDB + Mongo Express
|
||||||
|
docker compose down # Stop services
|
||||||
|
```
|
||||||
|
- MongoDB: `localhost:27017` (root/mongoose)
|
||||||
|
- Mongo Express UI: `localhost:8081` (admin/admin)
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
Server requires `.env` file in `apps/server/`:
|
||||||
|
```
|
||||||
|
JWT_SECRET=your-secret-key
|
||||||
|
JWT_EXPIRES_IN=1h
|
||||||
|
MONGODB_URI=mongodb://root:mongoose@localhost:27017/calchat?authSource=admin
|
||||||
|
```
|
||||||
|
|
||||||
## Current Implementation Status
|
## Current Implementation Status
|
||||||
|
|
||||||
**Backend:** Skeleton complete - all files exist with `throw new Error('Not implemented')` placeholders. Ready for step-by-step implementation.
|
**Backend:**
|
||||||
|
- **Implemented:**
|
||||||
|
- `AuthController`: login(), register() with error handling
|
||||||
|
- `AuthService`: login(), register() with password validation
|
||||||
|
- `MongoUserRepository`: findByEmail(), create()
|
||||||
|
- `utils/password`: hash(), compare() using bcrypt
|
||||||
|
- `utils/jwt`: signToken() (verifyToken() pending)
|
||||||
|
- `dotenv` integration for environment variables
|
||||||
|
- **Stubbed (TODO):**
|
||||||
|
- `AuthMiddleware.authenticate()`: Currently uses fake user for testing
|
||||||
|
- `AuthController`: refresh(), logout()
|
||||||
|
- `AuthService`: refreshToken()
|
||||||
|
- All Chat and Event functionality
|
||||||
|
- **Not started:**
|
||||||
|
- `ChatController`, `ChatService`, `MongoChatRepository`
|
||||||
|
- `EventController`, `EventService`, `MongoEventRepository`
|
||||||
|
- `ClaudeAdapter` (AI integration)
|
||||||
|
|
||||||
**Shared:** Types and DTOs defined and exported.
|
**Shared:** Types and DTOs defined and exported.
|
||||||
|
|
||||||
@@ -216,6 +251,7 @@ The repository pattern allows swapping databases:
|
|||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
Detailed architecture diagrams are in `docs/`:
|
Detailed architecture diagrams are in `docs/`:
|
||||||
|
- `api-routes.md` - API endpoint overview (German)
|
||||||
- `technisches_brainstorm.tex` - Technical concept document (German)
|
- `technisches_brainstorm.tex` - Technical concept document (German)
|
||||||
- `architecture-class-diagram.puml` - Backend class diagram
|
- `architecture-class-diagram.puml` - Backend class diagram
|
||||||
- `frontend-class-diagram.puml` - Frontend class diagram
|
- `frontend-class-diagram.puml` - Frontend class diagram
|
||||||
|
|||||||
1
apps/server/.gitignore
vendored
1
apps/server/.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
dist
|
dist
|
||||||
|
.env
|
||||||
|
|||||||
33
apps/server/docker/mongo/compose.yml
Normal file
33
apps/server/docker/mongo/compose.yml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
services:
|
||||||
|
mongo:
|
||||||
|
image: mongo:8
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "27017:27017"
|
||||||
|
environment:
|
||||||
|
MONGO_INITDB_ROOT_USERNAME: root
|
||||||
|
MONGO_INITDB_ROOT_PASSWORD: mongoose
|
||||||
|
volumes:
|
||||||
|
- mongo-data:/data/db
|
||||||
|
healthcheck:
|
||||||
|
test: mongosh --eval "db.adminCommand('ping')"
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
mongo-express:
|
||||||
|
image: mongo-express:latest
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "8081:8081"
|
||||||
|
environment:
|
||||||
|
ME_CONFIG_MONGODB_URL: mongodb://root:mongoose@mongo:27017/
|
||||||
|
ME_CONFIG_BASICAUTH_ENABLED: true
|
||||||
|
ME_CONFIG_BASICAUTH_USERNAME: admin
|
||||||
|
ME_CONFIG_BASICAUTH_PASSWORD: admin
|
||||||
|
depends_on:
|
||||||
|
mongo:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mongo-data:
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
"@anthropic-ai/sdk": "^0.71.2",
|
"@anthropic-ai/sdk": "^0.71.2",
|
||||||
"@caldav/shared": "*",
|
"@caldav/shared": "*",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
|
"dotenv": "^16.4.7",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"mongoose": "^9.1.1"
|
"mongoose": "^9.1.1"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import mongoose from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
|
import 'dotenv/config'
|
||||||
|
|
||||||
import { createRoutes } from './routes';
|
import { createRoutes } from './routes';
|
||||||
import { AuthController, ChatController, EventController } from './controllers';
|
import { AuthController, ChatController, EventController } from './controllers';
|
||||||
|
|||||||
@@ -5,11 +5,21 @@ export class AuthController {
|
|||||||
constructor(private authService: AuthService) {}
|
constructor(private authService: AuthService) {}
|
||||||
|
|
||||||
async login(req: Request, res: Response): Promise<void> {
|
async login(req: Request, res: Response): Promise<void> {
|
||||||
throw new Error('Not implemented');
|
try {
|
||||||
|
const result = await this.authService.login(req.body);
|
||||||
|
res.json(result);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(401).json({ error: (error as Error).message });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async register(req: Request, res: Response): Promise<void> {
|
async register(req: Request, res: Response): Promise<void> {
|
||||||
throw new Error('Not implemented');
|
try {
|
||||||
|
const result = await this.authService.register(req.body);
|
||||||
|
res.status(201).json(result);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(400).json({ error: (error as Error).message });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async refresh(req: Request, res: Response): Promise<void> {
|
async refresh(req: Request, res: Response): Promise<void> {
|
||||||
|
|||||||
@@ -6,5 +6,11 @@ export interface AuthenticatedRequest extends Request {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function authenticate(req: AuthenticatedRequest, res: Response, next: NextFunction): void {
|
export function authenticate(req: AuthenticatedRequest, res: Response, next: NextFunction): void {
|
||||||
throw new Error('Not implemented');
|
// TODO: Implement real JWT verification
|
||||||
|
// Fake user for testing purposes
|
||||||
|
req.user = {
|
||||||
|
userId: 'fake-user-id',
|
||||||
|
email: 'test@example.com',
|
||||||
|
};
|
||||||
|
next();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,12 @@ export class MongoUserRepository implements UserRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async findByEmail(email: string): Promise<User | null> {
|
async findByEmail(email: string): Promise<User | null> {
|
||||||
throw new Error('Not implemented');
|
const user = await UserModel.findOne({ email: email.toLowerCase() });
|
||||||
|
return user ? user.toJSON() as User : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(data: CreateUserData): Promise<User> {
|
async create(data: CreateUserData): Promise<User> {
|
||||||
throw new Error('Not implemented');
|
const user = await UserModel.create(data);
|
||||||
|
return user.toJSON() as User;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,33 @@ export class AuthService {
|
|||||||
constructor(private userRepo: UserRepository) {}
|
constructor(private userRepo: UserRepository) {}
|
||||||
|
|
||||||
async login(data: LoginDTO): Promise<AuthResponse> {
|
async login(data: LoginDTO): Promise<AuthResponse> {
|
||||||
throw new Error('Not implemented');
|
const user = await this.userRepo.findByEmail(data.email);
|
||||||
|
if (!user || !user.passwordHash) {
|
||||||
|
throw new Error('Invalid credentials');
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValid = await password.compare(data.password, user.passwordHash);
|
||||||
|
if (!isValid) {
|
||||||
|
throw new Error('Invalid credentials');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { user, accessToken: '' };
|
||||||
}
|
}
|
||||||
|
|
||||||
async register(data: CreateUserDTO): Promise<AuthResponse> {
|
async register(data: CreateUserDTO): Promise<AuthResponse> {
|
||||||
throw new Error('Not implemented');
|
const existingUser = await this.userRepo.findByEmail(data.email);
|
||||||
|
if (existingUser) {
|
||||||
|
throw new Error('Email already exists');
|
||||||
|
}
|
||||||
|
|
||||||
|
const passwordHash = await password.hash(data.password);
|
||||||
|
const user = await this.userRepo.create({
|
||||||
|
email: data.email,
|
||||||
|
displayName: data.displayName,
|
||||||
|
passwordHash,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { user, accessToken: '' };
|
||||||
}
|
}
|
||||||
|
|
||||||
async refreshToken(refreshToken: string): Promise<AuthResponse> {
|
async refreshToken(refreshToken: string): Promise<AuthResponse> {
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import bcrypt from 'bcrypt';
|
|||||||
const SALT_ROUNDS = 10;
|
const SALT_ROUNDS = 10;
|
||||||
|
|
||||||
export async function hash(password: string): Promise<string> {
|
export async function hash(password: string): Promise<string> {
|
||||||
throw new Error('Not implemented');
|
return bcrypt.hash(password, SALT_ROUNDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function compare(password: string, hash: string): Promise<boolean> {
|
export async function compare(password: string, hash: string): Promise<boolean> {
|
||||||
throw new Error('Not implemented');
|
return bcrypt.compare(password, hash);
|
||||||
}
|
}
|
||||||
|
|||||||
54
docs/api-routes.md
Normal file
54
docs/api-routes.md
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# CalChat API Routes
|
||||||
|
|
||||||
|
Base URL: `/api`
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
### Auth Endpoints (`/api/auth`)
|
||||||
|
Öffentliche Endpoints - keine Authentifizierung erforderlich.
|
||||||
|
|
||||||
|
| Method | Endpoint | Beschreibung |
|
||||||
|
|--------|----------|--------------|
|
||||||
|
| POST | `/auth/login` | User Login |
|
||||||
|
| POST | `/auth/register` | User Registrierung |
|
||||||
|
| POST | `/auth/refresh` | JWT Token erneuern |
|
||||||
|
| POST | `/auth/logout` | User Logout |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Events
|
||||||
|
|
||||||
|
### Event Endpoints (`/api/events`)
|
||||||
|
Alle Endpoints erfordern JWT-Authentifizierung.
|
||||||
|
|
||||||
|
| Method | Endpoint | Beschreibung |
|
||||||
|
|--------|----------|--------------|
|
||||||
|
| GET | `/events` | Alle Events des Users abrufen |
|
||||||
|
| GET | `/events/range` | Events nach Zeitraum filtern |
|
||||||
|
| GET | `/events/:id` | Einzelnes Event abrufen |
|
||||||
|
| POST | `/events` | Neues Event erstellen |
|
||||||
|
| PUT | `/events/:id` | Event aktualisieren |
|
||||||
|
| DELETE | `/events/:id` | Event löschen |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Chat
|
||||||
|
|
||||||
|
### Chat Endpoints (`/api/chat`)
|
||||||
|
Alle Endpoints erfordern JWT-Authentifizierung.
|
||||||
|
|
||||||
|
| Method | Endpoint | Beschreibung |
|
||||||
|
|--------|----------|--------------|
|
||||||
|
| POST | `/chat/message` | Nachricht an AI senden |
|
||||||
|
| POST | `/chat/confirm/:conversationId/:messageId` | Vorgeschlagenes Event bestätigen |
|
||||||
|
| POST | `/chat/reject/:conversationId/:messageId` | Vorgeschlagenes Event ablehnen |
|
||||||
|
| GET | `/chat/conversations` | Alle Konversationen abrufen |
|
||||||
|
| GET | `/chat/conversations/:id` | Nachrichten einer Konversation (mit Pagination) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Health
|
||||||
|
|
||||||
|
| Method | Endpoint | Beschreibung |
|
||||||
|
|--------|----------|--------------|
|
||||||
|
| GET | `/health` | Health Check |
|
||||||
1
package-lock.json
generated
1
package-lock.json
generated
@@ -64,6 +64,7 @@
|
|||||||
"@anthropic-ai/sdk": "^0.71.2",
|
"@anthropic-ai/sdk": "^0.71.2",
|
||||||
"@caldav/shared": "*",
|
"@caldav/shared": "*",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
|
"dotenv": "^16.4.7",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"mongoose": "^9.1.1"
|
"mongoose": "^9.1.1"
|
||||||
|
|||||||
Reference in New Issue
Block a user