+
diff --git a/lib/api/Account.ts b/lib/api/Account.ts
index fd2f91be..8568d7f3 100644
--- a/lib/api/Account.ts
+++ b/lib/api/Account.ts
@@ -10,9 +10,11 @@
*/
import {
+ ChangePasswordRequest,
LoginRequest,
LoginResponse,
ProblemDetails,
+ RecoverAccountRequest,
RegisterRequest,
UserViewModel,
ValidationProblemDetails,
@@ -34,6 +36,7 @@ export class Account<
* @response `400` `void` The request was malformed.
* @response `409` `ValidationProblemDetails` A `User` with the specified username or email already exists.
Validation error codes by property: - **Username**: - **UsernameTaken**: the username is already in use - **Email**: - **EmailAlreadyUsed**: the email is already in use
* @response `422` `void` The request contains errors.
Validation error codes by property: - **Username**: - **UsernameFormat**: Invalid username format - **Password**: - **PasswordFormat**: Invalid password format - **Email**: - **EmailValidator**: Invalid email format
+ * @response `500` `void` Server Error
*/
registerCreate = (data: RegisterRequest, params: RequestParams = {}) =>
this.request
({
@@ -92,4 +95,93 @@ export class Account<
secure: true,
...params,
})
+ /**
+ * No description
+ *
+ * @tags Account
+ * @name RecoverCreate
+ * @summary Sends an account recovery email.
+ * @request POST:/Account/recover
+ * @secure
+ * @response `200` `void` This endpoint returns 200 OK regardless of whether the email was sent successfully or not.
+ * @response `400` `ProblemDetails` The request object was malformed.
+ */
+ recoverCreate = (data: RecoverAccountRequest, params: RequestParams = {}) =>
+ this.request({
+ path: `/Account/recover`,
+ method: 'POST',
+ body: data,
+ secure: true,
+ type: ContentType.Json,
+ ...params,
+ })
+ /**
+ * No description
+ *
+ * @tags Account
+ * @name ConfirmUpdate
+ * @summary Confirms a user account.
+ * @request PUT:/Account/confirm/{id}
+ * @secure
+ * @response `200` `void` The account was confirmed successfully.
+ * @response `400` `ProblemDetails` Bad Request
+ * @response `404` `ProblemDetails` The token provided was invalid or expired.
+ * @response `409` `ProblemDetails` The user's account was either already confirmed or banned.
+ */
+ confirmUpdate = (id: string, params: RequestParams = {}) =>
+ this.request({
+ path: `/Account/confirm/${id}`,
+ method: 'PUT',
+ secure: true,
+ ...params,
+ })
+ /**
+ * No description
+ *
+ * @tags Account
+ * @name RecoverDetail
+ * @summary Tests an account recovery token for validity.
+ * @request GET:/Account/recover/{id}
+ * @secure
+ * @response `200` `void` The token provided is valid.
+ * @response `400` `ProblemDetails` Bad Request
+ * @response `404` `ProblemDetails` The token provided is invalid or expired, or the user is banned.
+ */
+ recoverDetail = (id: string, params: RequestParams = {}) =>
+ this.request({
+ path: `/Account/recover/${id}`,
+ method: 'GET',
+ secure: true,
+ ...params,
+ })
+ /**
+ * No description
+ *
+ * @tags Account
+ * @name RecoverCreate2
+ * @summary Recover the user's account by resetting their password to a new value.
+ * @request POST:/Account/recover/{id}
+ * @originalName recoverCreate
+ * @duplicate
+ * @secure
+ * @response `200` `void` The user's password was reset successfully.
+ * @response `400` `ProblemDetails` Bad Request
+ * @response `403` `ProblemDetails` The user is banned.
+ * @response `404` `ProblemDetails` The token provided is invalid or expired.
+ * @response `409` `ProblemDetails` The new password is the same as the user's existing password.
+ * @response `422` `ValidationProblemDetails` The request body contains errors.
A **PasswordFormat** Validation error on the Password field indicates that the password format is invalid.
+ */
+ recoverCreate2 = (
+ id: string,
+ data: ChangePasswordRequest,
+ params: RequestParams = {},
+ ) =>
+ this.request({
+ path: `/Account/recover/${id}`,
+ method: 'POST',
+ body: data,
+ secure: true,
+ type: ContentType.Json,
+ ...params,
+ })
}
diff --git a/lib/api/AccountRoute.ts b/lib/api/AccountRoute.ts
index b90b9893..f27622b8 100644
--- a/lib/api/AccountRoute.ts
+++ b/lib/api/AccountRoute.ts
@@ -10,8 +10,10 @@
*/
import {
+ ChangePasswordRequest,
LoginRequest,
LoginResponse,
+ RecoverAccountRequest,
RegisterRequest,
UserViewModel,
} from './data-contracts'
@@ -28,6 +30,7 @@ export namespace Account {
* @response `400` `void` The request was malformed.
* @response `409` `ValidationProblemDetails` A `User` with the specified username or email already exists.
Validation error codes by property: - **Username**: - **UsernameTaken**: the username is already in use - **Email**: - **EmailAlreadyUsed**: the email is already in use
* @response `422` `void` The request contains errors.
Validation error codes by property: - **Username**: - **UsernameFormat**: Invalid username format - **Password**: - **PasswordFormat**: Invalid password format - **Email**: - **EmailValidator**: Invalid email format
+ * @response `500` `void` Server Error
*/
export namespace RegisterCreate {
export type RequestParams = {}
@@ -80,4 +83,103 @@ export namespace Account {
export type RequestHeaders = {}
export type ResponseBody = void
}
+
+ /**
+ * No description
+ * @tags Account
+ * @name RecoverCreate
+ * @summary Sends an account recovery email.
+ * @request POST:/Account/recover
+ * @secure
+ * @response `200` `void` This endpoint returns 200 OK regardless of whether the email was sent successfully or not.
+ * @response `400` `ProblemDetails` The request object was malformed.
+ */
+ export namespace RecoverCreate {
+ export type RequestParams = {}
+ export type RequestQuery = {}
+ export type RequestBody = RecoverAccountRequest
+ export type RequestHeaders = {}
+ export type ResponseBody = void
+ }
+
+ /**
+ * No description
+ * @tags Account
+ * @name ConfirmUpdate
+ * @summary Confirms a user account.
+ * @request PUT:/Account/confirm/{id}
+ * @secure
+ * @response `200` `void` The account was confirmed successfully.
+ * @response `400` `ProblemDetails` Bad Request
+ * @response `404` `ProblemDetails` The token provided was invalid or expired.
+ * @response `409` `ProblemDetails` The user's account was either already confirmed or banned.
+ */
+ export namespace ConfirmUpdate {
+ export type RequestParams = {
+ /**
+ * The confirmation token.
+ * @pattern ^[a-zA-Z0-9-_]{22}$
+ */
+ id: string
+ }
+ export type RequestQuery = {}
+ export type RequestBody = never
+ export type RequestHeaders = {}
+ export type ResponseBody = void
+ }
+
+ /**
+ * No description
+ * @tags Account
+ * @name RecoverDetail
+ * @summary Tests an account recovery token for validity.
+ * @request GET:/Account/recover/{id}
+ * @secure
+ * @response `200` `void` The token provided is valid.
+ * @response `400` `ProblemDetails` Bad Request
+ * @response `404` `ProblemDetails` The token provided is invalid or expired, or the user is banned.
+ */
+ export namespace RecoverDetail {
+ export type RequestParams = {
+ /**
+ * The recovery token.
+ * @pattern ^[a-zA-Z0-9-_]{22}$
+ */
+ id: string
+ }
+ export type RequestQuery = {}
+ export type RequestBody = never
+ export type RequestHeaders = {}
+ export type ResponseBody = void
+ }
+
+ /**
+ * No description
+ * @tags Account
+ * @name RecoverCreate2
+ * @summary Recover the user's account by resetting their password to a new value.
+ * @request POST:/Account/recover/{id}
+ * @originalName recoverCreate
+ * @duplicate
+ * @secure
+ * @response `200` `void` The user's password was reset successfully.
+ * @response `400` `ProblemDetails` Bad Request
+ * @response `403` `ProblemDetails` The user is banned.
+ * @response `404` `ProblemDetails` The token provided is invalid or expired.
+ * @response `409` `ProblemDetails` The new password is the same as the user's existing password.
+ * @response `422` `ValidationProblemDetails` The request body contains errors.
A **PasswordFormat** Validation error on the Password field indicates that the password format is invalid.
+ */
+ export namespace RecoverCreate2 {
+ export type RequestParams = {
+ /**
+ * The recovery token.
+ * @pattern ^[a-zA-Z0-9-_]{22}$
+ */
+ id: string
+ }
+ export type RequestQuery = {}
+ export type RequestBody = ChangePasswordRequest
+ export type RequestHeaders = {}
+ export type ResponseBody = void
+ }
}
diff --git a/lib/api/data-contracts.ts b/lib/api/data-contracts.ts
index 94e2afa5..ffad3b27 100644
--- a/lib/api/data-contracts.ts
+++ b/lib/api/data-contracts.ts
@@ -43,6 +43,10 @@ export interface CategoryViewModel {
rules?: string | null
}
+export interface ChangePasswordRequest {
+ password: string
+}
+
/** This request object is sent when creating a `Category`. */
export interface CreateCategoryRequest {
/**
@@ -186,6 +190,20 @@ export interface ProblemDetails {
[key: string]: any
}
+export interface RecoverAccountRequest {
+ /**
+ * The user's name.
+ * @minLength 1
+ */
+ username: string
+ /**
+ * The user's email address.
+ * @format email
+ * @minLength 1
+ */
+ email: string
+}
+
/** This request object is sent when a `User` is attempting to register. */
export interface RegisterRequest {
/**
diff --git a/lib/helpers.ts b/lib/helpers.ts
index b4f382fb..cf3d302a 100644
--- a/lib/helpers.ts
+++ b/lib/helpers.ts
@@ -32,7 +32,25 @@ export function isValidationProblemDetails(o: unknown): boolean {
)
}
+/**
+ * Takes a string and returns a sentence case version of it.
+ *
+ * **Note:** This function assumes that the string is camelCase or PascalCase.
+ *
+ * @param string The string to convert.
+ * @returns string
+ */
+export function sentenceCase(string: string): string {
+ let words = string.split(/(?=[A-Z])/)
+ words = words.map((word) => word.toLowerCase())
+
+ let sentence = words.join(' ')
+ sentence = sentence.charAt(0).toUpperCase() + sentence.slice(1)
+ return sentence
+}
+
export default {
isProblemDetails,
isValidationProblemDetails,
+ sentenceCase,
}
diff --git a/middleware/confirm-account.ts b/middleware/confirm-account.ts
new file mode 100644
index 00000000..52244d62
--- /dev/null
+++ b/middleware/confirm-account.ts
@@ -0,0 +1,32 @@
+import { useConfirmAccount } from 'composables/api'
+import { useModalAlert } from 'composables/useModalAlert'
+
+export default defineNuxtRouteMiddleware((_to, from) => {
+ const route = useRoute()
+ const confirmationCode = route.query?.code as string
+ const modalAlertState = useModalAlert().value
+
+ if (
+ !confirmationCode ||
+ from?.fullPath !== `/confirm-account?code=${confirmationCode}`
+ ) {
+ return navigateTo('/', { replace: true })
+ }
+
+ useConfirmAccount(confirmationCode, {
+ onError: () => {
+ modalAlertState.show = true
+ modalAlertState.title = 'Something went wrong...'
+ modalAlertState.body = 'Unable to confirm account.'
+ modalAlertState.type = 'error'
+ navigateTo('/', { replace: true })
+ },
+ onOkay: () => {
+ modalAlertState.show = true
+ modalAlertState.title = 'Success!'
+ modalAlertState.body = 'Account confirmed successfully!'
+ modalAlertState.type = 'success'
+ navigateTo('/', { replace: true })
+ },
+ })
+})
diff --git a/package.json b/package.json
index c62cb342..58265392 100644
--- a/package.json
+++ b/package.json
@@ -3,11 +3,11 @@
"version": "1.0.0",
"private": true,
"engines": {
- "node": ">=20.0.0",
- "pnpm": "8.7.5"
+ "node": ">=18.0.0",
+ "pnpm": "8.11.0"
},
"engineStrict": true,
- "packageManager": "pnpm@8.7.5",
+ "packageManager": "pnpm@8.11.0",
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
diff --git a/pages/confirm-account.test.ts b/pages/confirm-account.test.ts
new file mode 100644
index 00000000..a3d4de05
--- /dev/null
+++ b/pages/confirm-account.test.ts
@@ -0,0 +1,23 @@
+import { mount, enableAutoUnmount } from '@vue/test-utils'
+import { getByClass } from 'root/testUtils'
+import confirmAccount from 'pages/confirm-account.vue'
+
+function getConfirmAccountWrapper() {
+ return mount(confirmAccount)
+}
+
+vi.stubGlobal('definePageMeta', () => {})
+
+enableAutoUnmount(afterEach)
+
+describe('/confirm-account?code=', () => {
+ it('should render without crashing', () => {
+ const wrapper = getConfirmAccountWrapper()
+ expect(wrapper.isVisible()).toBe(true)
+ })
+
+ it('should render the loader', () => {
+ const wrapper = getConfirmAccountWrapper()
+ expect(getByClass(wrapper, 'loader__container').isVisible()).toBe(true)
+ })
+})
diff --git a/pages/confirm-account.vue b/pages/confirm-account.vue
new file mode 100644
index 00000000..b50fbfdf
--- /dev/null
+++ b/pages/confirm-account.vue
@@ -0,0 +1,12 @@
+
+
+
+
+
diff --git a/pages/index.test.ts b/pages/index.test.ts
index 6366c8b8..0adcb9ab 100644
--- a/pages/index.test.ts
+++ b/pages/index.test.ts
@@ -3,13 +3,7 @@ import { mount, enableAutoUnmount } from '@vue/test-utils'
import index from 'pages/index.vue'
function getIndexWrapper() {
- return mount(index, {
- global: {
- mocks: {
- $t: (msg: any) => msg,
- },
- },
- })
+ return mount(index)
}
enableAutoUnmount(afterEach)
diff --git a/testUtils.ts b/testUtils.ts
index bffe0fec..1fe1c347 100644
--- a/testUtils.ts
+++ b/testUtils.ts
@@ -1,6 +1,13 @@
import { DOMWrapper, VueWrapper } from '@vue/test-utils'
type WrappedElement = Omit, 'exists'>
+export function getByClass(
+ wrapper: VueWrapper,
+ className: string,
+): WrappedElement {
+ return wrapper.get(`.${className}`)
+}
+
export function getByTestId(
wrapper: VueWrapper,
testId: string,