diff --git a/.pnp.cjs b/.pnp.cjs index 852300dc..ab1b6b30 100644 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -2174,6 +2174,7 @@ const RAW_RUNTIME_STATE = ["@testing-library/react", "virtual:e06e30328889a833dfcbfbc5c33f5e1173628bc1414934ce897e2f1b90b91087fa3f8b59a8a3ced3a53b1f7df474adc821437919c179e8897ac6cecf6bf31f00#npm:16.0.1"],\ ["@types/dinero.js", "npm:1.9.4"],\ ["@types/jest", "npm:29.5.12"],\ + ["@types/luxon", "npm:3.4.2"],\ ["@types/react", "npm:18.3.3"],\ ["@types/react-dom", "npm:18.3.0"],\ ["@typescript-eslint/eslint-plugin", "virtual:36a01d8083315b8a6e8362097258ea8bc0f9dfb672cb210742e054760850c673a1038f542a6b7156397b5275ace8ee0482231cac5e8898044fa1a1c29f78ee5b#npm:7.18.0"],\ @@ -2187,6 +2188,7 @@ const RAW_RUNTIME_STATE = ["eslint-plugin-prettier", "virtual:36a01d8083315b8a6e8362097258ea8bc0f9dfb672cb210742e054760850c673a1038f542a6b7156397b5275ace8ee0482231cac5e8898044fa1a1c29f78ee5b#npm:5.2.1"],\ ["jest", "virtual:b2e857f8c518119e848cf4ef51cff2bf36fb4db0f8e551e1de9a65b88f5466b35ebea1913543d6258bb39baec552d66e8e4c2e8ae0858f2f3f9bf35009befb70#npm:29.7.0"],\ ["jest-environment-jsdom", "virtual:e06e30328889a833dfcbfbc5c33f5e1173628bc1414934ce897e2f1b90b91087fa3f8b59a8a3ced3a53b1f7df474adc821437919c179e8897ac6cecf6bf31f00#npm:29.7.0"],\ + ["luxon", "npm:3.5.0"],\ ["object-code", "npm:1.3.3"],\ ["polytype", "npm:0.17.0"],\ ["prettier", "npm:3.3.3"],\ diff --git a/packages/models/package.json b/packages/models/package.json index 86050fde..2eef080c 100644 --- a/packages/models/package.json +++ b/packages/models/package.json @@ -43,6 +43,7 @@ "another-deep-freeze": "^1.0.0", "context": "^3.0.31", "dinero.js": "^1.9.1", + "luxon": "^3.5.0", "object-code": "^1.3.3", "polytype": "^0.17.0", "tsd": "^0.31.2", @@ -56,6 +57,7 @@ "@testing-library/react": "^16.0.1", "@types/dinero.js": "^1", "@types/jest": "^29.5.12", + "@types/luxon": "^3", "@types/react": "^18.3.3", "@types/react-dom": "^18", "@typescript-eslint/eslint-plugin": "^7.18.0", diff --git a/packages/models/src/auth/Mfa/Mfa.ts b/packages/models/src/auth/Mfa/Mfa.ts new file mode 100644 index 00000000..24ea000d --- /dev/null +++ b/packages/models/src/auth/Mfa/Mfa.ts @@ -0,0 +1,51 @@ +import { DataModel } from "../../base/index.js"; +import { MfaStatusData, RecoverMfaRequestData } from "./types.js"; +import { provideReact } from "../../react/index.js"; +import { config } from "../../config/config.js"; +import { RecoveryCodes } from "../RecoveryCodes/RecoveryCodes.js"; + +export class Mfa { + public static getStatus = provideReact( + async (): Promise => { + const data = await config.behaviors.mfa.getStatus(); + return new MfaStatus(data); + }, + ); + + public static async confirm(multiFactorCode: string): Promise { + const response = await config.behaviors.mfa.confirm(multiFactorCode); + return new RecoveryCodes({ + codes: response.recoveryCodesList, + }); + } + + public static async disable(multiFactorCode: string): Promise { + await config.behaviors.mfa.disable(multiFactorCode); + } + + public static async recover(data: RecoverMfaRequestData): Promise { + await config.behaviors.mfa.recover(data); + } + + public static async resetRecoveryCodes( + multiFactorCode: string, + ): Promise { + const response = + await config.behaviors.mfa.resetRecoveryCodes(multiFactorCode); + + return new RecoveryCodes({ + codes: response.recoveryCodesList, + }); + } +} + +export class MfaStatus extends DataModel { + public readonly isInitialized; + public readonly isConfirmed; + + public constructor(data: MfaStatusData) { + super(data); + this.isConfirmed = data.confirmed; + this.isInitialized = data.initialized; + } +} diff --git a/packages/models/src/auth/Mfa/PendingMfaAuthentication.ts b/packages/models/src/auth/Mfa/PendingMfaAuthentication.ts new file mode 100644 index 00000000..7a1795ef --- /dev/null +++ b/packages/models/src/auth/Mfa/PendingMfaAuthentication.ts @@ -0,0 +1,26 @@ +import { UserAuthenticateRequestData } from "../../user/index.js"; +import { config } from "../../config/config.js"; +import { Session } from "../Session/index.js"; + +export class PendingMfaAuthentication { + private authenticationRequestData: UserAuthenticateRequestData | undefined; + + public constructor(authenticationRequestData: UserAuthenticateRequestData) { + this.authenticationRequestData = authenticationRequestData; + } + + public async provideMultiFactorCode(multiFactorCode: string) { + const sessionData = await config.behaviors.mfa.authenticateMfa({ + ...this.authenticationRequestData, + multiFactorCode, + }); + + this.authenticationRequestData = undefined; + + return new Session(sessionData); + } + + public get isAlreadyConfirmed() { + return !this.authenticationRequestData; + } +} diff --git a/packages/models/src/auth/Mfa/behaviors/api.ts b/packages/models/src/auth/Mfa/behaviors/api.ts new file mode 100644 index 00000000..b93021a3 --- /dev/null +++ b/packages/models/src/auth/Mfa/behaviors/api.ts @@ -0,0 +1,64 @@ +import { assertStatus, MittwaldAPIV2Client } from "@mittwald/api-client"; +import { MfaBehaviors } from "./types.js"; + +export const apiMfaBehaviors = (client: MittwaldAPIV2Client): MfaBehaviors => ({ + authenticateMfa: async (data) => { + const response = await client.user.authenticateMfa({ + data, + }); + + assertStatus(response, 200); + + return response.data; + }, + + getStatus: async () => { + const response = await client.user.getMfaStatus({}); + + assertStatus(response, 200); + + return response.data; + }, + + recover: async (data) => { + const { recoveryCode, ...restData } = data; + const response = await client.user.authenticateMfa({ + data: { + multiFactorCode: recoveryCode, + ...restData, + }, + }); + + assertStatus(response, 200); + + return response.data; + }, + + confirm: async (multiFactorCode: string) => { + const response = await client.user.confirmMfa({ + data: { multiFactorCode }, + }); + + assertStatus(response, 200); + + return response.data; + }, + + resetRecoveryCodes: async (multiFactorCode: string) => { + const response = await client.user.resetRecoverycodes({ + data: { multiFactorCode }, + }); + + assertStatus(response, 200); + + return response.data; + }, + + disable: async (multiFactorCode: string) => { + const response = await client.user.disableMfa({ + data: { multiFactorCode }, + }); + + assertStatus(response, 204); + }, +}); diff --git a/packages/models/src/auth/Mfa/behaviors/index.ts b/packages/models/src/auth/Mfa/behaviors/index.ts new file mode 100644 index 00000000..a7b74f7c --- /dev/null +++ b/packages/models/src/auth/Mfa/behaviors/index.ts @@ -0,0 +1,2 @@ +export * from "./api.js"; +export * from "./types.js"; diff --git a/packages/models/src/auth/Mfa/behaviors/types.ts b/packages/models/src/auth/Mfa/behaviors/types.ts new file mode 100644 index 00000000..1ae5959a --- /dev/null +++ b/packages/models/src/auth/Mfa/behaviors/types.ts @@ -0,0 +1,24 @@ +import { + AuthenticateMfaRequestData, + ConfirmMfaResponseData, + MfaStatusData, + RecoverMfaRequestData, + ResetMfaRecoveryResponseData, +} from "../types.js"; +import { SessionData } from "../../Session/types.js"; + +export interface MfaBehaviors { + getStatus: () => Promise; + + recover: (data: RecoverMfaRequestData) => Promise; + + resetRecoveryCodes: ( + multiFactorCode: string, + ) => Promise; + + confirm: (multiFactorCode: string) => Promise; + + disable: (multiFactorCode: string) => Promise; + + authenticateMfa: (data: AuthenticateMfaRequestData) => Promise; +} diff --git a/packages/models/src/auth/Mfa/index.ts b/packages/models/src/auth/Mfa/index.ts new file mode 100644 index 00000000..a8b13866 --- /dev/null +++ b/packages/models/src/auth/Mfa/index.ts @@ -0,0 +1 @@ +export * from "./Mfa.js"; diff --git a/packages/models/src/auth/Mfa/types.ts b/packages/models/src/auth/Mfa/types.ts new file mode 100644 index 00000000..e45118f2 --- /dev/null +++ b/packages/models/src/auth/Mfa/types.ts @@ -0,0 +1,20 @@ +import { MittwaldAPIV2 } from "@mittwald/api-client"; + +export type MfaStatusData = + MittwaldAPIV2.Operations.UserGetMfaStatus.ResponseData; + +export type RecoverMfaRequestData = Omit< + MittwaldAPIV2.Paths.V2AuthenticateMfa.Post.Parameters.RequestBody, + "multiFactorCode" +> & { + recoveryCode: string; +}; + +export type ResetMfaRecoveryResponseData = + MittwaldAPIV2.Operations.UserResetRecoverycodes.ResponseData; + +export type ConfirmMfaResponseData = + MittwaldAPIV2.Operations.UserConfirmMfa.ResponseData; + +export type AuthenticateMfaRequestData = + MittwaldAPIV2.Paths.V2AuthenticateMfa.Post.Parameters.RequestBody; diff --git a/packages/models/src/auth/RecoveryCodes/RecoveryCodes.ts b/packages/models/src/auth/RecoveryCodes/RecoveryCodes.ts new file mode 100644 index 00000000..b8e70323 --- /dev/null +++ b/packages/models/src/auth/RecoveryCodes/RecoveryCodes.ts @@ -0,0 +1,11 @@ +import { DataModel } from "../../base/index.js"; +import { RecoveryCodesData } from "./types.js"; + +export class RecoveryCodes extends DataModel { + public readonly codes: string[]; + + public constructor(data: RecoveryCodesData) { + super(data); + this.codes = data.codes; + } +} diff --git a/packages/models/src/auth/RecoveryCodes/index.ts b/packages/models/src/auth/RecoveryCodes/index.ts new file mode 100644 index 00000000..81103f11 --- /dev/null +++ b/packages/models/src/auth/RecoveryCodes/index.ts @@ -0,0 +1 @@ +export * from "./RecoveryCodes.js"; diff --git a/packages/models/src/auth/RecoveryCodes/types.ts b/packages/models/src/auth/RecoveryCodes/types.ts new file mode 100644 index 00000000..1167ce57 --- /dev/null +++ b/packages/models/src/auth/RecoveryCodes/types.ts @@ -0,0 +1,3 @@ +export interface RecoveryCodesData { + codes: string[]; +} diff --git a/packages/models/src/auth/Session/Session.ts b/packages/models/src/auth/Session/Session.ts new file mode 100644 index 00000000..e32ea705 --- /dev/null +++ b/packages/models/src/auth/Session/Session.ts @@ -0,0 +1,20 @@ +import { DataModel } from "../../base/index.js"; +import { SessionData } from "./types.js"; +import { DateTime } from "luxon"; + +export class Session extends DataModel { + public readonly expirationDate: DateTime; + public readonly token: string; + public readonly refreshToken: string; + + public constructor(data: SessionData) { + super(data); + this.expirationDate = DateTime.fromISO(data.expires); + this.token = data.token; + this.refreshToken = data.refreshToken; + } + + public isExpired() { + return this.expirationDate > DateTime.now(); + } +} diff --git a/packages/models/src/auth/Session/index.ts b/packages/models/src/auth/Session/index.ts new file mode 100644 index 00000000..29e4cf71 --- /dev/null +++ b/packages/models/src/auth/Session/index.ts @@ -0,0 +1 @@ +export * from "./Session.js"; diff --git a/packages/models/src/auth/Session/types.ts b/packages/models/src/auth/Session/types.ts new file mode 100644 index 00000000..bb8c0396 --- /dev/null +++ b/packages/models/src/auth/Session/types.ts @@ -0,0 +1,5 @@ +export interface SessionData { + expires: string; + token: string; + refreshToken: string; +} diff --git a/packages/models/src/config/behaviors/api.ts b/packages/models/src/config/behaviors/api.ts index 9fbb4efb..4dc65dc2 100644 --- a/packages/models/src/config/behaviors/api.ts +++ b/packages/models/src/config/behaviors/api.ts @@ -9,6 +9,8 @@ import { addUrlTagToProvideReactCache } from "../../react/asyncResourceInvalidat import { apiArticleBehaviors } from "../../article/Article/behaviors/index.js"; import { apiContractBehaviors } from "../../contract/Contract/behaviors/index.js"; import { apiContractItemBehaviors } from "../../contract/ContractItem/behaviors/index.js"; +import { apiUserBehaviors } from "../../user/User/behaviors/index.js"; +import { apiMfaBehaviors } from "../../auth/Mfa/behaviors/index.js"; class ApiSetupState { private _client: MittwaldAPIV2Client | undefined; @@ -29,6 +31,8 @@ class ApiSetupState { config.behaviors.customer = apiCustomerBehaviors(client); config.behaviors.ingress = apiIngressBehaviors(client); config.behaviors.appInstallation = apiAppInstallationBehaviors(client); + config.behaviors.user = apiUserBehaviors(client); + config.behaviors.mfa = apiMfaBehaviors(client); config.behaviors.contract = apiContractBehaviors(client); config.behaviors.contractItem = apiContractItemBehaviors(client); } diff --git a/packages/models/src/config/config.ts b/packages/models/src/config/config.ts index c20a52c3..484edc61 100644 --- a/packages/models/src/config/config.ts +++ b/packages/models/src/config/config.ts @@ -4,8 +4,10 @@ import { CustomerBehaviors } from "../customer/Customer/behaviors/index.js"; import { IngressBehaviors } from "../domain/Ingress/behaviors/index.js"; import { ContractBehaviors } from "../contract/Contract/behaviors/index.js"; import { AppInstallationBehaviors } from "../app/AppInstallation/behaviors/index.js"; +import { UserBehaviors } from "../user/User/behaviors/index.js"; import { ContractItemBehaviors } from "../contract/ContractItem/behaviors/index.js"; import { ArticleBehaviors } from "../article/Article/behaviors/index.js"; +import { MfaBehaviors } from "../auth/Mfa/behaviors/index.js"; interface Config { defaultPaginationLimit: number; @@ -18,6 +20,8 @@ interface Config { customer: CustomerBehaviors; ingress: IngressBehaviors; appInstallation: AppInstallationBehaviors; + user: UserBehaviors; + mfa: MfaBehaviors; }; } @@ -32,5 +36,7 @@ export const config: Config = { customer: undefined as unknown as CustomerBehaviors, ingress: undefined as unknown as IngressBehaviors, appInstallation: undefined as unknown as AppInstallationBehaviors, + user: undefined as unknown as UserBehaviors, + mfa: undefined as unknown as MfaBehaviors, }, }; diff --git a/packages/models/src/user/User/User.ts b/packages/models/src/user/User/User.ts new file mode 100644 index 00000000..4bfd99a8 --- /dev/null +++ b/packages/models/src/user/User/User.ts @@ -0,0 +1,164 @@ +import { config } from "../../config/config.js"; +import { classes } from "polytype"; +import { + UserAuthenticateRequestData, + UserConfirmPasswordResetRequestData, + UserData, + UserDeleteRequestData, + UserRegisterRequestData, + UserRequestAvatarUploadResponseData, + UserResendVerificationEmailRequestData, + UserUpdatePasswordRequestData, + UserUpdatePasswordResponseData, + UserUpdatePersonalInformationRequestData, + UserVerifyEmailRequestData, + UserVerifyPhoneNumberRequestData, + UserVerifyRegistrationRequestData, +} from "./types.js"; +import assertObjectFound from "../../base/assertObjectFound.js"; +import { DataModel, ReferenceModel } from "../../base/index.js"; +import { AsyncResourceVariant, provideReact } from "../../react.js"; +import { Session } from "../../auth/Session/index.js"; +import { PendingMfaAuthentication } from "../../auth/Mfa/PendingMfaAuthentication.js"; + +export class User extends ReferenceModel { + public static ofId(id: string): User { + return new User(id); + } + + public static find = provideReact( + async (id: string): Promise => { + const data = await config.behaviors.user.find(id); + if (data !== undefined) { + return new UserDetailed(data); + } + }, + ); + + public static get = provideReact( + async (id: string): Promise => { + const user = await this.find(id); + assertObjectFound(user, this, id); + return user; + }, + ); + + public static getOwn = provideReact(async (): Promise => { + return await this.get("self"); + }); + + public getDetailed = provideReact( + () => User.get(this.id), + [this.id], + ) as AsyncResourceVariant<() => Promise>; + + public findDetailed = provideReact( + () => User.find(this.id), + [this.id], + ) as AsyncResourceVariant<() => Promise>; + + public getPasswordUpdatedAt = provideReact( + async (): Promise<{ passwordUpdatedAt: string }> => { + return await config.behaviors.user.getPasswordUpdatedAt(); + }, + ); + + public async updatePersonalInformation( + data: UserUpdatePersonalInformationRequestData, + ): Promise { + await config.behaviors.user.updatePersonalInformation(this.id, data); + } + + public async addPhoneNumber(phoneNumber: string): Promise { + await config.behaviors.user.addPhoneNumber(this.id, phoneNumber); + } + + public async verifyPhoneNumber( + data: UserVerifyPhoneNumberRequestData, + ): Promise { + await config.behaviors.user.verifyPhoneNumber(this.id, data); + } + + public async removePhoneNumber(): Promise { + await config.behaviors.user.removePhoneNumber(this.id); + } + + public async updateEmail(email: string): Promise { + await config.behaviors.user.updateEmail(email); + } + + public async verifyEmail(data: UserVerifyEmailRequestData): Promise { + await config.behaviors.user.verifyEmail(data); + } + + public async requestAvatarUpload(): Promise { + return await config.behaviors.user.requestAvatarUpload(this.id); + } + + public async removeAvatar(): Promise { + await config.behaviors.user.removeAvatar(this.id); + } + + public async updatePassword( + data: UserUpdatePasswordRequestData, + ): Promise { + return await config.behaviors.user.updatePassword(data); + } + + public async resetPassword(email: string): Promise { + await config.behaviors.user.resetPassword(email); + } + + public async confirmPasswordReset( + data: UserConfirmPasswordResetRequestData, + ): Promise { + await config.behaviors.user.confirmPasswordReset(data); + } + + public static async authenticate( + data: UserAuthenticateRequestData, + ): Promise { + const result = await config.behaviors.user.authenticate(data); + if ("token" in result) { + return new Session(result); + } + return new PendingMfaAuthentication(data); + } + + public static async register( + data: UserRegisterRequestData, + ): Promise<{ id: string }> { + return await config.behaviors.user.register(data); + } + + public static async verifyRegistration( + data: UserVerifyRegistrationRequestData, + ): Promise { + return await config.behaviors.user.verifyRegistration(data); + } + + public static async resendVerificationEmail( + data: UserResendVerificationEmailRequestData, + ): Promise { + return await config.behaviors.user.resendVerificationEmail(data); + } + + public static async delete(data: UserDeleteRequestData): Promise { + await config.behaviors.user.delete(data); + } +} + +class UserCommon extends classes(DataModel, User) { + public readonly fullName: string; + + public constructor(data: UserData) { + super([data], [data.userId]); + this.fullName = `${data.person.firstName} ${data.person.lastName}`; + } +} + +export class UserDetailed extends classes(UserCommon, DataModel) { + public constructor(data: UserData) { + super([data], [data]); + } +} diff --git a/packages/models/src/user/User/behaviors/api.ts b/packages/models/src/user/User/behaviors/api.ts new file mode 100644 index 00000000..a1295315 --- /dev/null +++ b/packages/models/src/user/User/behaviors/api.ts @@ -0,0 +1,153 @@ +import { + assertOneOfStatus, + assertStatus, + MittwaldAPIV2Client, +} from "@mittwald/api-client"; +import { UserBehaviors } from "./types.js"; +import { + UserAuthenticateRequestData, + UserConfirmPasswordResetRequestData, + UserDeleteRequestData, + UserRegisterRequestData, + UserResendVerificationEmailRequestData, + UserVerifyRegistrationRequestData, +} from "../types.js"; + +export const apiUserBehaviors = ( + client: MittwaldAPIV2Client, +): UserBehaviors => ({ + find: async (id) => { + const response = await client.user.getUser({ userId: id }); + + if (response.status === 200) { + return response.data; + } + assertOneOfStatus(response, [403, 404]); + }, + + getPasswordUpdatedAt: async () => { + const response = await client.user.getPasswordUpdatedAt({}); + + assertStatus(response, 200); + + return response.data; + }, + + updatePersonalInformation: async (id, data) => { + const response = await client.user.updatePersonalInformation({ + userId: id, + data, + }); + + assertStatus(response, 204); + }, + + addPhoneNumber: async (id, phoneNumber) => { + const response = await client.user.addPhoneNumber({ + userId: id, + data: { phoneNumber }, + }); + + assertStatus(response, 204); + }, + + verifyPhoneNumber: async (id, data) => { + const response = await client.user.verifyPhoneNumber({ userId: id, data }); + + assertStatus(response, 204); + }, + + removePhoneNumber: async (id) => { + const response = await client.user.removePhoneNumber({ userId: id }); + + assertStatus(response, 204); + }, + + updateEmail: async (email) => { + const response = await client.user.changeEmail({ data: { email } }); + + assertStatus(response, 204); + }, + + verifyEmail: async (data) => { + const response = await client.user.verifyEmail({ data }); + + assertStatus(response, 204); + }, + + requestAvatarUpload: async (id) => { + const response = await client.user.requestAvatarUpload({ userId: id }); + + assertStatus(response, 200); + + return response.data; + }, + + removeAvatar: async (id) => { + const response = await client.user.removeAvatar({ userId: id }); + + assertStatus(response, 204); + }, + + updatePassword: async (data) => { + const response = await client.user.changePassword({ data }); + + assertStatus(response, 200); + + return response.data; + }, + + resetPassword: async (email) => { + const response = await client.user.initPasswordReset({ data: { email } }); + + assertStatus(response, 201); + }, + + confirmPasswordReset: async (data: UserConfirmPasswordResetRequestData) => { + const response = await client.user.confirmPasswordReset({ data }); + + assertStatus(response, 204); + }, + + authenticate: async (data: UserAuthenticateRequestData) => { + const response = await client.user.authenticate({ data }); + + assertOneOfStatus(response, [200, 202]); + + return response.data; + }, + + logout: async () => { + const response = await client.user.logout(); + + assertStatus(response, 204); + }, + + register: async (data: UserRegisterRequestData) => { + const response = await client.user.register({ data }); + + assertStatus(response, 201); + + return { id: response.data.userId }; + }, + + verifyRegistration: async (data: UserVerifyRegistrationRequestData) => { + const response = await client.user.verifyRegistration({ data }); + + assertStatus(response, 200); + }, + + resendVerificationEmail: async ( + data: UserResendVerificationEmailRequestData, + ) => { + const response = await client.user.resendVerificationEmail({ data }); + + assertStatus(response, 204); + }, + + delete: async (data: UserDeleteRequestData) => { + const response = await client.user.deleteUser({ data }); + + assertOneOfStatus(response, [200, 202]); + }, +}); diff --git a/packages/models/src/user/User/behaviors/index.ts b/packages/models/src/user/User/behaviors/index.ts new file mode 100644 index 00000000..a7b74f7c --- /dev/null +++ b/packages/models/src/user/User/behaviors/index.ts @@ -0,0 +1,2 @@ +export * from "./api.js"; +export * from "./types.js"; diff --git a/packages/models/src/user/User/behaviors/types.ts b/packages/models/src/user/User/behaviors/types.ts new file mode 100644 index 00000000..43f585d5 --- /dev/null +++ b/packages/models/src/user/User/behaviors/types.ts @@ -0,0 +1,67 @@ +import { + UserAuthenticateRequestData, + UserAuthenticateResponseData, + UserConfirmPasswordResetRequestData, + UserData, + UserDeleteRequestData, + UserRegisterRequestData, + UserRequestAvatarUploadResponseData, + UserResendVerificationEmailRequestData, + UserUpdatePasswordRequestData, + UserUpdatePasswordResponseData, + UserUpdatePersonalInformationRequestData, + UserVerifyEmailRequestData, + UserVerifyPhoneNumberRequestData, + UserVerifyRegistrationRequestData, +} from "../types.js"; + +export interface UserBehaviors { + find: (id: string) => Promise; + + getPasswordUpdatedAt: () => Promise<{ passwordUpdatedAt: string }>; + + updatePersonalInformation: ( + id: string, + data: UserUpdatePersonalInformationRequestData, + ) => Promise; + + addPhoneNumber: (id: string, phoneNumber: string) => Promise; + verifyPhoneNumber: ( + id: string, + data: UserVerifyPhoneNumberRequestData, + ) => Promise; + removePhoneNumber: (id: string) => Promise; + + updateEmail: (email: string) => Promise; + verifyEmail: (data: UserVerifyEmailRequestData) => Promise; + + requestAvatarUpload: ( + id: string, + ) => Promise; + removeAvatar: (id: string) => Promise; + + updatePassword: ( + data: UserUpdatePasswordRequestData, + ) => Promise; + + resetPassword: (email: string) => Promise; + confirmPasswordReset: ( + data: UserConfirmPasswordResetRequestData, + ) => Promise; + + authenticate: ( + data: UserAuthenticateRequestData, + ) => Promise; + + logout: () => Promise; + + register: (data: UserRegisterRequestData) => Promise<{ id: string }>; + verifyRegistration: ( + data: UserVerifyRegistrationRequestData, + ) => Promise; + resendVerificationEmail: ( + data: UserResendVerificationEmailRequestData, + ) => Promise; + + delete: (data: UserDeleteRequestData) => Promise; +} diff --git a/packages/models/src/user/User/index.ts b/packages/models/src/user/User/index.ts new file mode 100644 index 00000000..c0901e63 --- /dev/null +++ b/packages/models/src/user/User/index.ts @@ -0,0 +1,2 @@ +export * from "./User.js"; +export * from "./types.js"; diff --git a/packages/models/src/user/User/types.ts b/packages/models/src/user/User/types.ts new file mode 100644 index 00000000..10ca6a7d --- /dev/null +++ b/packages/models/src/user/User/types.ts @@ -0,0 +1,45 @@ +import { MittwaldAPIV2 } from "@mittwald/api-client"; + +export type UserData = MittwaldAPIV2.Operations.UserGetUser.ResponseData; + +export type UserUpdatePersonalInformationRequestData = + MittwaldAPIV2.Paths.V2UsersSelfPersonalInformation.Put.Parameters.RequestBody; + +export type UserVerifyPhoneNumberRequestData = + MittwaldAPIV2.Paths.V2UsersUserIdActionsVerifyPhone.Post.Parameters.RequestBody; + +export type UserRequestAvatarUploadResponseData = + MittwaldAPIV2.Paths.V2UsersUserIdAvatar.Post.Parameters.RequestBody; + +export type UserVerifyEmailRequestData = + MittwaldAPIV2.Paths.V2UsersSelfCredentialsEmailActionsVerifyEmail.Post.Parameters.RequestBody; + +export type UserUpdatePasswordRequestData = + MittwaldAPIV2.Paths.V2UsersSelfCredentialsPassword.Put.Parameters.RequestBody; + +export type UserUpdatePasswordResponseData = + MittwaldAPIV2.Paths.V2UsersSelfCredentialsPassword.Put.Responses.$200.Content.ApplicationJson; + +export type UserConfirmPasswordResetRequestData = + MittwaldAPIV2.Paths.V2UsersSelfCredentialsPasswordConfirmReset.Post.Parameters.RequestBody; + +export type UserAuthenticateRequestData = + MittwaldAPIV2.Paths.V2Authenticate.Post.Parameters.RequestBody & { + multiFactorCode?: string; + }; + +export type UserAuthenticateResponseData = + | MittwaldAPIV2.Paths.V2Authenticate.Post.Responses.$200.Content.ApplicationJson + | MittwaldAPIV2.Paths.V2Authenticate.Post.Responses.$202.Content.ApplicationJson; + +export type UserRegisterRequestData = + MittwaldAPIV2.Paths.V2Register.Post.Parameters.RequestBody; + +export type UserVerifyRegistrationRequestData = + MittwaldAPIV2.Paths.V2VerifyRegistration.Post.Parameters.RequestBody; + +export type UserResendVerificationEmailRequestData = + MittwaldAPIV2.Paths.V2UsersSelfCredentialsEmailActionsResendEmail.Post.Parameters.RequestBody; + +export type UserDeleteRequestData = + MittwaldAPIV2.Paths.V2UsersSelf.Delete.Parameters.RequestBody; diff --git a/packages/models/src/user/index.ts b/packages/models/src/user/index.ts new file mode 100644 index 00000000..8f9829a8 --- /dev/null +++ b/packages/models/src/user/index.ts @@ -0,0 +1 @@ +export * from "./User/index.js"; diff --git a/yarn.lock b/yarn.lock index 7b828bdd..bead5c43 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1538,6 +1538,7 @@ __metadata: "@testing-library/react": "npm:^16.0.1" "@types/dinero.js": "npm:^1" "@types/jest": "npm:^29.5.12" + "@types/luxon": "npm:^3" "@types/react": "npm:^18.3.3" "@types/react-dom": "npm:^18" "@typescript-eslint/eslint-plugin": "npm:^7.18.0" @@ -1551,6 +1552,7 @@ __metadata: eslint-plugin-prettier: "npm:^5.2.1" jest: "npm:^29.7.0" jest-environment-jsdom: "npm:^29.7.0" + luxon: "npm:^3.5.0" object-code: "npm:^1.3.3" polytype: "npm:^0.17.0" prettier: "npm:^3.3.3" @@ -2850,7 +2852,7 @@ __metadata: languageName: node linkType: hard -"@types/luxon@npm:3.4.2": +"@types/luxon@npm:3.4.2, @types/luxon@npm:^3": version: 3.4.2 resolution: "@types/luxon@npm:3.4.2" checksum: 10/fd89566e3026559f2bc4ddcc1e70a2c16161905ed50be9473ec0cfbbbe919165041408c4f6e06c4bcf095445535052e2c099087c76b1b38e368127e618fc968d @@ -8128,7 +8130,7 @@ __metadata: languageName: node linkType: hard -"luxon@npm:~3.5.0": +"luxon@npm:^3.5.0, luxon@npm:~3.5.0": version: 3.5.0 resolution: "luxon@npm:3.5.0" checksum: 10/48f86e6c1c96815139f8559456a3354a276ba79bcef0ae0d4f2172f7652f3ba2be2237b0e103b8ea0b79b47715354ac9fac04eb1db3485dcc72d5110491dd47f