diff --git a/src/domain/userService.schema.ts b/src/domain/userService.schema.ts new file mode 100644 index 0000000..6cd30ab --- /dev/null +++ b/src/domain/userService.schema.ts @@ -0,0 +1,114 @@ +import {TimeStamp} from "./common.schema"; + +export interface ChangeUsernameReq { + newUsername: string; + userid: string; +} + +export interface ChangeDisplayNameReq { + newDisplayName: string; + userid: string; +} + +export interface ChangePasswordReq { + newPassword: string; + currentPassword: string; + userid: string; +} + +export interface ChangeEmailReq { + currentPassword: string; + newMail: string; + userid: string; +} + +export interface VerifyMailChangeReq { + userid: string; + vCodeCurrent: number; + vCodeNew: number; + newAddress: string; +} + +export interface ChangePhoneReq { + currentPassword: string; + newPhone: string; + userid: string; +} + +export interface VerifyPhoneChange { + userid: string; + vCodeCurrent: number; + vCodeNew: number; + newPhone: string; +} + +export interface UploadNewPfpReq { + userid: string; + pfpId: string; +} + +export interface UploadNewPfpCdnReq { + userid: string; + data: string | null; + isImage: boolean; + monogramLetter: string | null; + monogramColors: string | null; +} + +export interface DeleteReq { + userid: string; + password: string; +} + +export interface RegisterFCMTokenReq { + userid: string; + token: string; + language: string; +} + +export interface GetSessionsReq { + userid: string; +} + +export interface UpdateUserDataReq { + userid: string; +} + +export interface ToggleGifSaveReq { + userid: string; + url: string; +} + +export interface UploadNewPfpCdnResp { + pfpId: string; +} + +export interface Session { + token: string; + os: string; + language: string; + login_at: TimeStamp | string; +} + +export interface GIF { + gifId: string; + url: string; + path: string; +} + +export interface CurrNewCodeTestingResp { + codeCurr: number|null; + codeNew: number|null; +} + +export interface PersonalUserData { + userid: string; + username: string; + displayName: string; + pfp: string; + pictureDiscovery: boolean; + gifs: GIF[]; + passwordSet: boolean; + emailSet: boolean; + phoneSet: boolean; +} \ No newline at end of file diff --git a/src/mocks/handlers/user.http.ts b/src/mocks/handlers/user.http.ts new file mode 100644 index 0000000..1e94e1e --- /dev/null +++ b/src/mocks/handlers/user.http.ts @@ -0,0 +1,11 @@ +import {http, HttpResponse} from "msw"; +import {GetResp} from "../../domain/pictureService.schema"; +import {Session} from "../../domain/userService.schema"; + +export const userHandler = [ + http.post('*/user/getSessions', () => { + return HttpResponse.json([{ + token: "sessionToken" + }]) + }), +] \ No newline at end of file diff --git a/src/mocks/index.ts b/src/mocks/index.ts index 025d479..3734b4c 100644 --- a/src/mocks/index.ts +++ b/src/mocks/index.ts @@ -5,6 +5,7 @@ import {callHandlers} from "./handlers/call.http"; import {fileUploadHandlers} from "./handlers/fUpl.http"; import {chatHandlers} from "./handlers/chat.http"; import {dmHandlers} from "./handlers/dm.http"; +import {userHandler} from "./handlers/user.http"; export const allHandlers = [ ...authHandlers, @@ -13,5 +14,6 @@ export const allHandlers = [ ...callHandlers, ...fileUploadHandlers, ...chatHandlers, - ...dmHandlers + ...dmHandlers, + ...userHandler ] \ No newline at end of file diff --git a/src/services/authService.ts b/src/services/authService.ts index e93af0b..c93fa45 100644 --- a/src/services/authService.ts +++ b/src/services/authService.ts @@ -164,7 +164,6 @@ export class AuthService { }); return resp.data.authCode } catch (e) { - console.log(e) if (isAxiosError(e)) { throw e; } diff --git a/src/services/chatService.ts b/src/services/chatService.ts index 0a121b8..f44db01 100644 --- a/src/services/chatService.ts +++ b/src/services/chatService.ts @@ -12,6 +12,7 @@ import { StartNewReq, ToggleChatMuteReq } from "../domain/chatService.schema"; +import {Message} from "../domain/dmService.schema"; /** * ChatService is an exception because it's one instance for all chats because it's unnecessary to create a new instance for each chat @@ -47,9 +48,9 @@ export class ChatService { async get(): Promise { try { const resp = await this.client.get(`chat/get?userid=${this.userid}`); + this.database.set("chats", this.userid, JSON.stringify(resp.data)) return resp.data } catch (e) { - console.log(e) if (isAxiosError(e)) { throw e; } @@ -57,6 +58,15 @@ export class ChatService { } } + getQuick(): Message[] { + const chats = this.database.get("chats", this.userid) + if (chats) { + return JSON.parse(chats) + } else { + throw new Error("No chats in database") + } + } + /** * Gets the availability of the specified user * @param userid diff --git a/src/services/dmService.ts b/src/services/dmService.ts index b37da53..2413561 100644 --- a/src/services/dmService.ts +++ b/src/services/dmService.ts @@ -53,6 +53,9 @@ export class DMService { async get(from: number = 0): Promise { try { const resp = await this.client.get(`chat/dm/messages?chatid=${this.chatid}&userid=${this.userid}&from=${from}`); + if (from == 0) { + this.database.set("messages", this.chatid, JSON.stringify(resp.data)) + } return resp.data } catch (e) { if (isAxiosError(e)) { @@ -62,6 +65,15 @@ export class DMService { } } + getQuick(): Message[] { + const messages = this.database.get("messages", this.chatid) + if (messages) { + return JSON.parse(messages) + } else { + throw new Error("No messages in database") + } + } + /** * Fetches the position of the specified message which can be used in get() to fetch an old message with it's surrounding messages * @param messageId diff --git a/src/services/networkService.ts b/src/services/networkService.ts index 226d5e9..c8452f0 100644 --- a/src/services/networkService.ts +++ b/src/services/networkService.ts @@ -19,6 +19,7 @@ import { import {PublicUserData, RGB} from "../domain/common.schema"; import {WebSocketHandler} from "../core/webSocketHandler"; import {MessageListener} from "../domain/websocket.schema"; +import {Message} from "../domain/dmService.schema"; export class NetworkService { userid: string; @@ -97,6 +98,7 @@ export class NetworkService { const resp = await this.client.post("network/get", { userid: this.userid, }); + this.database.set("networks", this.userid, JSON.stringify(resp.data)) return resp.data } catch (e) { if (isAxiosError(e)) { @@ -106,6 +108,15 @@ export class NetworkService { } } + getQuick(): Message[] { + const networks = this.database.get("networks", this.userid) + if (networks) { + return JSON.parse(networks) + } else { + throw new Error("No networks in database") + } + } + /** * Accepts the invite and joins the network * @param inviteId diff --git a/src/services/pictureService.ts b/src/services/pictureService.ts index 885a3f6..badb2fe 100644 --- a/src/services/pictureService.ts +++ b/src/services/pictureService.ts @@ -12,6 +12,7 @@ import { GetResp, PostCommentReq, ToggleFollowReq, TogglePictureLikeReq, UploadImageReq } from "../domain/pictureService.schema"; import {environment} from "../core/environment"; +import {Message} from "../domain/dmService.schema"; export class PictureService { userid: string; @@ -42,6 +43,7 @@ export class PictureService { async get(): Promise { try { const resp = await this.client.get(`picture/pictures?userid=${this.userid}&target=${this.uploaderId}`); + this.database.set("pictures", this.uploaderId, JSON.stringify(resp.data)) return resp.data } catch (e) { console.log(e) @@ -52,6 +54,15 @@ export class PictureService { } } + getQuick(): Message[] { + const pictures = this.database.get("pictures", this.uploaderId) + if (pictures) { + return JSON.parse(pictures) + } else { + throw new Error("No pictures in database") + } + } + /** * Fetches the top 10 most liked and newest pictures */ diff --git a/src/services/userService.test.ts b/src/services/userService.test.ts new file mode 100644 index 0000000..075a139 --- /dev/null +++ b/src/services/userService.test.ts @@ -0,0 +1,12 @@ +import {describe, expect, it} from "vitest"; +import {UserService} from "./userService"; +import {DatabaseMock} from "../mocks/storage/database"; + +describe("UserService", () => { + const service = new UserService("", "", new DatabaseMock()) + + it('should get all sessions', async () => { + const sessions = await service.getSessions() + expect(sessions[0].token).toBe("sessionToken") + }); +}) \ No newline at end of file diff --git a/src/services/userService.ts b/src/services/userService.ts new file mode 100644 index 0000000..c4ea931 --- /dev/null +++ b/src/services/userService.ts @@ -0,0 +1,253 @@ +import {DatabaseAPI} from "../storage/database"; +import {AxiosInstance, isAxiosError} from "axios"; +import {MessageListener} from "../domain/websocket.schema"; +import {getClient} from "../core/http"; +import {WebSocketHandler} from "../core/webSocketHandler"; +import {DeleteCategoryReq} from "../domain/networkService.schema"; +import {GenericErrorBody} from "../domain/http.schema"; +import { + ChangeDisplayNameReq, + ChangeEmailReq, + ChangePasswordReq, ChangePhoneReq, + ChangeUsernameReq, CurrNewCodeTestingResp, DeleteReq, GetSessionsReq, GIF, RegisterFCMTokenReq, Session, + ToggleGifSaveReq, UploadNewPfpCdnReq, UploadNewPfpCdnResp, + UploadNewPfpReq, VerifyMailChangeReq, VerifyPhoneChange +} from "../domain/userService.schema"; +import {RGB} from "../domain/common.schema"; +import {OtpPleCodeSendTestingResp} from "../domain/authService.schema"; + +export class UserService { + userid: string; + database: DatabaseAPI; + client: AxiosInstance + cdnClient: AxiosInstance + + constructor(userid: string, token: string, database: DatabaseAPI) { + this.userid = userid; + this.database = database; + this.client = getClient(false).create({ + headers: { + "Authorization": token, + } + }) + this.cdnClient = getClient(true).create({ + headers: { + "Authorization": token, + } + }) + } + + async changeUsername(username: string): Promise { + try { + await this.client.patch("user/changeUsername", { + userid: this.userid, + newUsername: username, + }); + return + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + async changeDisplayName(displayName: string): Promise { + try { + await this.client.patch("user/changeDisplayName", { + userid: this.userid, + newDisplayName: displayName + }); + return + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + async changePassword(newPassword: string, currentPassword: string): Promise { + try { + await this.client.patch("user/changePassword", { + userid: this.userid, + currentPassword: currentPassword, + newPassword: newPassword, + }); + return + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + async changeEmail(newMail: string, currentPassword: string): Promise { + try { + const resp = await this.client.patch("user/changeEmail", { + userid: this.userid, + currentPassword: currentPassword, + newMail: newMail, + }); + if (resp.data.codeCurr != null) { + return resp.data + } else { + return + } + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + async uploadNewPfp(pfpId: string): Promise { + try { + await this.client.patch("user/uploadNewPfp", { + userid: this.userid, + pfpId: pfpId, + }); + return + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + async uploadNewPfpCdn(image: string | null, monogramLetter: string | null, monogramColors: RGB): Promise { + try { + const resp = await this.cdnClient.post("pfp", { + userid: this.userid, + data: image, + monogramColors: JSON.stringify(monogramColors), + isImage: image !== null, + monogramLetter: monogramLetter, + }); + console.log(resp.data.pfpId, "PFPID") + return resp.data.pfpId + } catch (e) { + console.log(e) + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + async verifyEmailChange(vCodeCurrent: number, vCodeNew: number, newAddress: string): Promise { + try { + await this.client.patch("user/verifyMailChange", { + userid: this.userid, + newAddress: newAddress, + vCodeCurrent: vCodeCurrent, + vCodeNew: vCodeNew, + }); + return + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + async delete(password: string): Promise { + try { + await this.client.post("user/deleteAccount", { + userid: this.userid, + password: password + }); + return + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + async registerFirebaseToken(fcmToken: string): Promise { + try { + await this.client.post("user/registerFcmToken", { + userid: this.userid, + token: fcmToken, + }); + return + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + async getSessions(): Promise { + try { + const resp = await this.client.post("user/getSessions", { + userid: this.userid, + }); + return resp.data + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + async toggleGIFSave(url: string): Promise { + try { + const resp = await this.client.patch("user/toggleGIFSave", { + userid: this.userid, + url: url + }); + return resp.data + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + async changePhoneNumber(currentPassword: string, newPhone: string): Promise { + try { + const resp = await this.client.patch("user/changePhone", { + userid: this.userid, + newPhone: newPhone, + currentPassword: currentPassword, + }); + if (resp.data.codeCurr != null) { + return resp.data + } else { + return + } + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + async verifyPhoneNumberChange(newPhone: string, vCodeCurrent: number, vCodeNew: number): Promise { + try { + const resp = await this.client.patch("user/verifyPhoneChange", { + userid: this.userid, + newPhone: newPhone, + vCodeCurrent: vCodeCurrent, + vCodeNew: vCodeNew, + }); + return resp.data + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } +} \ No newline at end of file diff --git a/tests/userService.test.ts b/tests/userService.test.ts new file mode 100644 index 0000000..b0ebab8 --- /dev/null +++ b/tests/userService.test.ts @@ -0,0 +1,50 @@ +import {describe, expect, it} from "vitest"; +import {UserService} from "../src/services/userService"; +import {DatabaseMock} from "../src/mocks/storage/database"; +import {RGB} from "../src/domain/common.schema" + +const USER_SERVICE_TESTING_USER_ID = "000000000000000000000000" +const USER_SERVICE_TESTING_TOKEN = "testingToken" + + +describe("UserService Integration Testing", () => { + const service = new UserService(USER_SERVICE_TESTING_USER_ID, USER_SERVICE_TESTING_TOKEN, new DatabaseMock()) + + it('should not throw on username change', async () => { + await service.changeUsername("bob2") + }); + + it('should not throw on displayName change', async () => { + await service.changeDisplayName("New Display Name") + }); + + it('should set new password', async () => { + await service.changePassword("newPasswd", "") // The filler user doesn't have a password set yet + }); + + it('should set new e-mail', async () => { + const code = await service.changeEmail("bob2@example.com", "") + expect(code).not.toBeNull() + if (code != null) { + await service.verifyEmailChange(code.codeCurr??0, code.codeNew??0, "bob2@example.com") + } + }); + + it('should upload a new pfp', async () => { + const pfpId = await service.uploadNewPfpCdn(null, "A", {r: 255, g: 255, b: 255}) + await service.uploadNewPfp(pfpId) + }); + + it('should get sessions', async () => { + const sessions = await service.getSessions() + expect(sessions[0].token).toBe(USER_SERVICE_TESTING_TOKEN) + }); + + it('should set new phone', async () => { + const code = await service.changePhoneNumber("", "+36201234567") + expect(code).not.toBeNull() + if (code != null) { + await service.verifyPhoneNumberChange("+36201234567", code.codeCurr??0, code.codeNew??0) + } + }); +}) \ No newline at end of file