create-arktos 1.4.0 → 1.5.1
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/bin/cli.js +19 -75
- package/eslint.config.js +40 -25
- package/package.json +1 -4
- package/eslint.config.js.template +0 -31
- package/src-js/app.js +0 -97
- package/src-js/config/env.validation.js +0 -15
- package/src-js/config/logger.js +0 -76
- package/src-js/constants/errorCodes.js +0 -57
- package/src-js/constants/messages.js +0 -240
- package/src-js/controllers/auth.controller.js +0 -533
- package/src-js/middleware/index.js +0 -284
- package/src-js/routes/auth.routes.js +0 -28
- package/src-js/routes/index.js +0 -18
- package/src-js/schemas/index.js +0 -60
- package/src-js/services/database.service.js +0 -89
- package/src-js/services/email.service.js +0 -161
- package/src-js/services/jwt.service.js +0 -177
- package/src-js/utils/response.js +0 -53
- package/src-js/views/emails/notification.html +0 -87
- package/src-js/views/emails/resetPassword.html +0 -118
- package/src-js/views/emails/verification.html +0 -107
- package/src-js/views/emails/welcome.html +0 -113
- package/template.js.package.json +0 -77
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
import jwt from 'jsonwebtoken';
|
|
2
|
-
import crypto from 'crypto';
|
|
3
|
-
import logger from '../config/logger.js';
|
|
4
|
-
|
|
5
|
-
class JWTService {
|
|
6
|
-
constructor() {
|
|
7
|
-
this.accessTokenSecret = process.env.JWT_SECRET;
|
|
8
|
-
this.refreshTokenSecret = process.env.JWT_REFRESH_SECRET;
|
|
9
|
-
this.accessTokenExpiry = process.env.JWT_ACCESS_TOKEN_EXPIRY || '15m';
|
|
10
|
-
this.refreshTokenExpiry = process.env.JWT_REFRESH_TOKEN_EXPIRY || '7d';
|
|
11
|
-
|
|
12
|
-
if (!this.accessTokenSecret || !this.refreshTokenSecret) {
|
|
13
|
-
throw new Error('JWT secrets are required');
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// Generate access token
|
|
18
|
-
generateAccessToken(payload) {
|
|
19
|
-
try {
|
|
20
|
-
return jwt.sign(payload, this.accessTokenSecret, {
|
|
21
|
-
expiresIn: this.accessTokenExpiry,
|
|
22
|
-
issuer: 'arktos-backend',
|
|
23
|
-
audience: 'arktos-client',
|
|
24
|
-
});
|
|
25
|
-
} catch (error) {
|
|
26
|
-
logger.error('Failed to generate access token:', error);
|
|
27
|
-
throw error;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Generate refresh token
|
|
32
|
-
generateRefreshToken(payload) {
|
|
33
|
-
try {
|
|
34
|
-
return jwt.sign(payload, this.refreshTokenSecret, {
|
|
35
|
-
expiresIn: this.refreshTokenExpiry,
|
|
36
|
-
issuer: 'arktos-backend',
|
|
37
|
-
audience: 'arktos-client',
|
|
38
|
-
});
|
|
39
|
-
} catch (error) {
|
|
40
|
-
logger.error('Failed to generate refresh token:', error);
|
|
41
|
-
throw error;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Generate both tokens
|
|
46
|
-
generateTokenPair(payload) {
|
|
47
|
-
return {
|
|
48
|
-
accessToken: this.generateAccessToken(payload),
|
|
49
|
-
refreshToken: this.generateRefreshToken(payload),
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Verify access token
|
|
54
|
-
verifyAccessToken(token) {
|
|
55
|
-
try {
|
|
56
|
-
return jwt.verify(token, this.accessTokenSecret, {
|
|
57
|
-
issuer: 'arktos-backend',
|
|
58
|
-
audience: 'arktos-client',
|
|
59
|
-
});
|
|
60
|
-
} catch (error) {
|
|
61
|
-
logger.error('Failed to verify access token:', error);
|
|
62
|
-
throw error;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Verify refresh token
|
|
67
|
-
verifyRefreshToken(token) {
|
|
68
|
-
try {
|
|
69
|
-
return jwt.verify(token, this.refreshTokenSecret, {
|
|
70
|
-
issuer: 'arktos-backend',
|
|
71
|
-
audience: 'arktos-client',
|
|
72
|
-
});
|
|
73
|
-
} catch (error) {
|
|
74
|
-
logger.error('Failed to verify refresh token:', error);
|
|
75
|
-
throw error;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Decode token without verification (for debugging)
|
|
80
|
-
decodeToken(token) {
|
|
81
|
-
try {
|
|
82
|
-
return jwt.decode(token, { complete: true });
|
|
83
|
-
} catch (error) {
|
|
84
|
-
logger.error('Failed to decode token:', error);
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Get token expiration time
|
|
90
|
-
getTokenExpiration(token) {
|
|
91
|
-
try {
|
|
92
|
-
const decoded = this.decodeToken(token);
|
|
93
|
-
return decoded?.payload?.exp ? new Date(decoded.payload.exp * 1000) : null;
|
|
94
|
-
} catch (error) {
|
|
95
|
-
logger.error('Failed to get token expiration:', error);
|
|
96
|
-
return null;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Check if token is expired
|
|
101
|
-
isTokenExpired(token) {
|
|
102
|
-
try {
|
|
103
|
-
const expiration = this.getTokenExpiration(token);
|
|
104
|
-
return expiration ? expiration < new Date() : true;
|
|
105
|
-
} catch (error) {
|
|
106
|
-
logger.error('Failed to check token expiration:', error);
|
|
107
|
-
return true;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Generate random token (for email verification, password reset, etc.)
|
|
112
|
-
generateRandomToken(length = 32) {
|
|
113
|
-
return crypto.randomBytes(length).toString('hex');
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Generate secure random string
|
|
117
|
-
generateSecureString(length = 16) {
|
|
118
|
-
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
119
|
-
let result = '';
|
|
120
|
-
|
|
121
|
-
for (let i = 0; i < length; i++) {
|
|
122
|
-
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
return result;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Hash token for storage (one-way)
|
|
129
|
-
hashToken(token) {
|
|
130
|
-
return crypto.createHash('sha256').update(token).digest('hex');
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Create token with expiration
|
|
134
|
-
createTokenWithExpiry(payload, secret, expiresIn) {
|
|
135
|
-
try {
|
|
136
|
-
return jwt.sign(payload, secret, {
|
|
137
|
-
expiresIn,
|
|
138
|
-
issuer: 'arktos-backend',
|
|
139
|
-
audience: 'arktos-client',
|
|
140
|
-
});
|
|
141
|
-
} catch (error) {
|
|
142
|
-
logger.error('Failed to create token with expiry:', error);
|
|
143
|
-
throw error;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Verify token with custom secret
|
|
148
|
-
verifyTokenWithSecret(token, secret) {
|
|
149
|
-
try {
|
|
150
|
-
return jwt.verify(token, secret, {
|
|
151
|
-
issuer: 'arktos-backend',
|
|
152
|
-
audience: 'arktos-client',
|
|
153
|
-
});
|
|
154
|
-
} catch (error) {
|
|
155
|
-
logger.error('Failed to verify token with custom secret:', error);
|
|
156
|
-
throw error;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Extract user ID from token
|
|
161
|
-
extractUserIdFromToken(token) {
|
|
162
|
-
try {
|
|
163
|
-
const decoded = this.decodeToken(token);
|
|
164
|
-
return decoded?.payload?.userId || null;
|
|
165
|
-
} catch (error) {
|
|
166
|
-
logger.error('Failed to extract user ID from token:', error);
|
|
167
|
-
return null;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Create short-lived token (for sensitive operations)
|
|
172
|
-
createShortLivedToken(payload, expiresIn = '5m') {
|
|
173
|
-
return this.createTokenWithExpiry(payload, this.accessTokenSecret, expiresIn);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
export default new JWTService();
|
package/src-js/utils/response.js
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
export const createSuccessResponse = (
|
|
2
|
-
data,
|
|
3
|
-
message,
|
|
4
|
-
code
|
|
5
|
-
) => {
|
|
6
|
-
return {
|
|
7
|
-
success: true,
|
|
8
|
-
message,
|
|
9
|
-
data,
|
|
10
|
-
code,
|
|
11
|
-
timestamp: new Date().toISOString(),
|
|
12
|
-
};
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export const createErrorResponse = (
|
|
16
|
-
message,
|
|
17
|
-
code,
|
|
18
|
-
additionalData
|
|
19
|
-
) => {
|
|
20
|
-
return {
|
|
21
|
-
success: false,
|
|
22
|
-
message,
|
|
23
|
-
error: message,
|
|
24
|
-
code,
|
|
25
|
-
timestamp: new Date().toISOString(),
|
|
26
|
-
...additionalData,
|
|
27
|
-
};
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
export const createPaginatedResponse = (
|
|
31
|
-
data,
|
|
32
|
-
total,
|
|
33
|
-
page,
|
|
34
|
-
limit,
|
|
35
|
-
message
|
|
36
|
-
) => {
|
|
37
|
-
const totalPages = Math.ceil(total / limit);
|
|
38
|
-
|
|
39
|
-
return {
|
|
40
|
-
success: true,
|
|
41
|
-
message,
|
|
42
|
-
data,
|
|
43
|
-
pagination: {
|
|
44
|
-
total,
|
|
45
|
-
page,
|
|
46
|
-
limit,
|
|
47
|
-
totalPages,
|
|
48
|
-
hasNext: page < totalPages,
|
|
49
|
-
hasPrev: page > 1,
|
|
50
|
-
},
|
|
51
|
-
timestamp: new Date().toISOString(),
|
|
52
|
-
};
|
|
53
|
-
};
|
|
@@ -1,87 +0,0 @@
|
|
|
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>© 2025 {{appName}}. All rights reserved.</p>
|
|
84
|
-
</div>
|
|
85
|
-
</div>
|
|
86
|
-
</body>
|
|
87
|
-
</html>
|
|
@@ -1,118 +0,0 @@
|
|
|
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>© 2025 {{appName}}. All rights reserved.</p>
|
|
115
|
-
</div>
|
|
116
|
-
</div>
|
|
117
|
-
</body>
|
|
118
|
-
</html>
|
|
@@ -1,107 +0,0 @@
|
|
|
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>Email Verification - {{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: #4f46e5;
|
|
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: #4f46e5;
|
|
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: #4338ca;
|
|
52
|
-
}
|
|
53
|
-
.footer {
|
|
54
|
-
margin-top: 40px;
|
|
55
|
-
text-align: center;
|
|
56
|
-
color: #6b7280;
|
|
57
|
-
font-size: 14px;
|
|
58
|
-
}
|
|
59
|
-
.footer a {
|
|
60
|
-
color: #4f46e5;
|
|
61
|
-
text-decoration: none;
|
|
62
|
-
}
|
|
63
|
-
.divider {
|
|
64
|
-
border-top: 1px solid #e5e7eb;
|
|
65
|
-
margin: 30px 0;
|
|
66
|
-
}
|
|
67
|
-
.security-note {
|
|
68
|
-
background-color: #fef3cd;
|
|
69
|
-
border-left: 4px solid #f59e0b;
|
|
70
|
-
padding: 15px;
|
|
71
|
-
margin: 20px 0;
|
|
72
|
-
font-size: 14px;
|
|
73
|
-
}
|
|
74
|
-
</style>
|
|
75
|
-
</head>
|
|
76
|
-
<body>
|
|
77
|
-
<div class="container">
|
|
78
|
-
<div class="header">
|
|
79
|
-
<div class="logo">{{appName}}</div>
|
|
80
|
-
</div>
|
|
81
|
-
|
|
82
|
-
<h1>Hi {{firstName}},</h1>
|
|
83
|
-
|
|
84
|
-
<p>Welcome to {{appName}}! We're excited to have you on board.</p>
|
|
85
|
-
|
|
86
|
-
<p>To complete your account setup and start using all our features, please verify your email address by clicking the button below:</p>
|
|
87
|
-
|
|
88
|
-
<div style="text-align: center;">
|
|
89
|
-
<a href="{{verificationUrl}}" class="button">Verify Email Address</a>
|
|
90
|
-
</div>
|
|
91
|
-
|
|
92
|
-
<div class="security-note">
|
|
93
|
-
<strong>Security Note:</strong> This verification link will expire in 24 hours for your security. If you didn't create an account with {{appName}}, you can safely ignore this email.
|
|
94
|
-
</div>
|
|
95
|
-
|
|
96
|
-
<p>If the button doesn't work, you can also copy and paste the following link into your browser:</p>
|
|
97
|
-
<p style="word-break: break-all; color: #4f46e5;">{{verificationUrl}}</p>
|
|
98
|
-
|
|
99
|
-
<div class="divider"></div>
|
|
100
|
-
|
|
101
|
-
<div class="footer">
|
|
102
|
-
<p>Need help? Contact our support team or visit our <a href="{{frontendUrl}}">help center</a>.</p>
|
|
103
|
-
<p>© 2025 {{appName}}. All rights reserved.</p>
|
|
104
|
-
</div>
|
|
105
|
-
</div>
|
|
106
|
-
</body>
|
|
107
|
-
</html>
|
|
@@ -1,113 +0,0 @@
|
|
|
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>Welcome to {{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: #10b981;
|
|
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: #10b981;
|
|
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: #059669;
|
|
52
|
-
}
|
|
53
|
-
.footer {
|
|
54
|
-
margin-top: 40px;
|
|
55
|
-
text-align: center;
|
|
56
|
-
color: #6b7280;
|
|
57
|
-
font-size: 14px;
|
|
58
|
-
}
|
|
59
|
-
.footer a {
|
|
60
|
-
color: #10b981;
|
|
61
|
-
text-decoration: none;
|
|
62
|
-
}
|
|
63
|
-
.divider {
|
|
64
|
-
border-top: 1px solid #e5e7eb;
|
|
65
|
-
margin: 30px 0;
|
|
66
|
-
}
|
|
67
|
-
.welcome-box {
|
|
68
|
-
background-color: #f0fdf4;
|
|
69
|
-
border-left: 4px solid #10b981;
|
|
70
|
-
padding: 20px;
|
|
71
|
-
margin: 20px 0;
|
|
72
|
-
}
|
|
73
|
-
</style>
|
|
74
|
-
</head>
|
|
75
|
-
<body>
|
|
76
|
-
<div class="container">
|
|
77
|
-
<div class="header">
|
|
78
|
-
<div class="logo">{{appName}}</div>
|
|
79
|
-
</div>
|
|
80
|
-
|
|
81
|
-
<h1>Welcome, {{firstName}}! 🎉</h1>
|
|
82
|
-
|
|
83
|
-
<div class="welcome-box">
|
|
84
|
-
<p><strong>Congratulations!</strong> Your account has been successfully verified and you're now part of the {{appName}} community.</p>
|
|
85
|
-
</div>
|
|
86
|
-
|
|
87
|
-
<p>We're thrilled to have you on board! Here's what you can do next:</p>
|
|
88
|
-
|
|
89
|
-
<ul>
|
|
90
|
-
<li>Complete your profile to get the most out of our services</li>
|
|
91
|
-
<li>Explore our features and tools</li>
|
|
92
|
-
<li>Connect with other users in your area</li>
|
|
93
|
-
<li>Start using our platform to its full potential</li>
|
|
94
|
-
</ul>
|
|
95
|
-
|
|
96
|
-
<div style="text-align: center;">
|
|
97
|
-
<a href="{{loginUrl}}" class="button">Get Started</a>
|
|
98
|
-
</div>
|
|
99
|
-
|
|
100
|
-
<p>If you have any questions or need help getting started, don't hesitate to reach out to our support team. We're here to help!</p>
|
|
101
|
-
|
|
102
|
-
<div class="divider"></div>
|
|
103
|
-
|
|
104
|
-
<div class="footer">
|
|
105
|
-
<p>Welcome aboard!</p>
|
|
106
|
-
<p>The {{appName}} Team</p>
|
|
107
|
-
<br>
|
|
108
|
-
<p>Need help? Contact our support team or visit our <a href="{{frontendUrl}}">help center</a>.</p>
|
|
109
|
-
<p>© 2025 {{appName}}. All rights reserved.</p>
|
|
110
|
-
</div>
|
|
111
|
-
</div>
|
|
112
|
-
</body>
|
|
113
|
-
</html>
|