main
vue 426 lines 11 KB
Raw
1 <template>
2 <div>
3 <n-spin :show="loading">
4 <n-form ref="formRef" :model :rules>
5 <n-steps :current="wizardCurrent" vertical>
6 <n-step title="Account">
7 <div v-show="wizardCurrent === 1" class="pt-3">
8 <n-form-item path="email" label="Email" first>
9 <n-input
10 v-model:value="model.email"
11 size="large"
12 placeholder="Email..."
13 @keydown.enter="signUp"
14 />
15 </n-form-item>
16 <n-form-item path="password" label="Password" first>
17 <n-input
18 v-model:value="model.password"
19 type="password"
20 size="large"
21 show-password-on="click"
22 placeholder="At least 8 characters"
23 @keydown.enter="signUp"
24 />
25 </n-form-item>
26 <n-form-item path="confirmPassword" label="Confirm Password" first>
27 <n-input
28 v-model:value="model.confirmPassword"
29 type="password"
30 :disabled="!model.password"
31 size="large"
32 show-password-on="click"
33 placeholder="At least 8 characters"
34 @keydown.enter="signUp"
35 />
36 </n-form-item>
37 <div class="mt-3 flex items-center justify-between gap-3">
38 <n-button secondary size="large" @click="reset(1)">Reset</n-button>
39
40 <n-button
41 type="primary"
42 size="large"
43 icon-placement="right"
44 :disabled="!accountStepValid"
45 @click="wizardCurrent = 2"
46 >
47 <template #icon>
48 <Icon :name="ArrowRightIcon" />
49 </template>
50 Next
51 </n-button>
52 </div>
53 </div>
54 </n-step>
55 <n-step title="Details">
56 <div v-show="wizardCurrent === 2" class="pt-3">
57 <n-form-item path="username" label="Username" first>
58 <n-input
59 v-model:value="model.username"
60 size="large"
61 placeholder="Username..."
62 @keydown.enter="signUp"
63 />
64 </n-form-item>
65 <n-form-item path="role" label="Role">
66 <n-select
67 v-model:value="model.role"
68 :options="roleOptions"
69 placeholder="Choose a role"
70 size="large"
71 to="body"
72 @keydown.enter="signUp"
73 />
74 </n-form-item>
75
76 <!--
77 <div class="propic flex gap-5 mb-5">
78 <div class="avatar">
79 <n-avatar :size="80" :src="model.propic" round />
80 </div>
81 <div class="buttons flex flex-col gap-2 justify-center">
82 <ImageCropper
83 v-slot="{ openCropper }"
84 @crop="setCroppedImage"
85 shape="circle"
86 :placeholder="'Select your profile picture'"
87 >
88 <n-button type="primary" @click="openCropper()" size="small">
89 <template #icon>
90 <Icon :name="ImageIcon"/>
91 </template>
92 {{ model.propic ? "Edit" : "Add" }} Photo
93 </n-button>
94 </ImageCropper>
95 <n-button @click="model.propic = ''" v-if="model.propic" size="small">
96 <template #icon>
97 <Icon :name="RemoveImageIcon"/>
98 </template>
99 Remove Photo
100 </n-button>
101 </div>
102 </div>
103 <n-form-item path="firstName" label="First Name">
104 <n-input
105 v-model:value="model.firstName"
106 @keydown.enter="signUp"
107 size="large"
108 placeholder="First Name..."
109 />
110 </n-form-item>
111 <n-form-item path="lastName" label="Last Name">
112 <n-input
113 v-model:value="model.lastName"
114 @keydown.enter="signUp"
115 size="large"
116 placeholder="Last Name..."
117 />
118 </n-form-item>
119 <n-form-item path="customerCode" label="Customer Code">
120 <n-input
121 v-model:value="model.customerCode"
122 @keydown.enter="signUp"
123 size="large"
124 placeholder="Customer Code..."
125 />
126 </n-form-item>
127 -->
128
129 <div class="mt-3 flex items-center justify-between gap-3">
130 <div class="flex items-center justify-between gap-3">
131 <n-button size="large" @click="wizardCurrent = 1">
132 <template #icon>
133 <Icon :name="ArrowLeftIcon" />
134 </template>
135 Back
136 </n-button>
137 <n-button secondary size="large" @click="reset(2)">Reset</n-button>
138 </div>
139 <n-button
140 type="success"
141 size="large"
142 :loading
143 :disabled="!accountStepValid || !detailsStepValid"
144 @click="signUp"
145 >
146 <template #icon>
147 <Icon :name="UserAddIcon" />
148 </template>
149 Create account
150 </n-button>
151 </div>
152 </div>
153 </n-step>
154 </n-steps>
155 </n-form>
156 </n-spin>
157 </div>
158 </template>
159
160 <script lang="ts" setup>
161 // TODO-FE: refactor
162 import type { FormInst, FormItemRule, FormRules, FormValidationError } from "naive-ui"
163 import type { SelectBaseOption } from "naive-ui/es/select/src/interface"
164 import type { RegisterPayload } from "@/types/auth.d"
165 import _trim from "lodash/trim"
166 import { NButton, NForm, NFormItem, NInput, NSelect, NSpin, NStep, NSteps, useMessage } from "naive-ui"
167 import PasswordValidator from "password-validator"
168 import isEmail from "validator/es/lib/isEmail"
169 import { computed, ref } from "vue"
170 import Api from "@/api"
171 import Icon from "@/components/common/Icon.vue"
172 import { AuthUserRole } from "@/types/auth.d"
173 // import ImageCropper, { type ImageCropperResult } from "@/components/common/ImageCropper.vue"
174
175 interface ModelType {
176 email: string | null
177 password: string | null
178 username: string | null
179 confirmPassword: string | null
180 role: number | null
181 /*
182 customerCode: string | null
183 firstName: string | null
184 lastName: string | null
185 propic: string | null
186 */
187 }
188
189 const { unavailableUsernameList, unavailableEmailList } = defineProps<{
190 unavailableUsernameList?: string[]
191 unavailableEmailList?: string[]
192 }>()
193
194 const emit = defineEmits<{
195 (e: "success"): void
196 }>()
197
198 // const ImageIcon = "carbon:image"
199 // const RemoveImageIcon = "carbon:no-image"
200 const ArrowRightIcon = "carbon:arrow-right"
201 const ArrowLeftIcon = "carbon:arrow-left"
202 const UserAddIcon = "carbon:user-admin"
203
204 const wizardCurrent = ref(1)
205 const loading = ref(false)
206 const formRef = ref<FormInst | null>(null)
207 const message = useMessage()
208 const model = ref<ModelType>(getModel())
209
210 const roleOptions: SelectBaseOption[] = Object.values(AuthUserRole)
211 .filter(o => typeof o === "number" && o !== 0)
212 .map(o => ({
213 label: `${Object.entries(AuthUserRole).find(e => e[1] === o)?.[0]}`,
214 value: o
215 }))
216
217 const accountStepValid = computed(
218 () =>
219 !!_trim(model.value.email || "") &&
220 !!model.value.password &&
221 !!model.value.confirmPassword &&
222 model.value.password === model.value.confirmPassword
223 )
224 const detailsStepValid = computed(() => !!_trim(model.value.username || "") && !!model.value.role)
225 // const detailsStepValid = computed(() => !!model.value.customerCode && !!model.value.firstName && !!model.value.lastName)
226
227 const passwordSchema = new PasswordValidator()
228 passwordSchema
229 .is()
230 .min(8) // Minimum length 8
231 .is()
232 .max(100) // Maximum length 100
233 .has()
234 .uppercase() // Must have uppercase letters
235 .has()
236 .lowercase() // Must have lowercase letters
237 .has()
238 .digits(1) // Must have at least 1 digit
239 .has()
240 .symbols(1) // Must have at least 1 symbol
241
242 const rules: FormRules = {
243 email: [
244 {
245 required: true,
246 trigger: ["blur", "input"],
247 message: "Email is required"
248 },
249 {
250 validator: (_rule: FormItemRule, value: string): boolean => {
251 return isEmail(value)
252 },
253 message: "The email is not formatted correctly",
254 trigger: ["blur", "input"]
255 },
256 {
257 validator: (_rule: FormItemRule, value: string): boolean => {
258 return !unavailableEmailList?.length || !unavailableEmailList.includes(value)
259 },
260 message: "The Email is already used",
261 trigger: ["blur", "input"]
262 }
263 ],
264 password: [
265 {
266 required: true,
267 trigger: ["blur"],
268 message: "Password is required"
269 },
270 {
271 validator: (_rule: FormItemRule, value: string): boolean => {
272 return !!passwordSchema.validate(value, { details: false })
273 },
274 message:
275 "The string should have a minimum length of 8 characters, minimum of 1 uppercase and lowercase letter, minimum of 1 digit and 1 symbol",
276 trigger: ["blur"]
277 }
278 ],
279 confirmPassword: [
280 {
281 required: true,
282 trigger: ["blur", "input"],
283 message: "Confirm Password is required"
284 },
285 {
286 validator: (_rule: FormItemRule, value: string): boolean => {
287 return value === model.value.password
288 },
289 message: "Password is not same as re-entered password",
290 trigger: ["blur"]
291 }
292 ],
293 username: [
294 {
295 required: true,
296 trigger: ["blur", "input"],
297 message: "Username is required"
298 },
299 {
300 validator: (_rule: FormItemRule, value: string): boolean => {
301 return !unavailableUsernameList?.length || !unavailableUsernameList.includes(value)
302 },
303 message: "The Username is already used",
304 trigger: ["blur", "input"]
305 }
306 ],
307 role: [
308 {
309 required: true,
310 trigger: ["blur"],
311 type: "number",
312 message: "Role is required"
313 }
314 ]
315 /*
316 customerCode: [
317 {
318 required: true,
319 trigger: ["blur"],
320 message: "Customer Code is required"
321 }
322 ],
323 firstName: [
324 {
325 required: true,
326 trigger: ["blur"],
327 message: "First Name is required"
328 }
329 ],
330 lastName: [
331 {
332 required: true,
333 trigger: ["blur"],
334 message: "Last Name is required"
335 }
336 ]
337 */
338 }
339
340 function getModel(): ModelType {
341 return {
342 email: null,
343 password: null,
344 confirmPassword: null,
345 username: null,
346 role: null
347 /*
348 customerCode: null,
349 firstName: null,
350 lastName: null,
351 propic: null
352 */
353 }
354 }
355
356 function reset(step?: number) {
357 switch (step) {
358 case 1:
359 model.value.email = null
360 model.value.password = null
361 model.value.confirmPassword = null
362 break
363 case 2:
364 model.value.username = null
365 model.value.role = null
366 break
367 default:
368 model.value = getModel()
369 wizardCurrent.value = 1
370 break
371 }
372 }
373
374 function signUp(e: Event) {
375 e.preventDefault()
376 formRef.value?.validate((errors: Array<FormValidationError> | undefined) => {
377 if (!errors) {
378 loading.value = true
379
380 const payload: RegisterPayload = {
381 password: model.value.password || "",
382 email: _trim(model.value.email || ""),
383 username: _trim(model.value.username || ""),
384 role_id: model.value.role || 1
385 /*
386 customerCode: model.value.customerCode,
387 usersFirstName: model.value.firstName,
388 usersLastName: model.value.lastName,
389 usersEmail: model.value.email,
390 usersRole: "admin",
391 imageFile: model.value.propic,
392 notifications: 0,
393 */
394 }
395
396 Api.auth
397 .register(payload)
398 .then(res => {
399 if (res.data.success) {
400 message.success("User registered successfully.")
401 reset()
402 emit("success")
403 } else {
404 message.warning(res.data?.message || "An error occurred. Please try again later.")
405 }
406 })
407 .catch(err => {
408 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
409 })
410 .finally(() => {
411 loading.value = false
412 })
413 } else {
414 for (const err of errors) {
415 message.error(err[0]?.message ?? "Invalid fields")
416 }
417 }
418 })
419 }
420 /*
421 function setCroppedImage(result: ImageCropperResult) {
422 const canvas = result.canvas as HTMLCanvasElement
423 // model.value.propic = canvas.toDataURL()
424 }
425 */
426 </script>