@re-shell/cli
Version:
Full-stack development platform uniting microservices and microfrontends. Build complete applications with .NET (ASP.NET Core Web API, Minimal API), Java (Spring Boot, Quarkus, Micronaut, Vert.x), Rust (Actix-Web, Warp, Rocket, Axum), Python (FastAPI, Dja
1,761 lines (1,488 loc) • 76.7 kB
JavaScript
"use strict";
/**
* Conduit Framework Template Generator
* Modern HTTP framework for Dart with built-in ORM
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.ConduitGenerator = void 0;
const dart_base_generator_1 = require("./dart-base-generator");
const fs_1 = require("fs");
const path = __importStar(require("path"));
class ConduitGenerator extends dart_base_generator_1.DartBackendGenerator {
constructor() {
super('Conduit');
}
getFrameworkDependencies() {
return [
'conduit: ^4.0.0',
'conduit_core: ^4.0.0',
'conduit_postgresql: ^4.0.0',
'conduit_test: ^4.0.0',
'conduit_open_api: ^4.0.0',
'conduit_codable: ^4.0.0',
'conduit_isolate_exec: ^4.0.0',
'jaguar_jwt: ^3.0.0',
'crypto: ^3.0.3',
'bcrypt: ^1.1.3',
'uuid: ^4.2.2',
'dotenv: ^4.1.0',
'yaml: ^3.1.2',
'args: ^2.4.2',
'logging: ^1.2.0',
'intl: ^0.18.1',
'http: ^1.1.2',
'collection: ^1.18.0',
'meta: ^1.11.0'
];
}
getDevDependencies() {
return [
'conduit_test: ^4.0.0',
'test_process: ^2.1.0',
'mockito: ^5.4.4',
'build_runner: ^2.4.7',
'build_test: ^2.2.2'
];
}
async generateFrameworkFiles(projectPath, options) {
// Generate main application file
await this.generateApplication(projectPath, options);
// Generate channel configuration
await this.generateChannel(projectPath, options);
// Generate controllers
await this.generateControllers(projectPath);
// Generate models
await this.generateModels(projectPath);
// Generate services
await this.generateServices(projectPath);
// Generate utilities
await this.generateUtilities(projectPath);
// Generate configuration
await this.generateConfig(projectPath);
// Generate migrations
await this.generateMigrations(projectPath);
// Generate test harness
await this.generateTestHarness(projectPath);
// Generate API documentation
await this.generateApiDocs(projectPath);
// Generate CLI commands
await this.generateCommands(projectPath);
}
async generateApplication(projectPath, options) {
const appContent = `import 'package:conduit_core/conduit_core.dart';
import 'package:dotenv/dotenv.dart';
import 'package:${options.name}/channel.dart';
/// This is the entry point for the application.
Future main() async {
// Load environment variables
final env = DotEnv()..load();
// Start the application
final port = int.parse(env['PORT'] ?? '8080');
final app = Application<${options.name.charAt(0).toUpperCase() + options.name.slice(1)}Channel>()
..options.port = port
..options.address = InternetAddress.anyIPv4;
// Configure logging
app.options.context['logger'] = Logger('${options.name}')
..level = env['LOG_LEVEL'] == 'debug' ? Level.ALL : Level.INFO;
// Start the application
await app.start(numberOfInstances: int.parse(env['INSTANCES'] ?? '1'));
print('Application started on port \\\${app.options.port}');
print('Use Ctrl-C (SIGINT) to stop running the application.');
}
`;
await fs_1.promises.writeFile(path.join(projectPath, 'bin', 'main.dart'), appContent);
}
async generateChannel(projectPath, options) {
const channelContent = `import 'dart:async';
import 'package:conduit_core/conduit_core.dart';
import 'package:conduit_postgresql/conduit_postgresql.dart';
import 'package:conduit_open_api/v3.dart';
import 'package:dotenv/dotenv.dart';
import 'controllers/auth_controller.dart';
import 'controllers/user_controller.dart';
import 'controllers/health_controller.dart';
import 'models/user.dart';
import 'services/auth_service.dart';
import 'utilities/auth_validator.dart';
/// This type initializes an application.
///
/// Override methods in this class to set up routes and initialize services like
/// database connections. See http://conduit.io/docs/http/channel/.
class ${options.name.charAt(0).toUpperCase() + options.name.slice(1)}Channel extends ApplicationChannel {
late ManagedContext context;
late AuthService authService;
late AuthValidator authValidator;
/// Initialize services in this method.
///
/// Implement this method to initialize services, read values from [options]
/// and any other initialization required before constructing [entryPoint].
///
/// This method is invoked prior to [entryPoint] being accessed.
@override
Future prepare() async {
final env = DotEnv()..load();
// Configure logging
logger.onRecord.listen((rec) {
print("\\\${rec.level.name}: \\\${rec.time}: \\\${rec.message}");
});
// Configure database
final config = DatabaseConfiguration()
..host = env['DB_HOST'] ?? 'localhost'
..port = int.parse(env['DB_PORT'] ?? '5432')
..databaseName = env['DB_NAME'] ?? 'conduit_db'
..username = env['DB_USER'] ?? 'postgres'
..password = env['DB_PASSWORD'] ?? 'postgres';
final dataModel = ManagedDataModel.fromCurrentMirrorSystem();
final persistentStore = PostgreSQLPersistentStore.fromConnectionInfo(
config.username!,
config.password!,
config.host!,
config.port!,
config.databaseName!,
);
context = ManagedContext(dataModel, persistentStore);
// Initialize services
authService = AuthService(context);
authValidator = AuthValidator(authService);
}
/// Construct the request channel.
///
/// Return an instance of some [Controller] that will be the initial receiver
/// of all HTTP requests.
///
/// This method is invoked after [prepare].
@override
Controller get entryPoint {
final router = Router();
// API documentation
router.route("/docs/*").link(() => FileController("doc/api"));
// Health check
router.route("/health").link(() => HealthController());
// Authentication routes
router
.route("/auth/register")
.link(() => AuthController(context, authService));
router
.route("/auth/login")
.link(() => AuthController(context, authService));
router
.route("/auth/refresh")
.link(() => AuthController(context, authService));
router
.route("/auth/logout")
.link(() => Authorizer.bearer(authValidator))!
.link(() => AuthController(context, authService));
// Protected routes
router
.route("/users/[:id]")
.link(() => Authorizer.bearer(authValidator))!
.link(() => UserController(context));
router
.route("/profile")
.link(() => Authorizer.bearer(authValidator))!
.link(() => UserController(context));
return router;
}
/// Final initialization tasks.
///
/// This method allows any resources that require asynchronous initialization to complete their
/// initialization process. This method is invoked after [entryPoint] has been constructed.
@override
Future didOpen() async {
// Run database migrations if needed
if (options!.context['migrate'] == true) {
logger.info("Running database migrations...");
await ManagedContext.defaultContext.upgrade();
}
// Log startup information
logger.info("Server started on port \\\${options!.port}");
logger.info("Database connected: \\\${context.persistentStore}");
}
/// Perform any cleanup tasks.
///
/// This method is invoked when the application is shutting down.
@override
Future willClose() async {
await context.close();
await super.willClose();
}
/// Document the API
@override
void documentComponents(APIDocumentContext context) {
super.documentComponents(context);
// Add security schemes
context.securitySchemes['bearer'] = APISecurityScheme.http('bearer');
// Add common responses
context.responses['BadRequest'] = APIResponse.schema(
'Bad Request',
APISchemaObject.object({
'error': APISchemaObject.string(),
'message': APISchemaObject.string(),
}),
contentType: 'application/json',
);
context.responses['Unauthorized'] = APIResponse.schema(
'Unauthorized',
APISchemaObject.object({
'error': APISchemaObject.string(),
'message': APISchemaObject.string(),
}),
contentType: 'application/json',
);
context.responses['NotFound'] = APIResponse.schema(
'Not Found',
APISchemaObject.object({
'error': APISchemaObject.string(),
'message': APISchemaObject.string(),
}),
contentType: 'application/json',
);
}
}
`;
await fs_1.promises.mkdir(path.join(projectPath, 'lib'), { recursive: true });
await fs_1.promises.writeFile(path.join(projectPath, 'lib', 'channel.dart'), channelContent);
}
async generateControllers(projectPath) {
const controllersDir = path.join(projectPath, 'lib', 'controllers');
await fs_1.promises.mkdir(controllersDir, { recursive: true });
// Health controller
const healthControllerContent = `import 'package:conduit_core/conduit_core.dart';
class HealthController extends Controller {
@override
FutureOr<RequestOrResponse?> handle(Request request) {
return Response.ok({
'status': 'healthy',
'service': 'conduit-api',
'timestamp': DateTime.now().toIso8601String(),
'uptime': DateTime.now().difference(_startTime).inSeconds,
'version': '1.0.0',
});
}
static final _startTime = DateTime.now();
}
`;
await fs_1.promises.writeFile(path.join(controllersDir, 'health_controller.dart'), healthControllerContent);
// Auth controller
const authControllerContent = `import 'dart:async';
import 'package:conduit_core/conduit_core.dart';
import '../models/user.dart';
import '../services/auth_service.dart';
import '../utilities/validators.dart';
class AuthController extends ResourceController {
AuthController(this.context, this.authService);
final ManagedContext context;
final AuthService authService;
@Operation.post()
Future<Response> register(@Bind.body() User user) async {
// Validate input
if (user.email == null || user.password == null || user.name == null) {
return Response.badRequest(body: {
'error': 'Missing required fields',
'fields': ['email', 'password', 'name'],
});
}
// Validate email format
if (!Validators.isValidEmail(user.email!)) {
return Response.badRequest(body: {
'error': 'Invalid email format',
});
}
// Validate password strength
final passwordError = Validators.validatePassword(user.password!);
if (passwordError != null) {
return Response.badRequest(body: {
'error': passwordError,
});
}
// Check if email exists
final existingQuery = Query<User>(context)
..where((u) => u.email).equalTo(user.email);
final existing = await existingQuery.fetchOne();
if (existing != null) {
return Response.conflict(body: {
'error': 'Email already registered',
});
}
// Hash password
user.hashedPassword = authService.hashPassword(user.password!);
user.password = null;
// Set defaults
user.role = user.role ?? 'user';
user.isActive = true;
user.emailVerified = false;
user.createdAt = DateTime.now();
user.updatedAt = DateTime.now();
// Create user
final insertQuery = Query<User>(context)..values = user;
final newUser = await insertQuery.insert();
// Generate tokens
final tokens = authService.generateTokens(newUser);
// Remove sensitive data
newUser.hashedPassword = null;
return Response.created('/users/\\\${newUser.id}', body: {
'user': newUser,
'tokens': tokens,
});
}
@Operation.post('login')
Future<Response> login(@Bind.body() Map<String, dynamic> body) async {
final email = body['email'] as String?;
final password = body['password'] as String?;
if (email == null || password == null) {
return Response.badRequest(body: {
'error': 'Email and password required',
});
}
// Find user
final query = Query<User>(context)
..where((u) => u.email).equalTo(email);
final user = await query.fetchOne();
if (user == null) {
return Response.unauthorized(body: {
'error': 'Invalid credentials',
});
}
// Verify password
if (!authService.verifyPassword(password, user.hashedPassword!)) {
return Response.unauthorized(body: {
'error': 'Invalid credentials',
});
}
// Check if active
if (user.isActive != true) {
return Response.forbidden(body: {
'error': 'Account deactivated',
});
}
// Update last login
user.lastLogin = DateTime.now();
final updateQuery = Query<User>(context)
..values = user
..where((u) => u.id).equalTo(user.id);
await updateQuery.updateOne();
// Generate tokens
final tokens = authService.generateTokens(user);
// Remove sensitive data
user.hashedPassword = null;
return Response.ok({
'user': user,
'tokens': tokens,
});
}
@Operation.post('refresh')
Future<Response> refreshToken(@Bind.body() Map<String, dynamic> body) async {
final refreshToken = body['refreshToken'] as String?;
if (refreshToken == null) {
return Response.badRequest(body: {
'error': 'Refresh token required',
});
}
try {
final tokens = await authService.refreshTokens(refreshToken);
return Response.ok({'tokens': tokens});
} catch (e) {
return Response.unauthorized(body: {
'error': 'Invalid refresh token',
});
}
}
@Operation.post('logout')
Future<Response> logout() async {
final authorization = request!.authorization;
if (authorization == null) {
return Response.unauthorized();
}
await authService.revokeToken(authorization.credentials);
return Response.ok({
'message': 'Logged out successfully',
});
}
@Operation.post('forgot-password')
Future<Response> forgotPassword(@Bind.body() Map<String, dynamic> body) async {
final email = body['email'] as String?;
if (email == null) {
return Response.badRequest(body: {
'error': 'Email required',
});
}
// Find user (don't reveal if email exists)
final query = Query<User>(context)
..where((u) => u.email).equalTo(email);
final user = await query.fetchOne();
if (user != null) {
await authService.sendPasswordResetEmail(user);
}
return Response.ok({
'message': 'If the email exists, a password reset link has been sent',
});
}
@Operation.post('reset-password')
Future<Response> resetPassword(@Bind.body() Map<String, dynamic> body) async {
final token = body['token'] as String?;
final newPassword = body['password'] as String?;
if (token == null || newPassword == null) {
return Response.badRequest(body: {
'error': 'Token and password required',
});
}
// Validate password
final passwordError = Validators.validatePassword(newPassword);
if (passwordError != null) {
return Response.badRequest(body: {
'error': passwordError,
});
}
try {
await authService.resetPassword(token, newPassword);
return Response.ok({
'message': 'Password reset successfully',
});
} catch (e) {
return Response.badRequest(body: {
'error': 'Invalid or expired token',
});
}
}
@override
Map<String, APIResponse> documentOperationResponses(
APIDocumentContext context,
Operation operation,
) {
final responses = super.documentOperationResponses(context, operation);
if (operation.method == 'POST') {
responses['201'] = APIResponse.schema(
'User created successfully',
APISchemaObject.object({
'user': context.schema['User']!,
'tokens': APISchemaObject.object({
'accessToken': APISchemaObject.string(),
'refreshToken': APISchemaObject.string(),
'expiresIn': APISchemaObject.integer(),
}),
}),
contentType: 'application/json',
);
responses['400'] = context.responses['BadRequest']!;
responses['401'] = context.responses['Unauthorized']!;
responses['409'] = APIResponse.schema(
'Conflict',
APISchemaObject.object({
'error': APISchemaObject.string(),
}),
contentType: 'application/json',
);
}
return responses;
}
}
`;
await fs_1.promises.writeFile(path.join(controllersDir, 'auth_controller.dart'), authControllerContent);
// User controller
const userControllerContent = `import 'dart:async';
import 'package:conduit_core/conduit_core.dart';
import '../models/user.dart';
import '../utilities/validators.dart';
class UserController extends ResourceController {
UserController(this.context);
final ManagedContext context;
@Operation.get()
Future<Response> getAllUsers({
@Bind.query('page') int page = 1,
@Bind.query('limit') int limit = 20,
@Bind.query('search') String? search,
@Bind.query('role') String? role,
}) async {
// Validate pagination
if (page < 1) page = 1;
if (limit < 1 || limit > 100) limit = 20;
final query = Query<User>(context);
// Apply filters
if (search != null && search.isNotEmpty) {
query.predicate = QueryPredicate(
'name ILIKE @name OR email ILIKE @email',
{'name': '%\\\$search%', 'email': '%\\\$search%'},
);
}
if (role != null && ['user', 'admin', 'moderator'].contains(role)) {
query.where((u) => u.role).equalTo(role);
}
// Apply pagination
query
..offset = (page - 1) * limit
..fetchLimit = limit
..sortBy((u) => u.createdAt, QuerySortOrder.descending);
// Execute query
final users = await query.fetch();
// Get total count
final countQuery = Query<User>(context);
if (search != null && search.isNotEmpty) {
countQuery.predicate = QueryPredicate(
'name ILIKE @name OR email ILIKE @email',
{'name': '%\\\$search%', 'email': '%\\\$search%'},
);
}
if (role != null) {
countQuery.where((u) => u.role).equalTo(role);
}
final total = await countQuery.reduce.count();
// Remove sensitive data
for (final user in users) {
user.hashedPassword = null;
}
return Response.ok({
'data': users,
'pagination': {
'page': page,
'limit': limit,
'total': total,
'totalPages': (total / limit).ceil(),
},
});
}
@Operation.get('id')
Future<Response> getUserById(@Bind.path('id') int id) async {
final query = Query<User>(context)
..where((u) => u.id).equalTo(id);
final user = await query.fetchOne();
if (user == null) {
return Response.notFound(body: {
'error': 'User not found',
});
}
// Check authorization
final requestUser = request!.authorization!.ownerID;
if (requestUser != id && request!.authorization!.resourceOwnerIdentifier != 'admin') {
// Only return public info for other users
user
..hashedPassword = null
..email = null
..lastLogin = null;
} else {
user.hashedPassword = null;
}
return Response.ok(user);
}
@Operation.put('id')
Future<Response> updateUser(
@Bind.path('id') int id,
@Bind.body() Map<String, dynamic> body,
) async {
// Check authorization
final requestUser = request!.authorization!.ownerID;
final isAdmin = request!.authorization!.resourceOwnerIdentifier == 'admin';
if (requestUser != id && !isAdmin) {
return Response.forbidden(body: {
'error': 'Cannot update other users',
});
}
// Get existing user
final query = Query<User>(context)
..where((u) => u.id).equalTo(id);
final user = await query.fetchOne();
if (user == null) {
return Response.notFound(body: {
'error': 'User not found',
});
}
// Update allowed fields
if (body.containsKey('name')) {
user.name = body['name'] as String;
}
if (body.containsKey('email')) {
final newEmail = body['email'] as String;
if (!Validators.isValidEmail(newEmail)) {
return Response.badRequest(body: {
'error': 'Invalid email format',
});
}
// Check if email is taken
final emailQuery = Query<User>(context)
..where((u) => u.email).equalTo(newEmail)
..where((u) => u.id).notEqualTo(id);
final existing = await emailQuery.fetchOne();
if (existing != null) {
return Response.conflict(body: {
'error': 'Email already in use',
});
}
user.email = newEmail;
user.emailVerified = false; // Require re-verification
}
// Only admins can change roles
if (isAdmin && body.containsKey('role')) {
final role = body['role'] as String;
if (['user', 'admin', 'moderator'].contains(role)) {
user.role = role;
}
}
// Only admins can change active status
if (isAdmin && body.containsKey('isActive')) {
user.isActive = body['isActive'] as bool;
}
// Update timestamp
user.updatedAt = DateTime.now();
// Save changes
final updateQuery = Query<User>(context)
..values = user
..where((u) => u.id).equalTo(id);
final updated = await updateQuery.updateOne();
// Remove sensitive data
updated!.hashedPassword = null;
return Response.ok(updated);
}
@Operation.delete('id')
Future<Response> deleteUser(@Bind.path('id') int id) async {
// Only admins can delete users
if (request!.authorization!.resourceOwnerIdentifier != 'admin') {
return Response.forbidden(body: {
'error': 'Admin access required',
});
}
final query = Query<User>(context)
..where((u) => u.id).equalTo(id);
final deleted = await query.delete();
if (deleted == 0) {
return Response.notFound(body: {
'error': 'User not found',
});
}
return Response.ok({
'message': 'User deleted successfully',
});
}
@Operation.get('profile')
Future<Response> getProfile() async {
final userId = request!.authorization!.ownerID;
final query = Query<User>(context)
..where((u) => u.id).equalTo(userId);
final user = await query.fetchOne();
if (user == null) {
return Response.notFound(body: {
'error': 'User not found',
});
}
user.hashedPassword = null;
return Response.ok(user);
}
@Operation.put('profile')
Future<Response> updateProfile(@Bind.body() Map<String, dynamic> body) async {
final userId = request!.authorization!.ownerID;
// Get user
final query = Query<User>(context)
..where((u) => u.id).equalTo(userId);
final user = await query.fetchOne();
if (user == null) {
return Response.notFound(body: {
'error': 'User not found',
});
}
// Update allowed fields
if (body.containsKey('name')) {
user.name = body['name'] as String;
}
if (body.containsKey('password')) {
final currentPassword = body['currentPassword'] as String?;
final newPassword = body['password'] as String;
if (currentPassword == null) {
return Response.badRequest(body: {
'error': 'Current password required',
});
}
// Verify current password
final authService = AuthService(context);
if (!authService.verifyPassword(currentPassword, user.hashedPassword!)) {
return Response.unauthorized(body: {
'error': 'Invalid current password',
});
}
// Validate new password
final passwordError = Validators.validatePassword(newPassword);
if (passwordError != null) {
return Response.badRequest(body: {
'error': passwordError,
});
}
user.hashedPassword = authService.hashPassword(newPassword);
}
// Update timestamp
user.updatedAt = DateTime.now();
// Save changes
final updateQuery = Query<User>(context)
..values = user
..where((u) => u.id).equalTo(userId);
final updated = await updateQuery.updateOne();
updated!.hashedPassword = null;
return Response.ok(updated);
}
@override
Map<String, APIResponse> documentOperationResponses(
APIDocumentContext context,
Operation operation,
) {
final responses = super.documentOperationResponses(context, operation);
if (operation.method == 'GET') {
responses['200'] = APIResponse.schema(
'Success',
operation.pathVariables.isEmpty
? APISchemaObject.object({
'data': APISchemaObject.array(ofSchema: context.schema['User']!),
'pagination': APISchemaObject.object({
'page': APISchemaObject.integer(),
'limit': APISchemaObject.integer(),
'total': APISchemaObject.integer(),
'totalPages': APISchemaObject.integer(),
}),
})
: context.schema['User']!,
contentType: 'application/json',
);
}
responses['401'] = context.responses['Unauthorized']!;
responses['403'] = APIResponse.schema(
'Forbidden',
APISchemaObject.object({
'error': APISchemaObject.string(),
}),
contentType: 'application/json',
);
responses['404'] = context.responses['NotFound']!;
return responses;
}
}
`;
await fs_1.promises.writeFile(path.join(controllersDir, 'user_controller.dart'), userControllerContent);
}
async generateModels(projectPath) {
const modelsDir = path.join(projectPath, 'lib', 'models');
await fs_1.promises.mkdir(modelsDir, { recursive: true });
// User model
const userModelContent = `import 'package:conduit_core/conduit_core.dart';
import 'package:conduit_codable/conduit_codable.dart';
class User extends ManagedObject<_User> implements _User {
@override
void willUpdate() {
updatedAt = DateTime.now();
}
@override
void willInsert() {
createdAt = DateTime.now();
updatedAt = DateTime.now();
}
/// Transient property for password input
@Serialize(input: true, output: false)
String? password;
/// Convert to public JSON (without sensitive data)
Map<String, dynamic> toPublicJson() {
final json = asMap();
json.remove('hashedPassword');
json.remove('sessions');
json.remove('refreshTokens');
return json;
}
}
class _User {
@primaryKey
int? id;
@Column(indexed: true, unique: true)
String? email;
@Column()
String? name;
@Column(omitByDefault: true)
String? hashedPassword;
@Column(defaultValue: "'user'")
String? role;
@Column(defaultValue: 'true')
bool? isActive;
@Column(defaultValue: 'false')
bool? emailVerified;
@Column()
DateTime? lastLogin;
@Column()
DateTime? createdAt;
@Column()
DateTime? updatedAt;
/// User's sessions
ManagedSet<Session>? sessions;
/// User's refresh tokens
ManagedSet<RefreshToken>? refreshTokens;
}
class Session extends ManagedObject<_Session> implements _Session {
@override
void willInsert() {
createdAt = DateTime.now();
}
}
class _Session {
@primaryKey
int? id;
@Column(indexed: true)
String? token;
@Relate(#sessions)
User? user;
@Column()
String? ipAddress;
@Column()
String? userAgent;
@Column()
DateTime? expiresAt;
@Column(defaultValue: 'true')
bool? isActive;
@Column()
DateTime? createdAt;
}
class RefreshToken extends ManagedObject<_RefreshToken> implements _RefreshToken {
@override
void willInsert() {
createdAt = DateTime.now();
}
}
class _RefreshToken {
@primaryKey
int? id;
@Column(indexed: true, unique: true)
String? token;
@Relate(#refreshTokens)
User? user;
@Column()
DateTime? expiresAt;
@Column(defaultValue: 'false')
bool? isRevoked;
@Column()
DateTime? revokedAt;
@Column()
String? replacedByToken;
@Column()
DateTime? createdAt;
}
/// API Key model for service-to-service auth
class ApiKey extends ManagedObject<_ApiKey> implements _ApiKey {
@override
void willInsert() {
createdAt = DateTime.now();
lastUsed = DateTime.now();
}
}
class _ApiKey {
@primaryKey
int? id;
@Column(indexed: true, unique: true)
String? key;
@Column()
String? name;
@Column()
String? description;
@Column(defaultValue: "'read'")
String? scope;
@Column(defaultValue: 'true')
bool? isActive;
@Column()
DateTime? expiresAt;
@Column()
DateTime? lastUsed;
@Column()
int? usageCount;
@Column()
DateTime? createdAt;
}
`;
await fs_1.promises.writeFile(path.join(modelsDir, 'user.dart'), userModelContent);
}
async generateServices(projectPath) {
const servicesDir = path.join(projectPath, 'lib', 'services');
await fs_1.promises.mkdir(servicesDir, { recursive: true });
// Auth service
const authServiceContent = `import 'dart:convert';
import 'dart:math';
import 'package:conduit_core/conduit_core.dart';
import 'package:jaguar_jwt/jaguar_jwt.dart';
import 'package:crypto/crypto.dart';
import 'package:bcrypt/bcrypt.dart';
import 'package:dotenv/dotenv.dart';
import '../models/user.dart';
class AuthService {
AuthService(this.context) {
final env = DotEnv()..load();
_jwtSecret = env['JWT_SECRET'] ?? _generateSecret();
_jwtIssuer = env['JWT_ISSUER'] ?? 'conduit-api';
_accessTokenExpiry = Duration(minutes: int.parse(env['ACCESS_TOKEN_EXPIRY'] ?? '15'));
_refreshTokenExpiry = Duration(days: int.parse(env['REFRESH_TOKEN_EXPIRY'] ?? '30'));
}
final ManagedContext context;
late final String _jwtSecret;
late final String _jwtIssuer;
late final Duration _accessTokenExpiry;
late final Duration _refreshTokenExpiry;
/// Hash password using bcrypt
String hashPassword(String password) {
return BCrypt.hashpw(password, BCrypt.gensalt());
}
/// Verify password against hash
bool verifyPassword(String password, String hash) {
return BCrypt.checkpw(password, hash);
}
/// Generate access and refresh tokens
Map<String, dynamic> generateTokens(User user) {
final now = DateTime.now();
final accessExpiry = now.add(_accessTokenExpiry);
final refreshExpiry = now.add(_refreshTokenExpiry);
// Create access token
final accessClaims = JwtClaim(
subject: user.id.toString(),
issuer: _jwtIssuer,
audience: ['conduit-api'],
jwtId: _generateTokenId(),
issuedAt: now,
expiry: accessExpiry,
otherClaims: {
'email': user.email,
'role': user.role,
'type': 'access',
},
);
final accessToken = issueJwtHS256(accessClaims, _jwtSecret);
// Create refresh token
final refreshToken = _generateRefreshToken();
// Store refresh token
final refreshQuery = Query<RefreshToken>(context)
..values.token = refreshToken
..values.user = user
..values.expiresAt = refreshExpiry;
refreshQuery.insert();
return {
'accessToken': accessToken,
'refreshToken': refreshToken,
'tokenType': 'Bearer',
'expiresIn': _accessTokenExpiry.inSeconds,
};
}
/// Refresh access token using refresh token
Future<Map<String, dynamic>> refreshTokens(String refreshToken) async {
// Find refresh token
final query = Query<RefreshToken>(context)
..where((t) => t.token).equalTo(refreshToken)
..where((t) => t.isRevoked).equalTo(false)
..join(object: (t) => t.user);
final token = await query.fetchOne();
if (token == null) {
throw StateError('Invalid refresh token');
}
// Check expiry
if (DateTime.now().isAfter(token.expiresAt!)) {
// Revoke expired token
token.isRevoked = true;
token.revokedAt = DateTime.now();
final updateQuery = Query<RefreshToken>(context)
..values = token
..where((t) => t.id).equalTo(token.id);
await updateQuery.updateOne();
throw StateError('Refresh token expired');
}
// Generate new tokens
final newTokens = generateTokens(token.user!);
// Revoke old refresh token and link to new one
token.isRevoked = true;
token.revokedAt = DateTime.now();
token.replacedByToken = newTokens['refreshToken'];
final updateQuery = Query<RefreshToken>(context)
..values = token
..where((t) => t.id).equalTo(token.id);
await updateQuery.updateOne();
return newTokens;
}
/// Validate access token
Future<User?> validateAccessToken(String token) async {
try {
final claims = verifyJwtHS256Signature(token, _jwtSecret);
// Check token type
if (claims.otherClaims['type'] != 'access') {
return null;
}
// Check expiry
if (claims.expiry != null && DateTime.now().isAfter(claims.expiry!)) {
return null;
}
// Get user
final userId = int.parse(claims.subject!);
final query = Query<User>(context)
..where((u) => u.id).equalTo(userId)
..where((u) => u.isActive).equalTo(true);
return await query.fetchOne();
} catch (e) {
return null;
}
}
/// Revoke token (logout)
Future<void> revokeToken(String accessToken) async {
try {
final claims = verifyJwtHS256Signature(accessToken, _jwtSecret);
final userId = int.parse(claims.subject!);
// Revoke all user's refresh tokens
final query = Query<RefreshToken>(context)
..where((t) => t.user.id).equalTo(userId)
..where((t) => t.isRevoked).equalTo(false)
..values.isRevoked = true
..values.revokedAt = DateTime.now();
await query.update();
} catch (e) {
// Token might be invalid, but logout should still succeed
}
}
/// Send password reset email
Future<void> sendPasswordResetEmail(User user) async {
final token = _generateResetToken();
final expiry = DateTime.now().add(Duration(hours: 1));
// Store reset token (in production, use Redis or similar)
// For now, we'll store it in a temporary session
final sessionQuery = Query<Session>(context)
..values.token = 'reset_\$token'
..values.user = user
..values.expiresAt = expiry
..values.ipAddress = 'password_reset'
..values.userAgent = 'email';
await sessionQuery.insert();
// In production, send actual email
print('Password reset link: http://localhost:8080/reset-password?token=\\\$token');
}
/// Reset password with token
Future<void> resetPassword(String token, String newPassword) async {
// Find reset token
final query = Query<Session>(context)
..where((s) => s.token).equalTo('reset_\$token')
..where((s) => s.isActive).equalTo(true)
..join(object: (s) => s.user);
final session = await query.fetchOne();
if (session == null) {
throw StateError('Invalid reset token');
}
// Check expiry
if (DateTime.now().isAfter(session.expiresAt!)) {
throw StateError('Reset token expired');
}
// Update password
final user = session.user!;
user.hashedPassword = hashPassword(newPassword);
final userQuery = Query<User>(context)
..values = user
..where((u) => u.id).equalTo(user.id);
await userQuery.updateOne();
// Invalidate reset token
session.isActive = false;
final sessionQuery = Query<Session>(context)
..values = session
..where((s) => s.id).equalTo(session.id);
await sessionQuery.updateOne();
// Revoke all refresh tokens for security
final revokeQuery = Query<RefreshToken>(context)
..where((t) => t.user.id).equalTo(user.id)
..values.isRevoked = true
..values.revokedAt = DateTime.now();
await revokeQuery.update();
}
/// Generate a random secret
String _generateSecret() {
final random = Random.secure();
final bytes = List<int>.generate(32, (_) => random.nextInt(256));
return base64Url.encode(bytes);
}
/// Generate token ID
String _generateTokenId() {
final random = Random.secure();
final bytes = List<int>.generate(16, (_) => random.nextInt(256));
return base64Url.encode(bytes);
}
/// Generate refresh token
String _generateRefreshToken() {
final random = Random.secure();
final bytes = List<int>.generate(32, (_) => random.nextInt(256));
return base64Url.encode(bytes);
}
/// Generate reset token
String _generateResetToken() {
final random = Random.secure();
final bytes = List<int>.generate(24, (_) => random.nextInt(256));
return base64Url.encode(bytes);
}
}
`;
await fs_1.promises.writeFile(path.join(servicesDir, 'auth_service.dart'), authServiceContent);
}
async generateUtilities(projectPath) {
const utilitiesDir = path.join(projectPath, 'lib', 'utilities');
await fs_1.promises.mkdir(utilitiesDir, { recursive: true });
// Auth validator
const authValidatorContent = `import 'dart:async';
import 'package:conduit_core/conduit_core.dart';
import '../models/user.dart';
import '../services/auth_service.dart';
/// Bearer token validator for Conduit authorization
class AuthValidator extends AuthValidator<User> {
AuthValidator(this.authService);
final AuthService authService;
@override
FutureOr<Authorization?> validate<T>(
AuthorizationParser<T> parser,
T authorizationData,
{List<AuthScope>? requiredScope}
) async {
if (authorizationData is! String) {
return null;
}
final user = await authService.validateAccessToken(authorizationData);
if (user == null) {
return null;
}
// Create authorization
return Authorization(
user.id!,
this,
credentials: authorizationData,
resourceOwnerIdentifier: user.role,
);
}
}
/// API Key validator
class ApiKeyValidator extends AuthValidator<String> {
ApiKeyValidator(this.context);
final ManagedContext context;
@override
FutureOr<Authorization?> validate<T>(
AuthorizationParser<T> parser,
T authorizationData,
{List<AuthScope>? requiredScope}
) async {
if (authorizationData is! String) {
return null;
}
// Find API key
final query = Query<ApiKey>(context)
..where((k) => k.key).equalTo(authorizationData)
..where((k) => k.isActive).equalTo(true);
final apiKey = await query.fetchOne();
if (apiKey == null) {
return null;
}
// Check expiry
if (apiKey.expiresAt != null && DateTime.now().isAfter(apiKey.expiresAt!)) {
return null;
}
// Update usage
apiKey.lastUsed = DateTime.now();
apiKey.usageCount = (apiKey.usageCount ?? 0) + 1;
final updateQuery = Query<ApiKey>(context)
..values = apiKey
..where((k) => k.id).equalTo(apiKey.id);
await updateQuery.updateOne();
// Create authorization
return Authorization(
apiKey.id!,
this,
credentials: authorizationData,
resourceOwnerIdentifier: apiKey.scope,
);
}
}
`;
await fs_1.promises.writeFile(path.join(utilitiesDir, 'auth_validator.dart'), authValidatorContent);
// Validators
const validatorsContent = `/// Input validation utilities
class Validators {
/// Validate email format
static bool isValidEmail(String email) {
final regex = RegExp(
r'^[a-zA-Z0-9.!#\$%&*+/=?^_\`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,253}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,253}[a-zA-Z0-9])?)*\$',
);
return regex.hasMatch(email);
}
/// Validate password strength
static String? validatePassword(String password) {
if (password.length < 8) {
return 'Password must be at least 8 characters';
}
if (!password.contains(RegExp(r'[A-Z]'))) {
return 'Password must contain uppercase letter';
}
if (!password.contains(RegExp(r'[a-z]'))) {
return 'Password must contain lowercase letter';
}
if (!password.contains(RegExp(r'[0-9]'))) {
return 'Password must contain number';
}
if (!password.contains(RegExp(r'[!@#$%^&*(),.?":{}|<>]'))) {
return 'Password must contain special character';
}
return null; // Valid
}
/// Validate username
static String? validateUsername(String username) {
if (username.length < 3 || username.length > 20) {
return 'Username must be 3-20 characters';
}
if (!RegExp(r'^[a-zA-Z0-9_]+$').hasMatch(username)) {
return 'Username can only contain letters, numbers, and underscore';
}
return null; // Valid
}
/// Validate phone number
static bool isValidPhone(String phone) {
final regex = RegExp(r'^\+?[1-9]\d{1,14}$');
return regex.hasMatch(phone);
}
/// Validate URL
static bool isValidUrl(String url) {
try {
final uri = Uri.parse(url);
return uri.isAbsolute && (uri.scheme == 'http' || uri.scheme == 'https');
} catch (_) {
return false;
}
}
/// Validate UUID
static bool isValidUuid(String uuid) {
final regex = RegExp(
r'^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\$',
caseSensitive: false,
);
return regex.hasMatch(uuid);
}
/// Sanitize input for security
static String sanitizeInput(String input) {
return input
.replaceAll('<', '<')
.replaceAll('>', '>')
.replaceAll('"', '"')
.replaceAll("'", ''')
.replaceAll('/', '/');
}
}
/// Rate limiting helper
class RateLimiter {
static final Map<String, List<DateTime>> _requests = {};
static final Duration _window = Duration(minutes: 15);
static final int _maxRequests = 100;
/// Check if request should be rate limited
static bool shouldLimit(String key, {int? maxRequests, Duration? window}) {
final now = DateTime.now();
final windowDuration = window ?? _window;
final limit = maxRequests ?? _maxRequests;
// Get or create request list
_requests[key] ??= [];
final requests = _requests[key]!;
// Remove old requests outside window
requests.removeWhere((time) => now.difference(time) > windowDuration);
// Check limit
if (requests.length >= limit) {
return true;
}
// Add current request
requests.add(now);
return false;
}
/// Get remaining requests
static int remaining(String key, {int? maxRequests}) {
final limit = maxRequests ?? _maxRequests;
final requests = _requests[key]?.length ?? 0;
return limit - requests;
}
/// Reset rate limit for key
static void reset(String key) {
_requests.remove(key);
}
}
`;
await fs_1.promises.writeFile(path.join(utilitiesDir, 'validators.dart'), validatorsContent);
// Response helpers
const responseHelpersContent = `import 'package:conduit_core/conduit_core.dart';
/// Standard response helpers
class ResponseHelpers {
/// Success response
static Response success(dynamic data, {String? message, int statusCode = 200}) {
return Response(
statusCode,
null,
{
'success': true,
'message': message ?? 'Operation successful',
'data': data,
},
);
}
/// Error response
static Response error(String message, {int statusCode = 400, dynamic errors}) {
final body = {
'success': false,
'message': message,
};
if (errors != null) {
body['errors'] = errors;
}
return Response(statusCode, null, body);
}
/// Paginated response
static Response paginated({
required List<dynamic> data,
required int page,
required int limit,
required int total,
String? message,
}) {
return Response.ok({
'success': true,
'message': message ?? 'Data retrieved successfully',
'data': data,
'pagination': {
'page': page,
'limit': limit,
'total': total,
'totalPages': (total / limit).ceil(),
'hasNext': page < (total / limit).ceil(),
'hasPrev': page > 1,
},
});
}
/// File response
static Response file(List<int> bytes, String filename, String contentType) {
return Response.ok(bytes)
..contentType = ContentType.parse(contentType)
..headers['content-disposition'] = 'attachment; filename="\$filename"';
}
/// Stream response
static Response stream(Stream<List<int>> stream, String contentType) {
return Response.ok(stream)
..contentType = ContentType.parse(contentType);
}
}
/// Common error messages
class ErrorMessages {
static const String unauthorized = 'Unauthorized access';
static const String forbidden = 'Access forbidden';
static const String notFound = 'Resource not found';
static const String badRequest = 'Invalid request';
static const String conflict = 'Resource conflict';
static const String serverError = 'Internal server error';
static const String validationFailed = 'Validation failed';
static const String rateLimited = 'Too many requests';
}
`;
await fs_1.promises.writeFile(path.join(utilitiesDir, 'response_helpers.dart'), responseHelpersContent);
}
async generateConfig(projectPath) {
const configDir = path.join(projectPath, 'config');
await fs_1.promises.mkdir(configDir, { recursive: true });
// Database configuration
const dbConfigContent = `name: ${this.config.framework.toLowerCase()}_db
host: localhost
port: 5432
username: postgres
password: postgres
databaseName: conduit_dev
# Test database
test:
host: localhost
port: 5432
username: postgres
password: postgres
databaseName: conduit_test
`;
await fs_1.promises.writeFile(path.join(configDir, 'database.yaml'), dbConfigContent);
// Application configuration
const appConfigContent = `# Application configuration
server:
port: 8080
host: 0.0.0.0
instances: 1
# Database
database:
host: \${DB_HOST:-localhost}
port: \${DB_PORT:-5432}
name: \${DB_NAME:-conduit_db}
username: \${DB_USER:-postgres}
password: \${DB_PASSWORD:-postgres}
ssl: \${DB_SSL:-false}
poolSize: \${DB_POOL_SIZE:-10}
# JWT Configuration
jwt:
secret: \${JWT_SECRET:-your-secret-key-change-in-production}
issuer: \${JWT_ISSUER:-conduit-api}
accessTokenExpiry: \${ACCESS_TOKEN_EXPIRY:-15} # minutes
refreshTokenExpiry: \${REFRESH_TOKEN_EXPIRY:-30} # days
# Redis (optional)
redis:
host: \${REDIS_HOST:-localhost}
port: \${REDIS_PORT:-6379}
password: \${REDIS_PASSWORD:-}
database: \${REDIS_DB:-0}
# Email
email:
smtp:
host: \${SMTP_HOST:-smtp.gmail.com}
port: \${SMTP_PORT:-587}
username: \${SMTP_USER:-}
password: \${SMTP_PASSWORD:-}
secure: \${SMTP_SECURE:-true}
from:
email: \${EMAIL_FROM:-noreply@example.com}
name: \${EMAIL_FROM_NAME:-Conduit API}
# CORS
cors:
allowOrigins:
- http://localhost:3000
- http://localhost:8080
allowMethods:
- GET
- POST
- PUT
- DELETE
- PATCH
- OPTIONS
allowHeaders:
- Content-Type
- Authorization
- X-Requested-With
allowCredentials: true
maxAge: 86400
# Rate Limiting
rateLimit:
windowMinutes: 15
maxRequests: 100
strictWindowMinutes: 15
strictMaxRequests: 5
# Features
features:
registration: true
emailVerification: true
passwordReset: true
apiKeys: true