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,227 @@
1
+ import jwt from 'jsonwebtoken';
2
+
3
+ export interface JwtPayload {
4
+ userId: string;
5
+ email: string;
6
+ role: string;
7
+ sessionId: string;
8
+ type: 'access' | 'refresh';
9
+ }
10
+
11
+ export class JwtService {
12
+ private static getAccessSecret(): string {
13
+ const secret = process.env.JWT_SECRET;
14
+ if (!secret) {
15
+ throw new Error('JWT_SECRET environment variable is not defined');
16
+ }
17
+ return secret;
18
+ }
19
+
20
+ private static getRefreshSecret(): string {
21
+ const secret = process.env.JWT_REFRESH_SECRET;
22
+ if (!secret) {
23
+ throw new Error('JWT_REFRESH_SECRET environment variable is not defined');
24
+ }
25
+ return secret;
26
+ }
27
+
28
+ private static getAccessExpiresIn(): string {
29
+ return process.env.JWT_EXPIRES_IN || '15m';
30
+ }
31
+
32
+ private static getRefreshExpiresIn(): string {
33
+ return process.env.JWT_REFRESH_EXPIRES_IN || '7d';
34
+ }
35
+
36
+ public static generateAccessToken(
37
+ userId: string,
38
+ email: string,
39
+ role: string,
40
+ sessionId: string
41
+ ): string {
42
+ try {
43
+ const payload: Omit<JwtPayload, 'type'> & { type: 'access' } = {
44
+ userId,
45
+ email,
46
+ role,
47
+ sessionId,
48
+ type: 'access',
49
+ };
50
+
51
+ return jwt.sign(payload, this.getAccessSecret(), {
52
+ expiresIn: this.getAccessExpiresIn(),
53
+ } as jwt.SignOptions);
54
+ } catch (error) {
55
+ throw new Error('Failed to generate access token');
56
+ }
57
+ }
58
+
59
+ public static generateRefreshToken(
60
+ userId: string,
61
+ email: string,
62
+ role: string,
63
+ sessionId: string
64
+ ): string {
65
+ try {
66
+ const payload: Omit<JwtPayload, 'type'> & { type: 'refresh' } = {
67
+ userId,
68
+ email,
69
+ role,
70
+ sessionId,
71
+ type: 'refresh',
72
+ };
73
+
74
+ return jwt.sign(payload, this.getRefreshSecret(), {
75
+ expiresIn: this.getRefreshExpiresIn(),
76
+ } as jwt.SignOptions);
77
+ } catch (error) {
78
+ throw new Error('Failed to generate refresh token');
79
+ }
80
+ }
81
+
82
+ public static verifyAccessToken(token: string): JwtPayload {
83
+ try {
84
+ const decoded = jwt.verify(token, this.getAccessSecret());
85
+ return decoded as JwtPayload;
86
+ } catch (error) {
87
+ if (error instanceof jwt.TokenExpiredError) {
88
+ throw new Error('Access token expired');
89
+ }
90
+ if (error instanceof jwt.JsonWebTokenError) {
91
+ throw new Error('Invalid access token');
92
+ }
93
+ throw new Error('Access token verification failed');
94
+ }
95
+ }
96
+
97
+ public static verifyRefreshToken(token: string): JwtPayload {
98
+ try {
99
+ const decoded = jwt.verify(token, this.getRefreshSecret());
100
+ return decoded as JwtPayload;
101
+ } catch (error) {
102
+ if (error instanceof jwt.TokenExpiredError) {
103
+ throw new Error('Refresh token expired');
104
+ }
105
+ if (error instanceof jwt.JsonWebTokenError) {
106
+ throw new Error('Invalid refresh token');
107
+ }
108
+ throw new Error('Refresh token verification failed');
109
+ }
110
+ }
111
+
112
+ public static generatePasswordResetToken(
113
+ userId: string,
114
+ email: string
115
+ ): string {
116
+ try {
117
+ const payload = {
118
+ userId,
119
+ email,
120
+ type: 'password_reset',
121
+ iat: Math.floor(Date.now() / 1000),
122
+ };
123
+
124
+ return jwt.sign(payload, this.getAccessSecret(), {
125
+ expiresIn: '1h',
126
+ });
127
+ } catch (error) {
128
+ throw new Error('Failed to generate password reset token');
129
+ }
130
+ }
131
+
132
+ public static verifyPasswordResetToken(token: string): {
133
+ userId: string;
134
+ email: string;
135
+ iat: number;
136
+ } {
137
+ try {
138
+ const decoded = jwt.verify(token, this.getAccessSecret()) as any;
139
+
140
+ if (decoded.type !== 'password_reset') {
141
+ throw new Error('Invalid token type');
142
+ }
143
+
144
+ return {
145
+ userId: decoded.userId,
146
+ email: decoded.email,
147
+ iat: decoded.iat,
148
+ };
149
+ } catch (error) {
150
+ if (error instanceof jwt.TokenExpiredError) {
151
+ throw new Error('Password reset token expired');
152
+ }
153
+ if (error instanceof jwt.JsonWebTokenError) {
154
+ throw new Error('Invalid password reset token');
155
+ }
156
+ throw new Error('Password reset token verification failed');
157
+ }
158
+ }
159
+
160
+ public static generateEmailVerifyToken(
161
+ userId: string,
162
+ email: string
163
+ ): string {
164
+ try {
165
+ const payload = {
166
+ userId,
167
+ email,
168
+ type: 'email_verify',
169
+ };
170
+
171
+ return jwt.sign(payload, this.getAccessSecret(), {
172
+ expiresIn: '24h',
173
+ });
174
+ } catch (error) {
175
+ throw new Error('Failed to generate email verification token');
176
+ }
177
+ }
178
+
179
+ public static verifyEmailVerifyToken(token: string): {
180
+ userId: string;
181
+ email: string;
182
+ } {
183
+ try {
184
+ const decoded = jwt.verify(token, this.getAccessSecret()) as any;
185
+
186
+ if (decoded.type !== 'email_verify') {
187
+ throw new Error('Invalid token type');
188
+ }
189
+
190
+ return {
191
+ userId: decoded.userId,
192
+ email: decoded.email,
193
+ };
194
+ } catch (error) {
195
+ if (error instanceof jwt.TokenExpiredError) {
196
+ throw new Error('Email verification token expired');
197
+ }
198
+ if (error instanceof jwt.JsonWebTokenError) {
199
+ throw new Error('Invalid email verification token');
200
+ }
201
+ throw new Error('Email verification token verification failed');
202
+ }
203
+ }
204
+
205
+ public static getExpiresInSeconds(): number {
206
+ try {
207
+ const expiresIn = this.getAccessExpiresIn();
208
+
209
+ if (expiresIn.endsWith('h')) {
210
+ return parseInt(expiresIn.slice(0, -1)) * 3600;
211
+ }
212
+ if (expiresIn.endsWith('m')) {
213
+ return parseInt(expiresIn.slice(0, -1)) * 60;
214
+ }
215
+ if (expiresIn.endsWith('d')) {
216
+ return parseInt(expiresIn.slice(0, -1)) * 24 * 3600;
217
+ }
218
+ if (expiresIn.endsWith('s')) {
219
+ return parseInt(expiresIn.slice(0, -1));
220
+ }
221
+
222
+ return parseInt(expiresIn);
223
+ } catch (error) {
224
+ throw new Error('Failed to calculate token expiration time');
225
+ }
226
+ }
227
+ }
@@ -0,0 +1,19 @@
1
+ import { User } from '@prisma/client';
2
+
3
+ declare global {
4
+ namespace Express {
5
+ interface Request {
6
+ user?: User;
7
+ file?: Express.Multer.File;
8
+ files?: Express.Multer.File[];
9
+ sessionId?: string;
10
+ rateLimitInfo?: {
11
+ limit: number;
12
+ remaining: number;
13
+ resetTime: Date;
14
+ };
15
+ }
16
+ }
17
+ }
18
+
19
+ export {};
@@ -0,0 +1,156 @@
1
+ import { Request } from 'express';
2
+ import type { User, Role } from '@prisma/client';
3
+
4
+ // API Response Types
5
+ export interface ApiResponse<T = any> {
6
+ success: boolean;
7
+ message?: string;
8
+ data?: T;
9
+ error?: string;
10
+ code?: string;
11
+ timestamp?: string;
12
+ path?: string;
13
+ method?: string;
14
+ }
15
+
16
+ export interface PaginatedResponse<T> extends ApiResponse<T[]> {
17
+ data: T[];
18
+ pagination: {
19
+ total: number;
20
+ page: number;
21
+ limit: number;
22
+ totalPages: number;
23
+ hasNext: boolean;
24
+ hasPrev: boolean;
25
+ };
26
+ }
27
+
28
+ // Authentication Types
29
+ export interface LoginRequest {
30
+ email: string;
31
+ password: string;
32
+ }
33
+
34
+ export interface RegisterRequest {
35
+ email: string;
36
+ password: string;
37
+ firstName?: string;
38
+ lastName?: string;
39
+ username?: string;
40
+ }
41
+
42
+ export interface AuthResponse {
43
+ user: Omit<User, 'password'>;
44
+ tokens: {
45
+ accessToken: string;
46
+ refreshToken: string;
47
+ };
48
+ }
49
+
50
+ export interface JwtPayload {
51
+ userId: string;
52
+ email: string;
53
+ role: Role;
54
+ iat?: number;
55
+ exp?: number;
56
+ }
57
+
58
+ // Express Request Extensions
59
+ export interface AuthenticatedRequest extends Omit<Request, 'user'> {
60
+ user?: {
61
+ id: string;
62
+ email: string;
63
+ username: string | null;
64
+ firstName: string | null;
65
+ lastName: string | null;
66
+ avatar: string | null;
67
+ role: Role;
68
+ isActive: boolean;
69
+ isEmailVerified: boolean;
70
+ emailVerifiedAt: Date | null;
71
+ lastLoginAt: Date | null;
72
+ createdAt: Date;
73
+ updatedAt: Date;
74
+ };
75
+ }
76
+
77
+ // Database Types
78
+ export interface DatabaseHealth {
79
+ connected: boolean;
80
+ responseTime?: number;
81
+ error?: string;
82
+ }
83
+
84
+ // File Upload Types
85
+ export interface FileUploadOptions {
86
+ maxSize?: number;
87
+ allowedTypes?: string[];
88
+ destination?: string;
89
+ filename?: string;
90
+ }
91
+
92
+ export interface UploadedFile {
93
+ originalname: string;
94
+ filename: string;
95
+ mimetype: string;
96
+ size: number;
97
+ path: string;
98
+ url?: string;
99
+ }
100
+
101
+ // Error Types
102
+ export interface AppError extends Error {
103
+ statusCode: number;
104
+ code?: string;
105
+ isOperational: boolean;
106
+ }
107
+
108
+ // Email Types
109
+ export interface EmailTemplate {
110
+ subject: string;
111
+ html: string;
112
+ text?: string;
113
+ }
114
+
115
+ export interface EmailContext {
116
+ [key: string]: any;
117
+ }
118
+
119
+ // Validation Types
120
+ export interface ValidationError {
121
+ field: string;
122
+ message: string;
123
+ value?: any;
124
+ }
125
+
126
+ // Service Types
127
+ export interface ServiceResponse<T = any> {
128
+ success: boolean;
129
+ data?: T;
130
+ error?: string;
131
+ code?: string;
132
+ }
133
+
134
+ // Cache Types
135
+ export interface CacheOptions {
136
+ ttl?: number; // Time to live in seconds
137
+ prefix?: string;
138
+ }
139
+
140
+ // Rate Limiting Types
141
+ export interface RateLimitConfig {
142
+ windowMs: number;
143
+ max: number;
144
+ message?: string;
145
+ standardHeaders?: boolean;
146
+ legacyHeaders?: boolean;
147
+ }
148
+
149
+ export type LogLevel = 'error' | 'warn' | 'info' | 'debug';
150
+
151
+ export interface LoggerConfig {
152
+ level: LogLevel;
153
+ format: string;
154
+ transports: string[];
155
+ meta?: boolean;
156
+ }
@@ -0,0 +1,55 @@
1
+ import { ApiResponse, PaginatedResponse } from '../types';
2
+
3
+ export const createSuccessResponse = <T = any>(
4
+ data?: T,
5
+ message?: string,
6
+ code?: string
7
+ ): ApiResponse<T> => {
8
+ return {
9
+ success: true,
10
+ message,
11
+ data,
12
+ code,
13
+ timestamp: new Date().toISOString(),
14
+ };
15
+ };
16
+
17
+ export const createErrorResponse = (
18
+ message: string,
19
+ code?: string,
20
+ additionalData?: any
21
+ ): ApiResponse => {
22
+ return {
23
+ success: false,
24
+ message,
25
+ error: message,
26
+ code,
27
+ timestamp: new Date().toISOString(),
28
+ ...additionalData,
29
+ };
30
+ };
31
+
32
+ export const createPaginatedResponse = <T>(
33
+ data: T[],
34
+ total: number,
35
+ page: number,
36
+ limit: number,
37
+ message?: string
38
+ ): PaginatedResponse<T> => {
39
+ const totalPages = Math.ceil(total / limit);
40
+
41
+ return {
42
+ success: true,
43
+ message,
44
+ data,
45
+ pagination: {
46
+ total,
47
+ page,
48
+ limit,
49
+ totalPages,
50
+ hasNext: page < totalPages,
51
+ hasPrev: page > 1,
52
+ },
53
+ timestamp: new Date().toISOString(),
54
+ };
55
+ };
@@ -0,0 +1,87 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Notification - {{appName}}</title>
7
+ <style>
8
+ body {
9
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
10
+ line-height: 1.6;
11
+ color: #333;
12
+ background-color: #f4f4f4;
13
+ margin: 0;
14
+ padding: 0;
15
+ }
16
+ .container {
17
+ max-width: 600px;
18
+ margin: 20px auto;
19
+ background: white;
20
+ padding: 40px;
21
+ border-radius: 10px;
22
+ box-shadow: 0 0 20px rgba(0,0,0,0.1);
23
+ }
24
+ .header {
25
+ text-align: center;
26
+ margin-bottom: 30px;
27
+ }
28
+ .logo {
29
+ font-size: 24px;
30
+ font-weight: bold;
31
+ color: #3b82f6;
32
+ margin-bottom: 10px;
33
+ }
34
+ h1 {
35
+ color: #1f2937;
36
+ margin-bottom: 20px;
37
+ font-size: 24px;
38
+ }
39
+ .message-box {
40
+ background-color: #f8fafc;
41
+ border: 1px solid #e2e8f0;
42
+ border-radius: 8px;
43
+ padding: 20px;
44
+ margin: 20px 0;
45
+ }
46
+ .footer {
47
+ margin-top: 40px;
48
+ text-align: center;
49
+ color: #6b7280;
50
+ font-size: 14px;
51
+ }
52
+ .footer a {
53
+ color: #3b82f6;
54
+ text-decoration: none;
55
+ }
56
+ .divider {
57
+ border-top: 1px solid #e5e7eb;
58
+ margin: 30px 0;
59
+ }
60
+ </style>
61
+ </head>
62
+ <body>
63
+ <div class="container">
64
+ <div class="header">
65
+ <div class="logo">{{appName}}</div>
66
+ </div>
67
+
68
+ <h1>Hi {{firstName}},</h1>
69
+
70
+ <div class="message-box">
71
+ {{message}}
72
+ </div>
73
+
74
+ <p>If you have any questions about this notification, please don't hesitate to contact our support team.</p>
75
+
76
+ <div class="divider"></div>
77
+
78
+ <div class="footer">
79
+ <p>Best regards,</p>
80
+ <p>The {{appName}} Team</p>
81
+ <br>
82
+ <p>Need help? Contact our support team or visit our <a href="{{frontendUrl}}">help center</a>.</p>
83
+ <p>&copy; 2025 {{appName}}. All rights reserved.</p>
84
+ </div>
85
+ </div>
86
+ </body>
87
+ </html>
@@ -0,0 +1,118 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Password Reset - {{appName}}</title>
7
+ <style>
8
+ body {
9
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
10
+ line-height: 1.6;
11
+ color: #333;
12
+ background-color: #f4f4f4;
13
+ margin: 0;
14
+ padding: 0;
15
+ }
16
+ .container {
17
+ max-width: 600px;
18
+ margin: 20px auto;
19
+ background: white;
20
+ padding: 40px;
21
+ border-radius: 10px;
22
+ box-shadow: 0 0 20px rgba(0,0,0,0.1);
23
+ }
24
+ .header {
25
+ text-align: center;
26
+ margin-bottom: 30px;
27
+ }
28
+ .logo {
29
+ font-size: 24px;
30
+ font-weight: bold;
31
+ color: #dc2626;
32
+ margin-bottom: 10px;
33
+ }
34
+ h1 {
35
+ color: #1f2937;
36
+ margin-bottom: 20px;
37
+ font-size: 24px;
38
+ }
39
+ .button {
40
+ display: inline-block;
41
+ padding: 12px 30px;
42
+ background-color: #dc2626;
43
+ color: white;
44
+ text-decoration: none;
45
+ border-radius: 6px;
46
+ font-weight: bold;
47
+ margin: 20px 0;
48
+ transition: background-color 0.3s ease;
49
+ }
50
+ .button:hover {
51
+ background-color: #b91c1c;
52
+ }
53
+ .footer {
54
+ margin-top: 40px;
55
+ text-align: center;
56
+ color: #6b7280;
57
+ font-size: 14px;
58
+ }
59
+ .footer a {
60
+ color: #dc2626;
61
+ text-decoration: none;
62
+ }
63
+ .divider {
64
+ border-top: 1px solid #e5e7eb;
65
+ margin: 30px 0;
66
+ }
67
+ .security-note {
68
+ background-color: #fef2f2;
69
+ border-left: 4px solid #dc2626;
70
+ padding: 15px;
71
+ margin: 20px 0;
72
+ font-size: 14px;
73
+ }
74
+ .warning {
75
+ background-color: #fffbeb;
76
+ border-left: 4px solid #f59e0b;
77
+ padding: 15px;
78
+ margin: 20px 0;
79
+ font-size: 14px;
80
+ }
81
+ </style>
82
+ </head>
83
+ <body>
84
+ <div class="container">
85
+ <div class="header">
86
+ <div class="logo">{{appName}}</div>
87
+ </div>
88
+
89
+ <h1>Hi {{firstName}},</h1>
90
+
91
+ <p>We received a request to reset the password for your {{appName}} account.</p>
92
+
93
+ <p>If you requested this password reset, please click the button below to create a new password:</p>
94
+
95
+ <div style="text-align: center;">
96
+ <a href="{{resetUrl}}" class="button">Reset Password</a>
97
+ </div>
98
+
99
+ <div class="security-note">
100
+ <strong>Security Notice:</strong> This password reset link will expire in 1 hour for your security. If you didn't request a password reset, please ignore this email and your password will remain unchanged.
101
+ </div>
102
+
103
+ <div class="warning">
104
+ <strong>Important:</strong> If you suspect someone else requested this password reset, please contact our support team immediately and consider changing your password through the app directly.
105
+ </div>
106
+
107
+ <p>If the button doesn't work, you can also copy and paste the following link into your browser:</p>
108
+ <p style="word-break: break-all; color: #dc2626;">{{resetUrl}}</p>
109
+
110
+ <div class="divider"></div>
111
+
112
+ <div class="footer">
113
+ <p>Need help? Contact our support team or visit our <a href="{{frontendUrl}}">help center</a>.</p>
114
+ <p>&copy; 2025 {{appName}}. All rights reserved.</p>
115
+ </div>
116
+ </div>
117
+ </body>
118
+ </html>