kybernus 2.0.10 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/templates/java-spring/clean/src/main/java/{{packagePath}}/infrastructure/persistence/PostgresUserRepository.java.hbs +40 -0
- package/templates/java-spring/clean/src/main/resources/application.properties.hbs +18 -0
- package/templates/java-spring/hexagonal/src/main/java/{{packagePath}}/{infrastructure/web/controller → adapters/inbound/web}/AuthController.java.hbs +4 -5
- package/templates/java-spring/hexagonal/src/main/java/{{packagePath}}/adapters/outbound/persistence/JpaUserAdapter.java.hbs +40 -0
- package/templates/java-spring/hexagonal/src/main/java/{{packagePath}}/adapters/outbound/persistence/entity/UserEntity.java.hbs +61 -0
- package/templates/java-spring/hexagonal/src/main/java/{{packagePath}}/adapters/outbound/persistence/repository/JpaUserRepository.java.hbs +11 -0
- package/templates/java-spring/hexagonal/src/main/java/{{packagePath}}/{infrastructure/security/SecurityAdapters.java.hbs → adapters/outbound/security/SecurityAdapter.java.hbs} +14 -14
- package/templates/java-spring/hexagonal/src/main/java/{{packagePath}}/{domain/entity → core/domain}/User.java.hbs +2 -2
- package/templates/java-spring/hexagonal/src/main/java/{{packagePath}}/{domain/usecase → core/ports/inbound}/LoginUserUseCase.java.hbs +8 -8
- package/templates/java-spring/hexagonal/src/main/java/{{packagePath}}/{domain/usecase → core/ports/inbound}/RegisterUserUseCase.java.hbs +7 -8
- package/templates/java-spring/hexagonal/src/main/java/{{packagePath}}/{domain/repository → core/ports/outbound}/UserRepository.java.hbs +4 -4
- package/templates/java-spring/hexagonal/src/main/java/{{packagePath}}/{application → core}/service/AuthService.java.hbs +9 -9
- package/templates/java-spring/hexagonal/src/main/java/{{packagePath}}/{{projectNamePascalCase}}Application.java.hbs +2 -2
- package/templates/java-spring/hexagonal/src/main/resources/application.properties.hbs +18 -0
- package/templates/nestjs/clean/package.json.hbs +9 -3
- package/templates/nestjs/clean/prisma/schema.prisma.hbs +20 -0
- package/templates/nestjs/clean/src/app.module.ts.hbs +17 -0
- package/templates/nestjs/clean/src/auth.module.ts.hbs +12 -10
- package/templates/nestjs/clean/src/infrastructure/database/prisma.service.ts.hbs +13 -0
- package/templates/nestjs/clean/src/infrastructure/database/repositories/prisma.user.repository.ts.hbs +32 -0
- package/templates/nestjs/clean/src/main.ts.hbs +11 -0
- package/templates/nestjs/hexagonal/package.json.hbs +9 -3
- package/templates/nestjs/hexagonal/prisma/schema.prisma +20 -0
- package/templates/nestjs/hexagonal/src/adapters/outbound/persistence/prisma.service.ts.hbs +13 -0
- package/templates/nestjs/hexagonal/src/adapters/outbound/persistence/prisma.user.adapter.ts.hbs +32 -0
- package/templates/nestjs/hexagonal/src/app.module.ts.hbs +17 -0
- package/templates/nestjs/hexagonal/src/auth.module.ts.hbs +15 -13
- package/templates/nestjs/hexagonal/src/main.ts.hbs +11 -0
- package/templates/nextjs/mvc/package.json.hbs +35 -32
- package/templates/nextjs/mvc/prisma/schema.prisma.hbs +12 -9
- package/templates/nextjs/mvc/src/lib/db.ts +15 -0
- package/templates/nodejs-express/clean/docker-compose.yml.hbs +5 -6
- package/templates/nodejs-express/clean/package.json.hbs +14 -8
- package/templates/nodejs-express/clean/prisma/schema.prisma +20 -0
- package/templates/nodejs-express/clean/src/config/index.ts +27 -0
- package/templates/nodejs-express/clean/src/index.ts.hbs +20 -24
- package/templates/nodejs-express/clean/src/infrastructure/database/PrismaUserRepository.ts.hbs +61 -0
- package/templates/nodejs-express/clean/src/infrastructure/database/prisma.ts.hbs +5 -0
- package/templates/nodejs-express/clean/src/infrastructure/http/controllers/AuthController.ts.hbs +24 -40
- package/templates/nodejs-express/clean/src/infrastructure/http/middlewares/errorHandler.ts +24 -0
- package/templates/nodejs-express/clean/tsconfig.json.hbs +8 -17
- package/templates/nodejs-express/hexagonal/docker-compose.yml.hbs +5 -6
- package/templates/nodejs-express/hexagonal/package.json.hbs +14 -8
- package/templates/nodejs-express/hexagonal/prisma/schema.prisma +20 -0
- package/templates/nodejs-express/hexagonal/src/adapters/inbound/http/AuthController.ts.hbs +29 -44
- package/templates/nodejs-express/hexagonal/src/adapters/inbound/http/middlewares/errorHandler.ts +24 -0
- package/templates/nodejs-express/hexagonal/src/adapters/outbound/persistence/PrismaUserAdapter.ts.hbs +61 -0
- package/templates/nodejs-express/hexagonal/src/adapters/outbound/persistence/prisma.ts +5 -0
- package/templates/nodejs-express/hexagonal/src/config/index.ts +27 -0
- package/templates/nodejs-express/hexagonal/src/index.ts.hbs +24 -27
- package/templates/nodejs-express/hexagonal/tsconfig.json.hbs +8 -17
- package/templates/python-fastapi/clean/app/application/services/__init__.py +0 -0
- package/templates/python-fastapi/clean/app/application/services/user_service.py.hbs +20 -0
- package/templates/python-fastapi/clean/app/config.py.hbs +24 -0
- package/templates/python-fastapi/clean/app/infrastructure/database/models.py.hbs +24 -0
- package/templates/python-fastapi/clean/app/infrastructure/database/postgres_repository.py.hbs +62 -0
- package/templates/python-fastapi/clean/app/infrastructure/database/session.py.hbs +27 -0
- package/templates/python-fastapi/clean/app/infrastructure/http/auth_controller.py.hbs +14 -8
- package/templates/python-fastapi/clean/app/main.py.hbs +25 -3
- package/templates/python-fastapi/clean/requirements.txt.hbs +3 -1
- package/templates/python-fastapi/hexagonal/app/adapters/inbound/http_adapter.py.hbs +41 -17
- package/templates/python-fastapi/hexagonal/app/adapters/outbound/postgres_user_repository.py.hbs +50 -0
- package/templates/python-fastapi/hexagonal/app/config.py.hbs +20 -0
- package/templates/python-fastapi/hexagonal/app/infrastructure/database/models.py.hbs +24 -0
- package/templates/python-fastapi/hexagonal/app/infrastructure/database/session.py.hbs +20 -0
- package/templates/python-fastapi/hexagonal/app/main.py.hbs +22 -14
- package/templates/python-fastapi/hexagonal/requirements.txt.hbs +3 -1
- package/templates/java-spring/clean/src/main/java/{{packagePath}}/infrastructure/persistence/InMemoryUserRepository.java.hbs +0 -41
- package/templates/java-spring/hexagonal/src/main/java/{{packagePath}}/infrastructure/persistence/InMemoryUserRepository.java.hbs +0 -41
- package/templates/nestjs/clean/src/infrastructure/database/in-memory.repository.ts.hbs +0 -17
- package/templates/nodejs-express/clean/src/infrastructure/database/InMemoryUserRepository.ts.hbs +0 -46
- package/templates/nodejs-express/hexagonal/src/adapters/outbound/persistence/InMemoryUserAdapter.ts.hbs +0 -38
- /package/templates/python-fastapi/hexagonal/app/core/{ports.py.hbs → ports/ports.py.hbs} +0 -0
|
@@ -9,16 +9,18 @@
|
|
|
9
9
|
"start": "node dist/index.js",
|
|
10
10
|
"lint": "eslint src --ext .ts",
|
|
11
11
|
"format": "prettier --write \"src/**/*.ts\"",
|
|
12
|
-
"test": "jest"
|
|
12
|
+
"test": "jest",
|
|
13
|
+
"migrate:dev": "prisma migrate dev",
|
|
14
|
+
"migrate:deploy": "prisma migrate deploy",
|
|
15
|
+
"generate": "prisma generate"
|
|
13
16
|
},
|
|
14
17
|
"keywords": [
|
|
15
18
|
"express",
|
|
16
19
|
"api",
|
|
17
20
|
"typescript",
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"auth"
|
|
21
|
+
"hexagonal-architecture",
|
|
22
|
+
"prisma",
|
|
23
|
+
"zod"
|
|
22
24
|
],
|
|
23
25
|
"author": "",
|
|
24
26
|
"license": "MIT",
|
|
@@ -30,7 +32,10 @@
|
|
|
30
32
|
"morgan": "^1.10.0",
|
|
31
33
|
"jsonwebtoken": "^9.0.2",
|
|
32
34
|
"bcryptjs": "^2.4.3",
|
|
33
|
-
"stripe": "^14.14.0"
|
|
35
|
+
"stripe": "^14.14.0",
|
|
36
|
+
"zod": "^3.22.4",
|
|
37
|
+
"express-async-errors": "^3.1.1",
|
|
38
|
+
"@prisma/client": "^5.10.2"
|
|
34
39
|
},
|
|
35
40
|
"devDependencies": {
|
|
36
41
|
"@types/express": "^4.17.21",
|
|
@@ -47,9 +52,10 @@
|
|
|
47
52
|
"@typescript-eslint/parser": "^6.21.0",
|
|
48
53
|
"prettier": "^3.2.5",
|
|
49
54
|
"jest": "^29.7.0",
|
|
50
|
-
"@types/jest": "^29.5.12"
|
|
55
|
+
"@types/jest": "^29.5.12",
|
|
56
|
+
"prisma": "^5.10.2"
|
|
51
57
|
},
|
|
52
58
|
"engines": {
|
|
53
59
|
"node": ">=18.0.0"
|
|
54
60
|
}
|
|
55
|
-
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
generator client {
|
|
2
|
+
provider = "prisma-client-js"
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
datasource db {
|
|
6
|
+
provider = "postgresql"
|
|
7
|
+
url = env("DATABASE_URL")
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
model User {
|
|
11
|
+
id String @id @default(uuid())
|
|
12
|
+
email String @unique
|
|
13
|
+
name String
|
|
14
|
+
password String
|
|
15
|
+
stripeCustomerId String? @map("stripe_customer_id")
|
|
16
|
+
createdAt DateTime @default(now()) @map("created_at")
|
|
17
|
+
updatedAt DateTime @updatedAt @map("updated_at")
|
|
18
|
+
|
|
19
|
+
@@map("users")
|
|
20
|
+
}
|
|
@@ -1,48 +1,33 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
import { IAuthService } from '../../../core/ports/inbound/IAuthService';
|
|
3
|
+
import { z } from 'zod';
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const router = Router();
|
|
5
|
+
const registerSchema = z.object({
|
|
6
|
+
email: z.string().email(),
|
|
7
|
+
name: z.string().min(2),
|
|
8
|
+
password: z.string().min(6),
|
|
9
|
+
});
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const { email, name, password } = req.body;
|
|
14
|
-
const result = await authService.register(email, name, password);
|
|
15
|
-
res.status(201).json({
|
|
16
|
-
token: result.token,
|
|
17
|
-
user: { id: result.user.id, email: result.user.email, name: result.user.name },
|
|
18
|
-
});
|
|
19
|
-
} catch (error: any) {
|
|
20
|
-
res.status(400).json({ error: error.message });
|
|
21
|
-
}
|
|
22
|
-
});
|
|
11
|
+
export class AuthController {
|
|
12
|
+
constructor(private authService: IAuthService) {}
|
|
23
13
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
res.status(401).json({ error: error.message });
|
|
34
|
-
}
|
|
35
|
-
});
|
|
14
|
+
register = async (req: Request, res: Response, next: NextFunction) => {
|
|
15
|
+
try {
|
|
16
|
+
const { email, name, password } = registerSchema.parse(req.body);
|
|
17
|
+
const result = await this.authService.register(email, name, password);
|
|
18
|
+
res.status(201).json(result);
|
|
19
|
+
} catch (error) {
|
|
20
|
+
next(error);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
36
23
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return router;
|
|
48
|
-
}
|
|
24
|
+
login = async (req: Request, res: Response, next: NextFunction) => {
|
|
25
|
+
try {
|
|
26
|
+
const { email, password } = req.body;
|
|
27
|
+
const result = await this.authService.login(email, password);
|
|
28
|
+
res.json(result);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
next(error);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
package/templates/nodejs-express/hexagonal/src/adapters/inbound/http/middlewares/errorHandler.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
import { ZodError } from 'zod';
|
|
3
|
+
|
|
4
|
+
export function errorHandler(
|
|
5
|
+
err: Error,
|
|
6
|
+
req: Request,
|
|
7
|
+
res: Response,
|
|
8
|
+
next: NextFunction
|
|
9
|
+
) {
|
|
10
|
+
console.error(err);
|
|
11
|
+
|
|
12
|
+
if (err instanceof ZodError) {
|
|
13
|
+
return res.status(400).json({
|
|
14
|
+
error: 'Validation Error',
|
|
15
|
+
details: err.format(),
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (err.message === 'User already exists' || err.message === 'Invalid credentials') {
|
|
20
|
+
return res.status(400).json({ error: err.message });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
res.status(500).json({ error: 'Internal Server Error' });
|
|
24
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { User } from '../../../core/domain/entities/User';
|
|
2
|
+
import { IUserRepositoryPort } from '../../../core/ports/outbound/IUserRepositoryPort';
|
|
3
|
+
import { prisma } from './prisma';
|
|
4
|
+
|
|
5
|
+
export class PrismaUserAdapter implements IUserRepositoryPort {
|
|
6
|
+
async findById(id: string): Promise<User | null> {
|
|
7
|
+
const user = await prisma.user.findUnique({ where: { id } });
|
|
8
|
+
if (!user) return null;
|
|
9
|
+
return User.restore({
|
|
10
|
+
id: user.id,
|
|
11
|
+
email: user.email,
|
|
12
|
+
name: user.name,
|
|
13
|
+
password: user.password,
|
|
14
|
+
stripeCustomerId: user.stripeCustomerId,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async findByEmail(email: string): Promise<User | null> {
|
|
19
|
+
const user = await prisma.user.findUnique({ where: { email } });
|
|
20
|
+
if (!user) return null;
|
|
21
|
+
return User.restore({
|
|
22
|
+
id: user.id,
|
|
23
|
+
email: user.email,
|
|
24
|
+
name: user.name,
|
|
25
|
+
password: user.password,
|
|
26
|
+
stripeCustomerId: user.stripeCustomerId,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async save(user: User): Promise<User> {
|
|
31
|
+
const data = {
|
|
32
|
+
id: user.id,
|
|
33
|
+
email: user.email,
|
|
34
|
+
name: user.name,
|
|
35
|
+
password: user.password,
|
|
36
|
+
stripeCustomerId: user.stripeCustomerId,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const savedUser = await prisma.user.upsert({
|
|
40
|
+
where: { id: user.id || '' },
|
|
41
|
+
update: { ...data },
|
|
42
|
+
create: { ...data },
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return User.restore({
|
|
46
|
+
id: savedUser.id,
|
|
47
|
+
email: savedUser.email,
|
|
48
|
+
name: savedUser.name,
|
|
49
|
+
password: savedUser.password,
|
|
50
|
+
stripeCustomerId: savedUser.stripeCustomerId,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async update(user: User): Promise<User> {
|
|
55
|
+
return this.save(user);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async delete(id: string): Promise<void> {
|
|
59
|
+
await prisma.user.delete({ where: { id } });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import dotenv from 'dotenv';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
dotenv.config();
|
|
5
|
+
|
|
6
|
+
const envSchema = z.object({
|
|
7
|
+
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
|
|
8
|
+
PORT: z.string().default('3000'),
|
|
9
|
+
DATABASE_URL: z.string(),
|
|
10
|
+
JWT_SECRET: z.string().min(10),
|
|
11
|
+
STRIPE_SECRET_KEY: z.string().optional(),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const _env = envSchema.safeParse(process.env);
|
|
15
|
+
|
|
16
|
+
if (!_env.success) {
|
|
17
|
+
console.error('❌ Invalid environment variables:', _env.error.format());
|
|
18
|
+
throw new Error('Invalid environment variables');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const config = {
|
|
22
|
+
env: _env.data.NODE_ENV,
|
|
23
|
+
port: parseInt(_env.data.PORT, 10),
|
|
24
|
+
databaseUrl: _env.data.DATABASE_URL,
|
|
25
|
+
jwtSecret: _env.data.JWT_SECRET,
|
|
26
|
+
stripeSecretKey: _env.data.STRIPE_SECRET_KEY,
|
|
27
|
+
};
|
|
@@ -1,41 +1,38 @@
|
|
|
1
|
+
import 'express-async-errors';
|
|
1
2
|
import express from 'express';
|
|
2
3
|
import cors from 'cors';
|
|
3
4
|
import helmet from 'helmet';
|
|
4
5
|
import morgan from 'morgan';
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
// Adapters
|
|
11
|
-
import { InMemoryUserAdapter } from './adapters/outbound/persistence/InMemoryUserAdapter';
|
|
12
|
-
import { bcryptAdapter, jwtAdapter } from './adapters/outbound/SecurityAdapters';
|
|
13
|
-
import { createAuthController } from './adapters/inbound/http/AuthController';
|
|
14
|
-
|
|
15
|
-
dotenv.config();
|
|
6
|
+
import { config } from './config';
|
|
7
|
+
import { PrismaUserAdapter } from './adapters/outbound/persistence/PrismaUserAdapter';
|
|
8
|
+
import { AuthService } from './core/services/AuthService';
|
|
9
|
+
import { AuthController } from './adapters/inbound/http/AuthController';
|
|
10
|
+
import { errorHandler } from './adapters/inbound/http/middlewares/errorHandler';
|
|
16
11
|
|
|
17
12
|
const app = express();
|
|
18
13
|
|
|
19
|
-
//
|
|
20
|
-
app.use(
|
|
14
|
+
// Middlewares
|
|
15
|
+
app.use(express.json());
|
|
21
16
|
app.use(cors());
|
|
17
|
+
app.use(helmet());
|
|
22
18
|
app.use(morgan('dev'));
|
|
23
|
-
app.use(express.json());
|
|
24
19
|
|
|
25
|
-
// Dependency Injection
|
|
26
|
-
const userRepository = new
|
|
27
|
-
const authService = new AuthService(userRepository
|
|
20
|
+
// Dependency Injection
|
|
21
|
+
const userRepository = new PrismaUserAdapter();
|
|
22
|
+
const authService = new AuthService(userRepository);
|
|
23
|
+
const authController = new AuthController(authService);
|
|
28
24
|
|
|
29
|
-
//
|
|
30
|
-
app.
|
|
25
|
+
// Routes
|
|
26
|
+
app.post('/api/auth/register', (req, res, next) => authController.register(req, res, next));
|
|
27
|
+
app.post('/api/auth/login', (req, res, next) => authController.login(req, res, next));
|
|
31
28
|
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
app.get('/health', (req, res) => {
|
|
30
|
+
res.json({ status: 'ok', architecture: 'hexagonal' });
|
|
31
|
+
});
|
|
34
32
|
|
|
35
|
-
|
|
33
|
+
// Error Handler
|
|
34
|
+
app.use(errorHandler);
|
|
36
35
|
|
|
37
|
-
app.listen(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
console.log(`🔗 Health check: http://localhost:${PORT}/health`);
|
|
41
|
-
});
|
|
36
|
+
app.listen(config.port, () => {
|
|
37
|
+
console.log(`🚀 Server running on port ${config.port}`);
|
|
38
|
+
});
|
|
@@ -1,27 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"compilerOptions": {
|
|
3
|
-
"target": "
|
|
3
|
+
"target": "es2020",
|
|
4
4
|
"module": "commonjs",
|
|
5
|
-
"lib": [
|
|
6
|
-
"ES2022"
|
|
7
|
-
],
|
|
5
|
+
"lib": ["es2020"],
|
|
8
6
|
"outDir": "./dist",
|
|
9
7
|
"rootDir": "./src",
|
|
10
8
|
"strict": true,
|
|
11
9
|
"esModuleInterop": true,
|
|
12
10
|
"skipLibCheck": true,
|
|
13
11
|
"forceConsistentCasingInFileNames": true,
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"node"
|
|
18
|
-
]
|
|
12
|
+
"experimentalDecorators": true,
|
|
13
|
+
"emitDecoratorMetadata": true,
|
|
14
|
+
"resolveJsonModule": true
|
|
19
15
|
},
|
|
20
|
-
"include": [
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"exclude": [
|
|
24
|
-
"node_modules",
|
|
25
|
-
"dist"
|
|
26
|
-
]
|
|
27
|
-
}
|
|
16
|
+
"include": ["src/**/*"],
|
|
17
|
+
"exclude": ["node_modules", "**/*.spec.ts", "**/*.test.ts"]
|
|
18
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
from app.domain.entities.user import User
|
|
3
|
+
from app.domain.repositories.user_repository import IUserRepository
|
|
4
|
+
from app.domain.usecases.register_user import RegisterUserUseCase, IPasswordHasher, ITokenGenerator
|
|
5
|
+
|
|
6
|
+
# In Clean Architecture, Services usually orchestrate Use Cases or coordinate cross-domain logic.
|
|
7
|
+
# For simple CRUD, Use Cases might be sufficient, but here is a Service example.
|
|
8
|
+
|
|
9
|
+
class UserService:
|
|
10
|
+
def __init__(self, repository: IUserRepository):
|
|
11
|
+
self.repository = repository
|
|
12
|
+
|
|
13
|
+
async def get_user(self, user_id: str) -> Optional[User]:
|
|
14
|
+
return await self.repository.find_by_id(user_id)
|
|
15
|
+
|
|
16
|
+
async def get_user_by_email(self, email: str) -> Optional[User]:
|
|
17
|
+
return await self.repository.find_by_email(email)
|
|
18
|
+
|
|
19
|
+
# Note: Registration logic is handled by RegisterUserUseCase, aligning with Clean Architecture
|
|
20
|
+
# where specific complex actions are Use Cases.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
2
|
+
from functools import lru_cache
|
|
3
|
+
|
|
4
|
+
class Settings(BaseSettings):
|
|
5
|
+
PROJECT_NAME: str = "{{projectName}}"
|
|
6
|
+
API_V1_STR: str = "/api/v1"
|
|
7
|
+
|
|
8
|
+
# Database
|
|
9
|
+
# Use asyncpg for async SQLAlchemy
|
|
10
|
+
DATABASE_URL: str = "postgresql+asyncpg://postgres:postgres@localhost:5432/{{projectName}}_db"
|
|
11
|
+
|
|
12
|
+
# Security
|
|
13
|
+
SECRET_KEY: str = "change_this_to_a_secure_random_key"
|
|
14
|
+
ALGORITHM: str = "HS256"
|
|
15
|
+
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
|
|
16
|
+
|
|
17
|
+
# Stripe
|
|
18
|
+
STRIPE_API_KEY: str | None = None
|
|
19
|
+
|
|
20
|
+
model_config = SettingsConfigDict(env_file=".env", case_sensitive=True)
|
|
21
|
+
|
|
22
|
+
@lru_cache
|
|
23
|
+
def get_settings():
|
|
24
|
+
return Settings()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from sqlalchemy import Column, String, DateTime, Boolean
|
|
2
|
+
from sqlalchemy.sql import func
|
|
3
|
+
from app.infrastructure.database.session import Base
|
|
4
|
+
import uuid
|
|
5
|
+
|
|
6
|
+
class UserModel(Base):
|
|
7
|
+
__tablename__ = "users"
|
|
8
|
+
|
|
9
|
+
id = Column(String, primary_key=True, index=True, default=lambda: str(uuid.uuid4()))
|
|
10
|
+
email = Column(String, unique=True, index=True, nullable=False)
|
|
11
|
+
name = Column(String, nullable=False)
|
|
12
|
+
password = Column(String, nullable=False)
|
|
13
|
+
stripe_customer_id = Column(String, nullable=True)
|
|
14
|
+
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
15
|
+
is_active = Column(Boolean, default=True)
|
|
16
|
+
|
|
17
|
+
def to_dict(self):
|
|
18
|
+
return {
|
|
19
|
+
"id": self.id,
|
|
20
|
+
"email": self.email,
|
|
21
|
+
"name": self.name,
|
|
22
|
+
"stripe_customer_id": self.stripe_customer_id,
|
|
23
|
+
"created_at": self.created_at
|
|
24
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
3
|
+
from sqlalchemy import select, delete
|
|
4
|
+
from app.domain.repositories.user_repository import IUserRepository
|
|
5
|
+
from app.domain.entities.user import User
|
|
6
|
+
from app.infrastructure.database.models import UserModel
|
|
7
|
+
|
|
8
|
+
class PostgresUserRepository(IUserRepository):
|
|
9
|
+
def __init__(self, session: AsyncSession):
|
|
10
|
+
self.session = session
|
|
11
|
+
|
|
12
|
+
def _to_entity(self, model: UserModel) -> User:
|
|
13
|
+
return User(
|
|
14
|
+
id=model.id,
|
|
15
|
+
email=model.email,
|
|
16
|
+
name=model.name,
|
|
17
|
+
password=model.password,
|
|
18
|
+
stripe_customer_id=model.stripe_customer_id,
|
|
19
|
+
created_at=model.created_at
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
async def find_by_id(self, user_id: str) -> Optional[User]:
|
|
23
|
+
result = await self.session.execute(select(UserModel).where(UserModel.id == user_id))
|
|
24
|
+
model = result.scalars().first()
|
|
25
|
+
return self._to_entity(model) if model else None
|
|
26
|
+
|
|
27
|
+
async def find_by_email(self, email: str) -> Optional[User]:
|
|
28
|
+
result = await self.session.execute(select(UserModel).where(UserModel.email == email))
|
|
29
|
+
model = result.scalars().first()
|
|
30
|
+
return self._to_entity(model) if model else None
|
|
31
|
+
|
|
32
|
+
async def save(self, user: User) -> User:
|
|
33
|
+
# Check if exists to decide update vs insert (simplistic approach)
|
|
34
|
+
# In a real app, optimize this or use merge
|
|
35
|
+
existing = await self.find_by_id(user.id)
|
|
36
|
+
|
|
37
|
+
if existing:
|
|
38
|
+
stmt = select(UserModel).where(UserModel.id == user.id)
|
|
39
|
+
result = await self.session.execute(stmt)
|
|
40
|
+
model = result.scalars().first()
|
|
41
|
+
model.email = user.email
|
|
42
|
+
model.name = user.name
|
|
43
|
+
model.password = user.password
|
|
44
|
+
model.stripe_customer_id = user.stripe_customer_id
|
|
45
|
+
else:
|
|
46
|
+
model = UserModel(
|
|
47
|
+
id=user.id,
|
|
48
|
+
email=user.email,
|
|
49
|
+
name=user.name,
|
|
50
|
+
password=user.password,
|
|
51
|
+
stripe_customer_id=user.stripe_customer_id,
|
|
52
|
+
created_at=user.created_at
|
|
53
|
+
)
|
|
54
|
+
self.session.add(model)
|
|
55
|
+
|
|
56
|
+
await self.session.commit()
|
|
57
|
+
await self.session.refresh(model)
|
|
58
|
+
return self._to_entity(model)
|
|
59
|
+
|
|
60
|
+
async def delete(self, user_id: str) -> None:
|
|
61
|
+
await self.session.execute(delete(UserModel).where(UserModel.id == user_id))
|
|
62
|
+
await self.session.commit()
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
|
2
|
+
from sqlalchemy.orm import DeclarativeBase
|
|
3
|
+
from app.config import get_settings
|
|
4
|
+
|
|
5
|
+
settings = get_settings()
|
|
6
|
+
|
|
7
|
+
engine = create_async_engine(
|
|
8
|
+
settings.DATABASE_URL,
|
|
9
|
+
echo=True, # Set to False in production
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
AsyncSessionLocal = async_sessionmaker(
|
|
13
|
+
bind=engine,
|
|
14
|
+
class_=AsyncSession,
|
|
15
|
+
expire_on_commit=False,
|
|
16
|
+
autoflush=False,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
class Base(DeclarativeBase):
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
async def get_db():
|
|
23
|
+
async with AsyncSessionLocal() as session:
|
|
24
|
+
try:
|
|
25
|
+
yield session
|
|
26
|
+
finally:
|
|
27
|
+
await session.close()
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
from fastapi import APIRouter, HTTPException, Depends
|
|
2
2
|
from pydantic import BaseModel, EmailStr
|
|
3
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
3
4
|
from app.domain.usecases.register_user import RegisterUserUseCase
|
|
4
|
-
from app.infrastructure.database.
|
|
5
|
+
from app.infrastructure.database.postgres_repository import PostgresUserRepository
|
|
5
6
|
from app.infrastructure.security.adapters import BcryptHasher, JwtTokenGenerator
|
|
7
|
+
from app.infrastructure.database.session import get_db
|
|
6
8
|
|
|
7
9
|
router = APIRouter()
|
|
8
10
|
|
|
9
|
-
# Dependency Injection
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
# Dependency Injection Factory
|
|
12
|
+
def get_register_usecase(db: AsyncSession = Depends(get_db)) -> RegisterUserUseCase:
|
|
13
|
+
repo = PostgresUserRepository(db)
|
|
14
|
+
hasher = BcryptHasher()
|
|
15
|
+
token_gen = JwtTokenGenerator()
|
|
16
|
+
return RegisterUserUseCase(repo, hasher, token_gen)
|
|
14
17
|
|
|
15
18
|
class RegisterRequest(BaseModel):
|
|
16
19
|
email: EmailStr
|
|
@@ -18,9 +21,12 @@ class RegisterRequest(BaseModel):
|
|
|
18
21
|
password: str
|
|
19
22
|
|
|
20
23
|
@router.post("/register")
|
|
21
|
-
async def register(
|
|
24
|
+
async def register(
|
|
25
|
+
req: RegisterRequest,
|
|
26
|
+
usecase: RegisterUserUseCase = Depends(get_register_usecase)
|
|
27
|
+
):
|
|
22
28
|
try:
|
|
23
|
-
result = await
|
|
29
|
+
result = await usecase.execute(req.email, req.name, req.password)
|
|
24
30
|
return result
|
|
25
31
|
except ValueError as e:
|
|
26
32
|
raise HTTPException(status_code=400, detail=str(e))
|
|
@@ -1,10 +1,32 @@
|
|
|
1
|
+
from contextlib import asynccontextmanager
|
|
1
2
|
from fastapi import FastAPI
|
|
2
3
|
from app.infrastructure.http import auth_controller
|
|
4
|
+
from app.config import get_settings
|
|
5
|
+
from app.infrastructure.database.session import engine, Base
|
|
3
6
|
|
|
4
|
-
|
|
7
|
+
settings = get_settings()
|
|
5
8
|
|
|
6
|
-
|
|
9
|
+
@asynccontextmanager
|
|
10
|
+
async def lifespan(app: FastAPI):
|
|
11
|
+
# Create tables on startup (for development)
|
|
12
|
+
# In production, use Alembic migrations
|
|
13
|
+
async with engine.begin() as conn:
|
|
14
|
+
await conn.run_sync(Base.metadata.create_all)
|
|
15
|
+
yield
|
|
16
|
+
# Cleanup
|
|
17
|
+
await engine.dispose()
|
|
18
|
+
|
|
19
|
+
app = FastAPI(
|
|
20
|
+
title="{{projectName}} - Clean Architecture",
|
|
21
|
+
lifespan=lifespan
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
app.include_router(auth_controller.router, prefix=settings.API_V1_STR + "/auth", tags=["Auth"])
|
|
7
25
|
|
|
8
26
|
@app.get("/health")
|
|
9
27
|
def health():
|
|
10
|
-
return {
|
|
28
|
+
return {
|
|
29
|
+
"status": "ok",
|
|
30
|
+
"architecture": "clean",
|
|
31
|
+
"project": settings.PROJECT_NAME
|
|
32
|
+
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
fastapi>=0.109.0
|
|
2
2
|
uvicorn[standard]>=0.27.0
|
|
3
3
|
pydantic>=2.5.0
|
|
4
|
+
pydantic-settings>=2.1.0
|
|
4
5
|
python-dotenv>=1.0.0
|
|
5
6
|
python-jose[cryptography]>=3.3.0
|
|
6
7
|
passlib[bcrypt]>=1.7.4
|
|
7
8
|
stripe>=8.0.0
|
|
8
9
|
sqlalchemy>=2.0.0
|
|
9
10
|
alembic>=1.13.0
|
|
11
|
+
asyncpg>=0.29.0
|
|
10
12
|
psycopg2-binary>=2.9.9
|
|
11
13
|
pytest>=8.0.0
|
|
12
|
-
httpx>=0.26.0
|
|
14
|
+
httpx>=0.26.0
|