Finished implementing AuthService (Unit/Integration tests, documentation and code)

This commit is contained in:
2026-04-01 19:34:59 +02:00
parent 0852cbd4f0
commit 7b6760952e
11 changed files with 389 additions and 38 deletions

View File

@@ -1,5 +1,7 @@
import {describe, expect, it} from "vitest";
import {AuthService} from "./authService";
import {VerificationTypeEmail} from "../domain/authService.schema";
import {faker} from "@faker-js/faker/locale/en";
describe("AuthService", () => {
it("should return authMethods", async () => {
@@ -9,4 +11,69 @@ describe("AuthService", () => {
expect(methods.email).toBeTruthy()
expect(methods.password).toBeTruthy()
})
it("should send OTP code", async () => {
const service = new AuthService();
const code = await service.otpSendCode("bob@example.com", VerificationTypeEmail)
expect(code).toBe(5000)
})
it("should send password reset code", async () => {
const service = new AuthService();
const code = await service.resetPassword("bob@example.com")
expect(code).toBe(5000)
})
it("should send PLE code", async () => {
const service = new AuthService();
const code = await service.pleSendVCode("bob@example.com", VerificationTypeEmail)
expect(code).toBe(5000)
})
it("should throw on wrong OTP code", async () => {
const service = new AuthService();
await expect(service.otpVerifyCode("bob@example.com", VerificationTypeEmail, 9999)).rejects.toThrow()
})
it("should throw on wrong password reset code", async () => {
const service = new AuthService();
await expect(service.verifyPasswordReset("bob@example.com", 9999, "newPassword")).rejects.toThrow()
})
it("should throw on wrong PLE code", async () => {
const service = new AuthService();
await expect(service.pleVerifyCode("bob@example.com", VerificationTypeEmail, 9999)).rejects.toThrow()
})
it("should throw on wrong password", async () => {
const service = new AuthService();
await expect(service.loginPasswordAuth("bob@example.com", "wrongPasswd")).rejects.toThrow();
})
it("should detect taken username", async () => {
const service = new AuthService();
const taken = await service.isUsernameUsed("username")
expect(taken).toBeTruthy()
})
it("should detect taken e-mail address", async () => {
const service = new AuthService();
const taken = await service.isEmailUsed("taken@example.com")
expect(taken).toBeTruthy()
})
it("should register passwordless account", async () => {
const service = new AuthService();
const username = faker.internet.username()
const displayName = faker.internet.displayName()
const userData = await service.finishPLEAccount(
"bob@example.com",
"goodAuthCode",
username,
displayName,
)
expect(userData.username).toBe(username)
expect(userData.displayName).toBe(displayName)
expect(userData.token).toBe("token")
})
})

View File

@@ -1,15 +1,20 @@
import {getClient} from "../core/http";
import {
AuthMethods, FinishPleAccountReq,
LoginPasswordAuthReq, LoginWithApple, LoginWithGoogleReq,
OtpPleSendCodeReq,
OtpPleVerifyCodeReq, PleVerifyCodeResp, RegisterReq, ResetPasswordReq, ResetPasswordResp,
LoginPasswordAuthReq, LoginWithApple, LoginWithGoogleReq, OtpPleCodeSendTestingResp,
OtpSendCodeReq,
OtpVerifyCodeReq, PleSendCodeReq,
PleVerifyCodeReq, PleVerifyCodeResp, RegisterReq, ResetPasswordReq, ResetPasswordResp,
SignInSuccessResp, UserDataValidationResp, VerifyPasswordResetReq
} from "../domain/authService.schema";
import {isAxiosError} from "axios";
import {GenericErrorBody, GenericSuccessBody} from "../domain/http.schema";
export class AuthService {
/**
* Fetches the available authentication methods for a user
* @param unameMailPhone Username, e-mail address or phone number in international format
*/
async getAuthMethods(unameMailPhone: string): Promise<AuthMethods> {
try {
const resp = await getClient().get<AuthMethods>(`user/authOptions?unameMailPhone=${unameMailPhone}`);
@@ -22,13 +27,23 @@ export class AuthService {
}
}
async otpSendCode(unameMailPhone: string, type: number): Promise<void> {
/**
* Starts the one-time-password sign-in procedure by sending a code to the preferred destination
* @param unameMailPhone Username, e-mail address or phone number in international format
* @param type Whether to send the code via e-mail or SMS
* @returns void in production. Number (the code) is only returned when unit testing with the API in testing mode
*/
async otpSendCode(unameMailPhone: string, type: number): Promise<void|number> {
try {
await getClient().post<GenericSuccessBody>("v2/user/otpSendCode", <OtpPleSendCodeReq>{
unameMailPhone: unameMailPhone,
const resp = await getClient().post<OtpPleCodeSendTestingResp>("v2/user/otpSendCode", <OtpSendCodeReq>{
usernamePhoneMail: unameMailPhone,
type: type
});
return
if (resp.data.code != null) {
return resp.data.code
} else {
return
}
} catch (e) {
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
@@ -37,14 +52,20 @@ export class AuthService {
}
}
async otpVerifyCode(unameMailPhone: string, type: number, code: number): Promise<void> {
/**
* With the provided code, this registers a new session and returns user data and auth token
* @param unameMailPhone Username, e-mail address or phone number in international format
* @param type Whether to send the code via e-mail or SMS
* @param code The verification code
*/
async otpVerifyCode(unameMailPhone: string, type: number, code: number): Promise<SignInSuccessResp> {
try {
await getClient().post<GenericSuccessBody>("v2/user/otpVerifyCode", <OtpPleVerifyCodeReq>{
unameMailPhone: unameMailPhone,
const resp = await getClient().post<SignInSuccessResp>("v2/user/otpVerifyCode", <OtpVerifyCodeReq>{
usernamePhoneMail: unameMailPhone,
type: type,
code: code
});
return
return resp.data
} catch (e) {
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
@@ -53,6 +74,11 @@ export class AuthService {
}
}
/**
* Signs into a Chatenium account via password
* @param usernamePhoneMail Username, e-mail address or phone number in international format
* @param password The password associated with the account
*/
async loginPasswordAuth(usernamePhoneMail: string, password: string): Promise<SignInSuccessResp> {
try {
const resp = await getClient().post<SignInSuccessResp>("v2/user/loginPasswordAuth", <LoginPasswordAuthReq>{
@@ -68,6 +94,9 @@ export class AuthService {
}
}
/**
* Checks whether the provided username is already taken
*/
async isUsernameUsed(username: string): Promise<Boolean> {
try {
const resp = await getClient().get<UserDataValidationResp>(`v2/user/unameUsage?username=${username}`);
@@ -80,6 +109,9 @@ export class AuthService {
}
}
/**
* Checks whether the provided e-mail address is already taken
*/
async isEmailUsed(email: string): Promise<Boolean> {
try {
const resp = await getClient().get<UserDataValidationResp>(`v2/user/emailUsage?email=${email}`);
@@ -92,13 +124,23 @@ export class AuthService {
}
}
async pleSendVCode(phoneMail: string, type: number): Promise<void> {
/**
* Starts the passwordless register procedure by sending a code to the preferred destination
* @param phoneMail E-mail address or phone number in international format
* @param type Whether to send the code via e-mail or SMS
* @returns void in production. Number (the code) is only returned when unit testing with the API in testing mode
*/
async pleSendVCode(phoneMail: string, type: number): Promise<void|number> {
try {
await getClient().post<UserDataValidationResp>("v2/user/pleSendVCode", <OtpPleSendCodeReq>{
unameMailPhone: phoneMail,
const resp = await getClient().post<OtpPleCodeSendTestingResp>("v2/user/pleSendVCode", <PleSendCodeReq>{
phoneMail: phoneMail,
type: type,
});
return
if (resp.data.code == null) {
return
} else {
return resp.data.code
}
} catch (e) {
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
@@ -107,10 +149,16 @@ export class AuthService {
}
}
/**
* Verifies if the provided code is valid to continue the procedure
* @param phoneMail E-mail address or phone number in international format
* @param type Whether to send the code via e-mail or SMS
* @param code The code that was sent out
*/
async pleVerifyCode(phoneMail: string, type: number, code: number): Promise<string> {
try {
const resp = await getClient().post<PleVerifyCodeResp>("v2/user/pleVerifyCode", <OtpPleVerifyCodeReq>{
unameMailPhone: phoneMail,
const resp = await getClient().post<PleVerifyCodeResp>("v2/user/pleVerifyCode", <PleVerifyCodeReq>{
phoneMail: phoneMail,
type: type,
code: code
});
@@ -123,7 +171,14 @@ export class AuthService {
}
}
async finishPLEAccount(phoneMail: string, type: number, authCode: string, username: string, displayName: string): Promise<SignInSuccessResp> {
/**
* Finishes the procedure by permanently registering the new account in the database
* @param phoneMail E-mail address or phone number in international format
* @param authCode Provided in pleVerifyCode
* @param username Must be provided by the user
* @param displayName Let the user optionally provide it
*/
async finishPLEAccount(phoneMail: string, authCode: string, username: string, displayName: string|null): Promise<SignInSuccessResp> {
try {
const resp = await getClient().post<SignInSuccessResp>("v2/user/finishPLEAccount", <FinishPleAccountReq>{
phoneMail: phoneMail,
@@ -140,6 +195,10 @@ export class AuthService {
}
}
/**
* A social login method via Google
* @param code Provided by the Google JavaScript API
*/
async loginWithGoogle(code: string): Promise<SignInSuccessResp> {
try {
const resp = await getClient().post<SignInSuccessResp>("user/loginWithGoogle", <LoginWithGoogleReq>{
@@ -154,6 +213,10 @@ export class AuthService {
}
}
/**
* A social login method via Apple
* @param code Provided by AppleJS
*/
async loginWithApple(code: string): Promise<SignInSuccessResp> {
try {
const resp = await getClient().post<SignInSuccessResp>("user/loginWithGoogle", <LoginWithApple>{
@@ -169,7 +232,13 @@ export class AuthService {
}
}
async register(username: string, displayName: string, password: string): Promise<SignInSuccessResp> {
/**
* A quick way to register a new Chatenium account
* @param username Must be provided by the user
* @param password Must be provided by the user
* @param displayName Let the user optionally provide it
*/
async register(username: string, password: string, displayName: string|null): Promise<SignInSuccessResp> {
try {
const resp = await getClient().post<SignInSuccessResp>("v2/user/register", <RegisterReq>{
username: username,
@@ -185,12 +254,22 @@ export class AuthService {
}
}
async resetPassword(unameMailPhone: string): Promise<string> {
/**
* Starts the password reset procedure by sending out a code via phone (or via e-mail if SMS is not available)
* @param unameMailPhone Username, e-mail address or phone number
* @returns void in production. Number (the code) is only returned when unit testing with the API in testing mode
*/
async resetPassword(unameMailPhone: string): Promise<void|number> {
try {
const resp = await getClient().post<ResetPasswordResp>("user/resetPassword", <ResetPasswordReq>{
const resp = await getClient().post<OtpPleCodeSendTestingResp>("user/resetPassword", <ResetPasswordReq>{
unameMailPhone: unameMailPhone,
});
return resp.data.userid
if (resp.data.code != null) {
return resp.data.code
} else {
return
}
} catch (e) {
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
@@ -199,9 +278,15 @@ export class AuthService {
}
}
/**
* Finishes the password reset procedure by verifying the code and providing a new password
* @param unameMailPhone Username, e-mail address or phone number
* @param code The code that was sent out
* @param newPassword Must be provided by the user
*/
async verifyPasswordReset(unameMailPhone: string, code: number, newPassword: string): Promise<void> {
try {
await getClient().post<GenericSuccessBody>("user/resetPassword", <VerifyPasswordResetReq>{
await getClient().post<GenericSuccessBody>("user/verifyResetCode", <VerifyPasswordResetReq>{
unameMailPhone: unameMailPhone,
vCode: code,
newPassword: newPassword,