@adminforth/open-signup 1.0.2
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/custom/SignupPage.vue +278 -0
- package/custom/SignupUnderLogin.vue +13 -0
- package/dist/custom/ResetPassword.vue +295 -0
- package/dist/custom/ResetPasswordUnderLogin.vue +13 -0
- package/dist/custom/SignupPage.vue +278 -0
- package/dist/custom/SignupUnderLogin.vue +13 -0
- package/dist/index.js +269 -0
- package/dist/types.js +1 -0
- package/index.ts +309 -0
- package/package.json +19 -0
- package/tsconfig.json +12 -0
- package/types.ts +68 -0
package/index.ts
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import AdminForth, { AdminForthPlugin, Filters, suggestIfTypo, AdminForthDataTypes } from "adminforth";
|
|
2
|
+
import type { IAdminForth, IHttpServer, AdminForthComponentDeclaration, AdminForthResourceColumn, AdminForthResource, BeforeLoginConfirmationFunction } from "adminforth";
|
|
3
|
+
import type { PluginOptions } from './types.js';
|
|
4
|
+
import { SendEmailCommand, SESClient } from '@aws-sdk/client-ses';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
export default class OpenSignupPlugin extends AdminForthPlugin {
|
|
8
|
+
options: PluginOptions;
|
|
9
|
+
emailField: AdminForthResourceColumn;
|
|
10
|
+
passwordField: AdminForthResourceColumn;
|
|
11
|
+
authResource: AdminForthResource;
|
|
12
|
+
emailConfirmedField?: AdminForthResourceColumn;
|
|
13
|
+
|
|
14
|
+
adminforth: IAdminForth;
|
|
15
|
+
|
|
16
|
+
constructor(options: PluginOptions) {
|
|
17
|
+
super(options, import.meta.url);
|
|
18
|
+
this.options = options;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async modifyResourceConfig(adminforth: IAdminForth, resourceConfig: AdminForthResource) {
|
|
22
|
+
super.modifyResourceConfig(adminforth, resourceConfig);
|
|
23
|
+
|
|
24
|
+
if (!this.options.emailField) {
|
|
25
|
+
throw new Error(`emailField is required and should be a name of field in auth resource`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (this.options.confirmEmails) {
|
|
29
|
+
if (!this.options.confirmEmails.emailProvider) {
|
|
30
|
+
throw new Error(`confirmEmails.emailProvider is required and should be a name of field in auth resource`);
|
|
31
|
+
}
|
|
32
|
+
if (!this.options.confirmEmails.emailConfirmedField) {
|
|
33
|
+
throw new Error(`confirmEmails.emailConfirmedField is required and should be a name of field in auth resource`);
|
|
34
|
+
}
|
|
35
|
+
const emailConfirmedField = this.authResource.columns.find(f => f.name === this.options.confirmEmails.emailConfirmedField);
|
|
36
|
+
if (!emailConfirmedField) {
|
|
37
|
+
const similar = suggestIfTypo(this.authResource.columns.map(f => f.name), this.options.confirmEmails.emailConfirmedField);
|
|
38
|
+
throw new Error(`Field with name ${this.options.confirmEmails.emailConfirmedField} not found in resource ${this.authResource.resourceId}.
|
|
39
|
+
${similar ? `Did you mean ${similar}?` : ''}
|
|
40
|
+
`);
|
|
41
|
+
}
|
|
42
|
+
this.emailConfirmedField = emailConfirmedField;
|
|
43
|
+
if (this.emailConfirmedField.type !== AdminForthDataTypes.BOOLEAN) {
|
|
44
|
+
throw new Error(`Field ${this.emailConfirmedField.name} must be of type boolean`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// find field with name resourceConfig.emailField in adminforth.auth.usersResourceId and show error if it doesn't exist
|
|
49
|
+
const authResource = adminforth.config.resources.find(r => r.resourceId === adminforth.config.auth.usersResourceId);
|
|
50
|
+
if (!authResource) {
|
|
51
|
+
throw new Error(`Resource with id config.auth.usersResourceId=${adminforth.config.auth.usersResourceId} not found`);
|
|
52
|
+
}
|
|
53
|
+
this.authResource = authResource;
|
|
54
|
+
|
|
55
|
+
const emailField = authResource.columns.find(f => f.name === this.options.emailField);
|
|
56
|
+
if (!emailField) {
|
|
57
|
+
const similar = suggestIfTypo(authResource.columns.map(f => f.name), this.options.emailField);
|
|
58
|
+
|
|
59
|
+
throw new Error(`Field with name ${this.options.emailField} not found in resource ${authResource.resourceId}.
|
|
60
|
+
${similar ? `Did you mean ${similar}?` : ''}
|
|
61
|
+
`);
|
|
62
|
+
}
|
|
63
|
+
this.emailField = emailField;
|
|
64
|
+
|
|
65
|
+
if (!this.options.passwordField) {
|
|
66
|
+
throw new Error(`passwordField is required to get password constraints and should be a name of virtual field in auth resource`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const passwordField: AdminForthResourceColumn = authResource.columns.find(f => f.name === this.options.passwordField);
|
|
70
|
+
if (!passwordField) {
|
|
71
|
+
const similar = suggestIfTypo(authResource.columns.map(f => f.name), this.options.passwordField);
|
|
72
|
+
throw new Error(`Field with name ${this.options.passwordField} not found in resource ${authResource.resourceId}.
|
|
73
|
+
${similar ? `Did you mean ${similar}?` : ''}
|
|
74
|
+
`);
|
|
75
|
+
}
|
|
76
|
+
this.passwordField = passwordField;
|
|
77
|
+
|
|
78
|
+
(adminforth.config.customization.loginPageInjections.underInputs as AdminForthComponentDeclaration[]).push({
|
|
79
|
+
file: this.componentPath('SignupUnderLogin.vue'),
|
|
80
|
+
});
|
|
81
|
+
adminforth.config.customization.customPages.push({
|
|
82
|
+
path: '/signup',
|
|
83
|
+
component: {
|
|
84
|
+
file: this.componentPath('SignupPage.vue'),
|
|
85
|
+
meta: {
|
|
86
|
+
customLayout: true,
|
|
87
|
+
pluginInstanceId: this.pluginInstanceId,
|
|
88
|
+
passwordField: {
|
|
89
|
+
minLength: passwordField.minLength,
|
|
90
|
+
maxLength: passwordField.maxLength,
|
|
91
|
+
validation: passwordField.validation
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// for confirmation disable login if email is not confirmed
|
|
98
|
+
if (this.options.confirmEmails) {
|
|
99
|
+
if (!adminforth.config.auth.beforeLoginConfirmation) {
|
|
100
|
+
adminforth.config.auth.beforeLoginConfirmation = [];
|
|
101
|
+
}
|
|
102
|
+
// unshift because if e.g. 2fa set it's hook, this one should be first
|
|
103
|
+
(adminforth.config.auth.beforeLoginConfirmation as BeforeLoginConfirmationFunction[]).unshift(
|
|
104
|
+
async ({ adminUser }) => {
|
|
105
|
+
if (!adminUser.dbUser[this.emailConfirmedField.name]) {
|
|
106
|
+
return { body: { allowedLogin: false }, error: 'You need to confirm your email to be able to login' };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
validateConfigAfterDiscover(adminforth: IAdminForth, resourceConfig: AdminForthResource) {
|
|
114
|
+
// optional method where you can safely check field types after database discovery was performed
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
instanceUniqueRepresentation(pluginOptions: any) : string {
|
|
118
|
+
// optional method to return unique string representation of plugin instance.
|
|
119
|
+
// Needed if plugin can have multiple instances on one resource
|
|
120
|
+
return `single`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async doLogin(email: string, response: any): Promise<{ error?: string; allowedLogin: boolean; redirectTo?: string; }> {
|
|
124
|
+
|
|
125
|
+
const username = email;
|
|
126
|
+
const userRecord = await this.adminforth.resource(this.authResource.resourceId).get(Filters.EQ(this.emailField.name, email));
|
|
127
|
+
const adminUser = {
|
|
128
|
+
dbUser: userRecord,
|
|
129
|
+
pk: userRecord[this.authResource.columns.find((col) => col.primaryKey).name],
|
|
130
|
+
username,
|
|
131
|
+
};
|
|
132
|
+
const toReturn = { allowedLogin: true, error: '' };
|
|
133
|
+
|
|
134
|
+
await this.adminforth.restApi.processLoginCallbacks(adminUser, toReturn, response);
|
|
135
|
+
if (toReturn.allowedLogin) {
|
|
136
|
+
this.adminforth.auth.setAuthCookie({
|
|
137
|
+
response,
|
|
138
|
+
username,
|
|
139
|
+
pk: userRecord[
|
|
140
|
+
this.authResource.columns.find((col) => col.primaryKey).name
|
|
141
|
+
]
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return toReturn;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
setupEndpoints(server: IHttpServer) {
|
|
150
|
+
server.endpoint({
|
|
151
|
+
method: 'POST',
|
|
152
|
+
path: `/plugin/${this.pluginInstanceId}/completeVerifiedSignup`,
|
|
153
|
+
noAuth: true,
|
|
154
|
+
handler: async ({ body, response }) => {
|
|
155
|
+
const { token } = body;
|
|
156
|
+
const { email } = await this.adminforth.auth.verify(token, 'tempVerifyEmailToken');
|
|
157
|
+
if (!email) {
|
|
158
|
+
return { error: 'Invalid token', ok: false };
|
|
159
|
+
}
|
|
160
|
+
const userRecord = await this.adminforth.resource(this.authResource.resourceId).get(Filters.EQ(this.emailField.name, email));
|
|
161
|
+
if (!userRecord) {
|
|
162
|
+
return { error: 'User not found', ok: false };
|
|
163
|
+
}
|
|
164
|
+
await this.adminforth.resource(this.authResource.resourceId).update(userRecord[this.authResource.columns.find((col) => col.primaryKey).name], {
|
|
165
|
+
[this.options.confirmEmails.emailConfirmedField]: true
|
|
166
|
+
});
|
|
167
|
+
return await this.doLogin(email, response);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
server.endpoint({
|
|
172
|
+
method: 'POST',
|
|
173
|
+
path: `/plugin/${this.pluginInstanceId}/signup`,
|
|
174
|
+
noAuth: true,
|
|
175
|
+
handler: async ({ body, response }) => {
|
|
176
|
+
const { email, url, password } = body;
|
|
177
|
+
|
|
178
|
+
// validate email
|
|
179
|
+
if (this.emailField.validation) {
|
|
180
|
+
for (const { regExp, message } of this.emailField.validation) {
|
|
181
|
+
if (!new RegExp(regExp).test(password)) {
|
|
182
|
+
return { error: message, ok: false };
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// validate password
|
|
188
|
+
if (password.length < this.passwordField.minLength) {
|
|
189
|
+
return { error: `Password must be at least ${this.passwordField.minLength} characters long`, ok: false };
|
|
190
|
+
}
|
|
191
|
+
if (password.length > this.passwordField.maxLength) {
|
|
192
|
+
return { error: `Password must be at most ${this.passwordField.maxLength} characters long`, ok: false };
|
|
193
|
+
}
|
|
194
|
+
if (this.passwordField.validation) {
|
|
195
|
+
for (const { regExp, message } of this.passwordField.validation) {
|
|
196
|
+
if (!new RegExp(regExp).test(password)) {
|
|
197
|
+
return { error: message, ok: false };
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// first check again if email already exists
|
|
203
|
+
const existing = await this.adminforth.resource(this.authResource.resourceId).count(Filters.EQ(this.emailField.name, email));
|
|
204
|
+
if (existing > 0) {
|
|
205
|
+
return { error: 'Email already exists', ok: false };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// create user
|
|
209
|
+
const created = await this.adminforth.resource(this.authResource.resourceId).create({
|
|
210
|
+
...(this.options.defaultFieldValues || {}),
|
|
211
|
+
...(this.options.confirmEmails ? { [this.options.confirmEmails.emailConfirmedField]: false } : {}),
|
|
212
|
+
[this.emailField.name]: email,
|
|
213
|
+
[this.options.passwordHashField]: await AdminForth.Utils.generatePasswordHash(password),
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
if (!this.options.confirmEmails) {
|
|
217
|
+
const resp = await this.doLogin(email, response);
|
|
218
|
+
return resp;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// send confirmation email
|
|
222
|
+
|
|
223
|
+
const brandName = this.adminforth.config.customization.brandName;
|
|
224
|
+
|
|
225
|
+
const verifyToken = this.adminforth.auth.issueJWT({email, issuer: brandName }, 'tempVerifyEmailToken', '2h');
|
|
226
|
+
|
|
227
|
+
// send email with AWS SES this.options.providerOptions.AWS_SES
|
|
228
|
+
const ses = new SESClient({
|
|
229
|
+
region: this.options.confirmEmails.providerOptions.AWS_SES.region,
|
|
230
|
+
credentials: {
|
|
231
|
+
accessKeyId: this.options.confirmEmails.providerOptions.AWS_SES.accessKeyId,
|
|
232
|
+
secretAccessKey: this.options.confirmEmails.providerOptions.AWS_SES.secretAccessKey
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
process.env.HEAVY_DEBUG && console.log('🐛Sending reset tok to', verifyToken);
|
|
237
|
+
|
|
238
|
+
const emailCommand = new SendEmailCommand({
|
|
239
|
+
Destination: {
|
|
240
|
+
ToAddresses: [email]
|
|
241
|
+
},
|
|
242
|
+
Message: {
|
|
243
|
+
Body: {
|
|
244
|
+
Text: {
|
|
245
|
+
Charset: "UTF-8",
|
|
246
|
+
Data: `
|
|
247
|
+
Dear user,
|
|
248
|
+
Welcome to ${brandName}!
|
|
249
|
+
|
|
250
|
+
To confirm your email, click the link below:\n\n
|
|
251
|
+
|
|
252
|
+
${url}?verifyToken=${verifyToken}\n\n
|
|
253
|
+
|
|
254
|
+
If you didn't request this, please ignore this email.\n\n
|
|
255
|
+
Link is valid for 2 hours.\n\n
|
|
256
|
+
|
|
257
|
+
Thanks,
|
|
258
|
+
The ${brandName} Team
|
|
259
|
+
|
|
260
|
+
`
|
|
261
|
+
},
|
|
262
|
+
Html: {
|
|
263
|
+
Charset: "UTF-8",
|
|
264
|
+
Data: `
|
|
265
|
+
<html>
|
|
266
|
+
<head></head>
|
|
267
|
+
<body>
|
|
268
|
+
<p>Dear user,</p>
|
|
269
|
+
<p>Welcome to ${brandName}!</p>
|
|
270
|
+
<p>To confirm your email, click the link below:</p>
|
|
271
|
+
<a href="${url}?token=${verifyToken}">Confirm email</a>
|
|
272
|
+
<p>If you didn't request this, please ignore this email.</p>
|
|
273
|
+
<p>Link is valid for 2 hours.</p>
|
|
274
|
+
<p>Thanks,</p>
|
|
275
|
+
<p>The ${brandName} Team</p>
|
|
276
|
+
</body>
|
|
277
|
+
</html>
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
`
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
Subject: {
|
|
284
|
+
Charset: "UTF-8",
|
|
285
|
+
Data: `Signup request at ${brandName}`
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
Source: `${this.options.confirmEmails.sendFrom}`
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
const sRes = await ses.send(emailCommand);
|
|
293
|
+
} catch (e) {
|
|
294
|
+
console.error('Error sending email', e);
|
|
295
|
+
if (process.env.NODE_ENV === 'development') {
|
|
296
|
+
return { error: 'Some thing went wrong, please check the console', ok: false };
|
|
297
|
+
}
|
|
298
|
+
return { error: 'Something went wrong, please contact support', ok: false };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return { ok: true };
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@adminforth/open-signup",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"types": "dist/index.d.ts",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc && rsync -av --exclude 'node_modules' custom dist/ && npm version patch",
|
|
9
|
+
"rollout": "npm run build && npm publish --access public",
|
|
10
|
+
"prepare": "npm link adminforth"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [],
|
|
13
|
+
"author": "",
|
|
14
|
+
"license": "ISC",
|
|
15
|
+
"description": "",
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@aws-sdk/client-ses": "^3.654.0"
|
|
18
|
+
}
|
|
19
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include*/
|
|
4
|
+
"module": "node16", /* Specify what module code is generated. */
|
|
5
|
+
"outDir": "./dist", /* Specify an output folder for all emitted files. */
|
|
6
|
+
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. */
|
|
7
|
+
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
|
8
|
+
"strict": false, /* Enable all strict type-checking options. */
|
|
9
|
+
"skipLibCheck": true, /* Skip type checking all .d.ts files. */
|
|
10
|
+
},
|
|
11
|
+
"exclude": ["node_modules", "dist", "custom"], /* Exclude files from compilation. */
|
|
12
|
+
}
|
package/types.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export interface PluginOptions {
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Field name in auth resource which contains email
|
|
5
|
+
*/
|
|
6
|
+
emailField: string;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* To work properly, this plugin requires a virtual password field in auth resource,
|
|
10
|
+
* which will define any constraints which determine the password strength (regex, minLength).
|
|
11
|
+
* When user will enter passowrd, it will be validated against this field constraints
|
|
12
|
+
* If you don't need it in backoffice itself, you can add it and set showIn = []
|
|
13
|
+
*/
|
|
14
|
+
passwordField: string;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Non-virtual field name in auth resource which contains password hash
|
|
18
|
+
*/
|
|
19
|
+
passwordHashField: string;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Default which will be assigned to user by default
|
|
23
|
+
* e.g. can be used to set default role
|
|
24
|
+
*/
|
|
25
|
+
defaultFieldValues?: {
|
|
26
|
+
[key: string]: any;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* If not set user will not be asked for email confirmation (might be dangerous)
|
|
34
|
+
*/
|
|
35
|
+
confirmEmails?: {
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A boolean field in auth resource which will be set to true when email is confirmed.
|
|
39
|
+
* Must have boolean type.
|
|
40
|
+
*/
|
|
41
|
+
emailConfirmedField: string;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Required to confirm email
|
|
45
|
+
*/
|
|
46
|
+
emailProvider: 'AWS_SES' ;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* From which email to send password reset emails
|
|
50
|
+
* e.g. no-reply@example.com
|
|
51
|
+
* Example.com must be allowed in provider to send emails
|
|
52
|
+
*/
|
|
53
|
+
sendFrom: string;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Options for email provider
|
|
57
|
+
*/
|
|
58
|
+
providerOptions: {
|
|
59
|
+
AWS_SES: {
|
|
60
|
+
region: string;
|
|
61
|
+
accessKeyId: string;
|
|
62
|
+
secretAccessKey: string;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
}
|