create-arktos 1.0.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.
@@ -0,0 +1,63 @@
1
+ import { Router, Request, Response } from 'express';
2
+ import authRoutes from './auth.routes';
3
+ import { createSuccessResponse } from '../utils/response';
4
+ import DatabaseService from '../services/database.service';
5
+
6
+ const router = Router();
7
+
8
+ // Health check endpoint
9
+ router.get('/health', async (req: Request, res: Response) => {
10
+ try {
11
+ const dbService = DatabaseService.getInstance();
12
+ const dbHealth = await dbService.healthCheck();
13
+
14
+ if (!dbHealth.connected) {
15
+ res.status(503).json({
16
+ status: 'error',
17
+ timestamp: new Date().toISOString(),
18
+ uptime: process.uptime(),
19
+ database: dbHealth,
20
+ memory: process.memoryUsage(),
21
+ version: process.env.npm_package_version || '1.0.0',
22
+ });
23
+ return;
24
+ }
25
+
26
+ const healthStatus = {
27
+ status: 'ok',
28
+ timestamp: new Date().toISOString(),
29
+ uptime: process.uptime(),
30
+ database: dbHealth,
31
+ memory: process.memoryUsage(),
32
+ version: process.env.npm_package_version || '1.0.0',
33
+ };
34
+
35
+ res.json(healthStatus);
36
+ } catch (error) {
37
+ res.status(503).json({
38
+ status: 'error',
39
+ message: 'Health check failed',
40
+ timestamp: new Date().toISOString(),
41
+ });
42
+ }
43
+ });
44
+
45
+ // API routes
46
+ router.use('/auth', authRoutes);
47
+
48
+ // Root endpoint
49
+ router.get('/', (req: Request, res: Response) => {
50
+ res.json(createSuccessResponse(
51
+ {
52
+ name: 'Arktos Backend API',
53
+ version: process.env.npm_package_version || '1.0.0',
54
+ description: 'Modern Node.js backend boilerplate with TypeScript, Express, JWT, Prisma, PostgreSQL, and Resend',
55
+ documentation: '/api/docs',
56
+ health: '/api/health',
57
+ },
58
+ 'Welcome to Arktos Backend API',
59
+ 'API_INFO'
60
+ ));
61
+ });
62
+
63
+ export default router;
@@ -0,0 +1,139 @@
1
+ import { z } from 'zod';
2
+
3
+ // Environment validation schema
4
+ export const envSchema = z.object({
5
+ NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
6
+ PORT: z.string().default('3001'),
7
+
8
+ // Database
9
+ DATABASE_URL: z.string().min(1, 'Database URL is required'),
10
+ DIRECT_URL: z.string().optional(),
11
+
12
+ // JWT
13
+ JWT_SECRET: z.string().min(32, 'JWT Secret must be at least 32 characters'),
14
+ JWT_REFRESH_SECRET: z.string().min(32, 'JWT Refresh Secret must be at least 32 characters'),
15
+ JWT_EXPIRES_IN: z.string().default('15m'),
16
+ JWT_REFRESH_EXPIRES_IN: z.string().default('7d'),
17
+
18
+ // Email
19
+ RESEND_API_KEY: z.string().optional(),
20
+ FROM_EMAIL: z.string().email().default('noreply@yourapp.com'),
21
+ FROM_NAME: z.string().default('Arktos'),
22
+
23
+ // App URLs
24
+ FRONTEND_URL: z.string().url().default('http://localhost:3000'),
25
+ BACKEND_URL: z.string().url().default('http://localhost:3001'),
26
+ APP_NAME: z.string().default('Arktos'),
27
+
28
+ // Security
29
+ BCRYPT_SALT_ROUNDS: z.string().default('12'),
30
+ PASSWORD_MIN_LENGTH: z.string().default('8'),
31
+ MAX_LOGIN_ATTEMPTS: z.string().default('5'),
32
+ ACCOUNT_LOCK_TIME: z.string().default('900000'),
33
+
34
+ // CORS
35
+ CORS_ORIGIN: z.string().default('http://localhost:3000'),
36
+ CORS_CREDENTIALS: z.string().default('true'),
37
+
38
+ // Rate Limiting
39
+ RATE_LIMIT_WINDOW: z.string().default('900000'),
40
+ RATE_LIMIT_MAX: z.string().default('100'),
41
+
42
+ // Logging
43
+ LOG_LEVEL: z.enum(['error', 'warn', 'info', 'debug']).default('info'),
44
+ LOG_FILE: z.string().default('logs/app.log'),
45
+ });
46
+
47
+ // Authentication schemas
48
+ export const loginSchema = z.object({
49
+ email: z.string().email('Invalid email format'),
50
+ password: z.string().min(1, 'Password is required'),
51
+ });
52
+
53
+ export const registerSchema = z.object({
54
+ email: z.string().email('Invalid email format'),
55
+ password: z
56
+ .string()
57
+ .min(8, 'Password must be at least 8 characters long')
58
+ .regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, 'Password must contain at least one lowercase letter, one uppercase letter, and one number'),
59
+ firstName: z.string().min(1, 'First name is required').max(50, 'First name too long').optional(),
60
+ lastName: z.string().min(1, 'Last name is required').max(50, 'Last name too long').optional(),
61
+ username: z
62
+ .string()
63
+ .min(3, 'Username must be at least 3 characters')
64
+ .max(30, 'Username too long')
65
+ .regex(/^[a-zA-Z0-9_-]+$/, 'Username can only contain letters, numbers, underscores, and hyphens')
66
+ .optional(),
67
+ });
68
+
69
+ export const refreshTokenSchema = z.object({
70
+ refreshToken: z.string().min(1, 'Refresh token is required'),
71
+ });
72
+
73
+ export const resendVerificationSchema = z.object({
74
+ email: z.string().email('Invalid email format'),
75
+ });
76
+
77
+ export const forgotPasswordSchema = z.object({
78
+ email: z.string().email('Invalid email format'),
79
+ });
80
+
81
+ export const resetPasswordSchema = z.object({
82
+ token: z.string().min(1, 'Reset token is required'),
83
+ newPassword: z
84
+ .string()
85
+ .min(8, 'Password must be at least 8 characters long')
86
+ .regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, 'Password must contain at least one lowercase letter, one uppercase letter, and one number'),
87
+ });
88
+
89
+ export const changePasswordSchema = z.object({
90
+ currentPassword: z.string().min(1, 'Current password is required'),
91
+ newPassword: z
92
+ .string()
93
+ .min(8, 'Password must be at least 8 characters long')
94
+ .regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, 'Password must contain at least one lowercase letter, one uppercase letter, and one number'),
95
+ }).refine((data) => {
96
+ return data.newPassword !== data.currentPassword;
97
+ }, {
98
+ message: 'New password must be different from current password',
99
+ path: ['newPassword'],
100
+ });
101
+
102
+ // Profile schemas
103
+ export const updateProfileSchema = z.object({
104
+ firstName: z.string().min(1, 'First name is required').max(50, 'First name too long').optional(),
105
+ lastName: z.string().min(1, 'Last name is required').max(50, 'Last name too long').optional(),
106
+ username: z
107
+ .string()
108
+ .min(3, 'Username must be at least 3 characters')
109
+ .max(30, 'Username too long')
110
+ .regex(/^[a-zA-Z0-9_-]+$/, 'Username can only contain letters, numbers, underscores, and hyphens')
111
+ .optional(),
112
+ avatar: z.string().url('Avatar must be a valid URL').optional(),
113
+ });
114
+
115
+ // Pagination schema
116
+ export const paginationSchema = z.object({
117
+ page: z.string().default('1'),
118
+ limit: z.string().default('10'),
119
+ });
120
+
121
+ // Contact/Support schemas
122
+ export const contactSchema = z.object({
123
+ name: z.string().min(1, 'Name is required').max(100, 'Name too long'),
124
+ email: z.string().email('Invalid email format'),
125
+ subject: z.string().min(1, 'Subject is required').max(200, 'Subject too long'),
126
+ message: z.string().min(10, 'Message too short').max(2000, 'Message too long'),
127
+ });
128
+
129
+ export type EnvSchema = z.infer<typeof envSchema>;
130
+ export type LoginSchema = z.infer<typeof loginSchema>;
131
+ export type RegisterSchema = z.infer<typeof registerSchema>;
132
+ export type RefreshTokenSchema = z.infer<typeof refreshTokenSchema>;
133
+ export type ResendVerificationSchema = z.infer<typeof resendVerificationSchema>;
134
+ export type ForgotPasswordSchema = z.infer<typeof forgotPasswordSchema>;
135
+ export type ResetPasswordSchema = z.infer<typeof resetPasswordSchema>;
136
+ export type ChangePasswordSchema = z.infer<typeof changePasswordSchema>;
137
+ export type UpdateProfileSchema = z.infer<typeof updateProfileSchema>;
138
+ export type PaginationSchema = z.infer<typeof paginationSchema>;
139
+ export type ContactSchema = z.infer<typeof contactSchema>;
@@ -0,0 +1,120 @@
1
+ import { PrismaClient } from '@prisma/client';
2
+ import { DatabaseHealth } from '../types';
3
+ import logger from '../config/logger';
4
+
5
+ export class DatabaseService {
6
+ private static instance: DatabaseService;
7
+ private prisma: PrismaClient;
8
+ private isConnected: boolean = false;
9
+
10
+ private constructor() {
11
+ this.prisma = new PrismaClient({
12
+ log: ['query', 'error', 'info', 'warn'],
13
+ });
14
+
15
+ // Set up event listeners
16
+ this.setupEventListeners();
17
+ }
18
+
19
+ public static getInstance(): DatabaseService {
20
+ if (!DatabaseService.instance) {
21
+ DatabaseService.instance = new DatabaseService();
22
+ }
23
+ return DatabaseService.instance;
24
+ }
25
+
26
+ private setupEventListeners(): void {
27
+ // Event listeners can be added here if needed
28
+ // For now, we're using simple logging configuration
29
+ }
30
+
31
+ public async connect(): Promise<void> {
32
+ try {
33
+ await this.prisma.$connect();
34
+ this.isConnected = true;
35
+ logger.info('✅ Database connected successfully');
36
+ } catch (error) {
37
+ this.isConnected = false;
38
+ logger.error('❌ Database connection failed:', error);
39
+ throw error;
40
+ }
41
+ }
42
+
43
+ public async disconnect(): Promise<void> {
44
+ try {
45
+ await this.prisma.$disconnect();
46
+ this.isConnected = false;
47
+ logger.info('✅ Database disconnected successfully');
48
+ } catch (error) {
49
+ logger.error('❌ Database disconnection failed:', error);
50
+ throw error;
51
+ }
52
+ }
53
+
54
+ public async healthCheck(): Promise<DatabaseHealth> {
55
+ try {
56
+ const startTime = Date.now();
57
+ await this.prisma.$queryRaw`SELECT 1`;
58
+ const responseTime = Date.now() - startTime;
59
+
60
+ return {
61
+ connected: true,
62
+ responseTime,
63
+ };
64
+ } catch (error) {
65
+ logger.error('Database health check failed:', error);
66
+ return {
67
+ connected: false,
68
+ error: error instanceof Error ? error.message : 'Unknown error',
69
+ };
70
+ }
71
+ }
72
+
73
+ public getClient(): PrismaClient {
74
+ return this.prisma;
75
+ }
76
+
77
+ public isConnectedToDatabase(): boolean {
78
+ return this.isConnected;
79
+ }
80
+
81
+ // Graceful shutdown handler
82
+ public async gracefulShutdown(): Promise<void> {
83
+ logger.info('🔄 Starting graceful database shutdown...');
84
+
85
+ try {
86
+ await this.disconnect();
87
+ logger.info('✅ Database shutdown completed');
88
+ } catch (error) {
89
+ logger.error('❌ Error during database shutdown:', error);
90
+ throw error;
91
+ }
92
+ }
93
+ }
94
+
95
+ // Setup process handlers for graceful shutdown
96
+ const dbService = DatabaseService.getInstance();
97
+
98
+ process.on('SIGINT', async () => {
99
+ logger.info('📡 Received SIGINT signal');
100
+ try {
101
+ await dbService.gracefulShutdown();
102
+ process.exit(0);
103
+ } catch (error) {
104
+ logger.error('Failed to shutdown gracefully:', error);
105
+ process.exit(1);
106
+ }
107
+ });
108
+
109
+ process.on('SIGTERM', async () => {
110
+ logger.info('📡 Received SIGTERM signal');
111
+ try {
112
+ await dbService.gracefulShutdown();
113
+ process.exit(0);
114
+ } catch (error) {
115
+ logger.error('Failed to shutdown gracefully:', error);
116
+ process.exit(1);
117
+ }
118
+ });
119
+
120
+ export default DatabaseService;
@@ -0,0 +1,244 @@
1
+ import { Resend } from 'resend';
2
+ import logger from '../config/logger';
3
+
4
+ export class EmailService {
5
+ private static resend = new Resend(process.env.RESEND_API_KEY);
6
+
7
+ private static getFromAddress(): string {
8
+ return process.env.FROM_EMAIL || 'noreply@arktos.com';
9
+ }
10
+
11
+ private static getFromName(): string {
12
+ return process.env.FROM_NAME || 'Arktos';
13
+ }
14
+
15
+ public static async sendEmailVerification(
16
+ email: string,
17
+ verifyToken: string,
18
+ userName: string
19
+ ): Promise<void> {
20
+ try {
21
+ if (!process.env.RESEND_API_KEY) {
22
+ throw new Error('Resend API key is not configured');
23
+ }
24
+
25
+ const verificationUrl = `${process.env.FRONTEND_URL}/verify-email?token=${verifyToken}`;
26
+
27
+ await this.resend.emails.send({
28
+ from: `${this.getFromName()} <${this.getFromAddress()}>`,
29
+ to: [email],
30
+ subject: 'Verify Your Email Address',
31
+ html: `
32
+ <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
33
+ <div style="text-align: center; margin-bottom: 30px;">
34
+ <h1 style="color: #333; margin: 0;">Welcome to ${this.getFromName()}!</h1>
35
+ </div>
36
+
37
+ <div style="background-color: #f8f9fa; padding: 20px; border-radius: 8px; margin-bottom: 20px;">
38
+ <h2 style="color: #333; margin: 0 0 15px 0;">Hi ${userName},</h2>
39
+ <p style="color: #555; line-height: 1.6; margin: 0;">
40
+ Thank you for signing up! To complete your registration and start using your account,
41
+ please verify your email address by clicking the button below.
42
+ </p>
43
+ </div>
44
+
45
+ <div style="text-align: center; margin: 30px 0;">
46
+ <a href="${verificationUrl}"
47
+ style="display: inline-block; padding: 12px 30px; background-color: #007bff; color: white; text-decoration: none; border-radius: 5px; font-weight: bold;">
48
+ Verify Email Address
49
+ </a>
50
+ </div>
51
+
52
+ <div style="background-color: #fff3cd; padding: 15px; border-radius: 5px; border-left: 4px solid #ffc107; margin: 20px 0;">
53
+ <p style="color: #856404; margin: 0; font-size: 14px;">
54
+ <strong>Important:</strong> This verification link will expire in 24 hours for security reasons.
55
+ </p>
56
+ </div>
57
+
58
+ <div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #eee;">
59
+ <p style="color: #666; font-size: 12px; margin: 0;">
60
+ If you didn't create an account with us, you can safely ignore this email.
61
+ <br><br>
62
+ If you're having trouble with the button above, copy and paste this link into your browser:
63
+ <br>
64
+ <a href="${verificationUrl}" style="color: #007bff;">${verificationUrl}</a>
65
+ </p>
66
+ </div>
67
+ </div>
68
+ `,
69
+ });
70
+
71
+ logger.info(`Email verification sent to: ${email}`, {
72
+ recipient: email,
73
+ userName,
74
+ timestamp: new Date().toISOString(),
75
+ });
76
+ } catch (error) {
77
+ logger.error('Failed to send verification email:', error);
78
+ throw new Error('Failed to send verification email');
79
+ }
80
+ }
81
+
82
+ public static async sendPasswordResetEmail(
83
+ email: string,
84
+ resetToken: string,
85
+ userName?: string
86
+ ): Promise<void> {
87
+ try {
88
+ if (!process.env.RESEND_API_KEY) {
89
+ throw new Error('Resend API key is not configured');
90
+ }
91
+
92
+ const resetUrl = `${process.env.FRONTEND_URL}/reset-password?token=${resetToken}`;
93
+
94
+ await this.resend.emails.send({
95
+ from: `${this.getFromName()} <${this.getFromAddress()}>`,
96
+ to: [email],
97
+ subject: 'Reset Your Password',
98
+ html: `
99
+ <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
100
+ <div style="text-align: center; margin-bottom: 30px;">
101
+ <h1 style="color: #333; margin: 0;">Password Reset Request</h1>
102
+ </div>
103
+
104
+ <div style="background-color: #f8f9fa; padding: 20px; border-radius: 8px; margin-bottom: 20px;">
105
+ <h2 style="color: #333; margin: 0 0 15px 0;">${userName ? `Hi ${userName}` : 'Hello'},</h2>
106
+ <p style="color: #555; line-height: 1.6; margin: 0;">
107
+ We received a request to reset the password for your account.
108
+ Click the button below to create a new password.
109
+ </p>
110
+ </div>
111
+
112
+ <div style="text-align: center; margin: 30px 0;">
113
+ <a href="${resetUrl}"
114
+ style="display: inline-block; padding: 12px 30px; background-color: #dc3545; color: white; text-decoration: none; border-radius: 5px; font-weight: bold;">
115
+ Reset Password
116
+ </a>
117
+ </div>
118
+
119
+ <div style="background-color: #f8d7da; padding: 15px; border-radius: 5px; border-left: 4px solid #dc3545; margin: 20px 0;">
120
+ <p style="color: #721c24; margin: 0; font-size: 14px;">
121
+ <strong>Security Notice:</strong> This password reset link will expire in 1 hour for your security.
122
+ </p>
123
+ </div>
124
+
125
+ <div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #eee;">
126
+ <p style="color: #666; font-size: 12px; margin: 0;">
127
+ If you didn't request a password reset, you can safely ignore this email.
128
+ Your password will remain unchanged.
129
+ <br><br>
130
+ If you're having trouble with the button above, copy and paste this link into your browser:
131
+ <br>
132
+ <a href="${resetUrl}" style="color: #dc3545;">${resetUrl}</a>
133
+ </p>
134
+ </div>
135
+ </div>
136
+ `,
137
+ });
138
+
139
+ logger.info(`Password reset email sent to: ${email}`);
140
+ } catch (error) {
141
+ logger.error('Failed to send password reset email:', error);
142
+ throw new Error('Failed to send password reset email');
143
+ }
144
+ }
145
+
146
+ public static async sendWelcomeEmail(
147
+ email: string,
148
+ userName: string
149
+ ): Promise<void> {
150
+ try {
151
+ if (!process.env.RESEND_API_KEY) {
152
+ throw new Error('Resend API key is not configured');
153
+ }
154
+
155
+ await this.resend.emails.send({
156
+ from: `${this.getFromName()} <${this.getFromAddress()}>`,
157
+ to: [email],
158
+ subject: `Welcome to ${this.getFromName()}!`,
159
+ html: `
160
+ <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
161
+ <div style="text-align: center; margin-bottom: 30px;">
162
+ <h1 style="color: #28a745; margin: 0;">Welcome to ${this.getFromName()}!</h1>
163
+ </div>
164
+
165
+ <div style="background-color: #d4edda; padding: 20px; border-radius: 8px; margin-bottom: 20px;">
166
+ <h2 style="color: #155724; margin: 0 0 15px 0;">Hi ${userName},</h2>
167
+ <p style="color: #155724; line-height: 1.6; margin: 0;">
168
+ 🎉 Your email has been successfully verified! Welcome to the ${this.getFromName()} community.
169
+ You can now access all features of your account.
170
+ </p>
171
+ </div>
172
+
173
+ <div style="text-align: center; margin: 30px 0;">
174
+ <a href="${process.env.FRONTEND_URL}/dashboard"
175
+ style="display: inline-block; padding: 12px 30px; background-color: #28a745; color: white; text-decoration: none; border-radius: 5px; font-weight: bold;">
176
+ Get Started
177
+ </a>
178
+ </div>
179
+
180
+ <div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #eee;">
181
+ <p style="color: #666; font-size: 12px; margin: 0;">
182
+ If you have any questions or need help getting started, feel free to contact our support team.
183
+ </p>
184
+ </div>
185
+ </div>
186
+ `,
187
+ });
188
+
189
+ logger.info(`Welcome email sent to: ${email}`);
190
+ } catch (error) {
191
+ logger.error('Failed to send welcome email:', error);
192
+ throw new Error('Failed to send welcome email');
193
+ }
194
+ }
195
+
196
+ public static async sendContactMessage(data: {
197
+ name: string;
198
+ email: string;
199
+ subject: string;
200
+ message: string;
201
+ }): Promise<void> {
202
+ try {
203
+ if (!process.env.RESEND_API_KEY) {
204
+ throw new Error('Resend API key is not configured');
205
+ }
206
+
207
+ const adminEmail = process.env.ADMIN_EMAIL || this.getFromAddress();
208
+
209
+ await this.resend.emails.send({
210
+ from: `${this.getFromName()} <${this.getFromAddress()}>`,
211
+ to: [adminEmail],
212
+ replyTo: data.email,
213
+ subject: `[Contact Form] ${data.subject}`,
214
+ html: `
215
+ <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
216
+ <h2 style="color: #333;">New Contact Form Submission</h2>
217
+
218
+ <div style="background-color: #f8f9fa; padding: 15px; border-radius: 5px; margin: 15px 0;">
219
+ <p style="margin: 5px 0;"><strong>Name:</strong> ${data.name}</p>
220
+ <p style="margin: 5px 0;"><strong>Email:</strong> ${data.email}</p>
221
+ <p style="margin: 5px 0;"><strong>Subject:</strong> ${data.subject}</p>
222
+ </div>
223
+
224
+ <div style="margin: 20px 0;">
225
+ <h3 style="color: #333;">Message:</h3>
226
+ <div style="background-color: #ffffff; padding: 15px; border: 1px solid #ddd; border-radius: 5px;">
227
+ ${data.message.replace(/\n/g, '<br>')}
228
+ </div>
229
+ </div>
230
+
231
+ <p style="color: #666; font-size: 12px;">
232
+ Submitted at: ${new Date().toISOString()}
233
+ </p>
234
+ </div>
235
+ `,
236
+ });
237
+
238
+ logger.info(`Contact message sent from: ${data.email}`);
239
+ } catch (error) {
240
+ logger.error('Failed to send contact message:', error);
241
+ throw new Error('Failed to send contact message');
242
+ }
243
+ }
244
+ }