Added caching and implemented UserService
This commit is contained in:
114
src/domain/userService.schema.ts
Normal file
114
src/domain/userService.schema.ts
Normal file
@@ -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;
|
||||
}
|
||||
11
src/mocks/handlers/user.http.ts
Normal file
11
src/mocks/handlers/user.http.ts
Normal file
@@ -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(<Session[]>[{
|
||||
token: "sessionToken"
|
||||
}])
|
||||
}),
|
||||
]
|
||||
@@ -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
|
||||
]
|
||||
@@ -164,7 +164,6 @@ export class AuthService {
|
||||
});
|
||||
return resp.data.authCode
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
if (isAxiosError<GenericErrorBody>(e)) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
@@ -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<Chat[]> {
|
||||
try {
|
||||
const resp = await this.client.get<Chat[]>(`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<GenericErrorBody>(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
|
||||
|
||||
@@ -53,6 +53,9 @@ export class DMService {
|
||||
async get(from: number = 0): Promise<Message[]> {
|
||||
try {
|
||||
const resp = await this.client.get<Message[]>(`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<GenericErrorBody>(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
|
||||
|
||||
@@ -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[]>("network/get", <GetNetworksReq>{
|
||||
userid: this.userid,
|
||||
});
|
||||
this.database.set("networks", this.userid, JSON.stringify(resp.data))
|
||||
return resp.data
|
||||
} catch (e) {
|
||||
if (isAxiosError<GenericErrorBody>(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
|
||||
|
||||
@@ -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<GetResp> {
|
||||
try {
|
||||
const resp = await this.client.get<GetResp>(`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
|
||||
*/
|
||||
|
||||
12
src/services/userService.test.ts
Normal file
12
src/services/userService.test.ts
Normal file
@@ -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")
|
||||
});
|
||||
})
|
||||
253
src/services/userService.ts
Normal file
253
src/services/userService.ts
Normal file
@@ -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<void> {
|
||||
try {
|
||||
await this.client.patch("user/changeUsername", <ChangeUsernameReq>{
|
||||
userid: this.userid,
|
||||
newUsername: username,
|
||||
});
|
||||
return
|
||||
} catch (e) {
|
||||
if (isAxiosError<GenericErrorBody>(e)) {
|
||||
throw e;
|
||||
}
|
||||
throw new Error("Unexpected error")
|
||||
}
|
||||
}
|
||||
|
||||
async changeDisplayName(displayName: string): Promise<void> {
|
||||
try {
|
||||
await this.client.patch("user/changeDisplayName", <ChangeDisplayNameReq>{
|
||||
userid: this.userid,
|
||||
newDisplayName: displayName
|
||||
});
|
||||
return
|
||||
} catch (e) {
|
||||
if (isAxiosError<GenericErrorBody>(e)) {
|
||||
throw e;
|
||||
}
|
||||
throw new Error("Unexpected error")
|
||||
}
|
||||
}
|
||||
|
||||
async changePassword(newPassword: string, currentPassword: string): Promise<void> {
|
||||
try {
|
||||
await this.client.patch("user/changePassword", <ChangePasswordReq>{
|
||||
userid: this.userid,
|
||||
currentPassword: currentPassword,
|
||||
newPassword: newPassword,
|
||||
});
|
||||
return
|
||||
} catch (e) {
|
||||
if (isAxiosError<GenericErrorBody>(e)) {
|
||||
throw e;
|
||||
}
|
||||
throw new Error("Unexpected error")
|
||||
}
|
||||
}
|
||||
|
||||
async changeEmail(newMail: string, currentPassword: string): Promise<void|CurrNewCodeTestingResp> {
|
||||
try {
|
||||
const resp = await this.client.patch<CurrNewCodeTestingResp>("user/changeEmail", <ChangeEmailReq>{
|
||||
userid: this.userid,
|
||||
currentPassword: currentPassword,
|
||||
newMail: newMail,
|
||||
});
|
||||
if (resp.data.codeCurr != null) {
|
||||
return resp.data
|
||||
} else {
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
if (isAxiosError<GenericErrorBody>(e)) {
|
||||
throw e;
|
||||
}
|
||||
throw new Error("Unexpected error")
|
||||
}
|
||||
}
|
||||
|
||||
async uploadNewPfp(pfpId: string): Promise<void> {
|
||||
try {
|
||||
await this.client.patch("user/uploadNewPfp", <UploadNewPfpReq>{
|
||||
userid: this.userid,
|
||||
pfpId: pfpId,
|
||||
});
|
||||
return
|
||||
} catch (e) {
|
||||
if (isAxiosError<GenericErrorBody>(e)) {
|
||||
throw e;
|
||||
}
|
||||
throw new Error("Unexpected error")
|
||||
}
|
||||
}
|
||||
|
||||
async uploadNewPfpCdn(image: string | null, monogramLetter: string | null, monogramColors: RGB): Promise<string> {
|
||||
try {
|
||||
const resp = await this.cdnClient.post<UploadNewPfpCdnResp>("pfp", <UploadNewPfpCdnReq>{
|
||||
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<GenericErrorBody>(e)) {
|
||||
throw e;
|
||||
}
|
||||
throw new Error("Unexpected error")
|
||||
}
|
||||
}
|
||||
|
||||
async verifyEmailChange(vCodeCurrent: number, vCodeNew: number, newAddress: string): Promise<void> {
|
||||
try {
|
||||
await this.client.patch("user/verifyMailChange", <VerifyMailChangeReq>{
|
||||
userid: this.userid,
|
||||
newAddress: newAddress,
|
||||
vCodeCurrent: vCodeCurrent,
|
||||
vCodeNew: vCodeNew,
|
||||
});
|
||||
return
|
||||
} catch (e) {
|
||||
if (isAxiosError<GenericErrorBody>(e)) {
|
||||
throw e;
|
||||
}
|
||||
throw new Error("Unexpected error")
|
||||
}
|
||||
}
|
||||
|
||||
async delete(password: string): Promise<void> {
|
||||
try {
|
||||
await this.client.post("user/deleteAccount", <DeleteReq>{
|
||||
userid: this.userid,
|
||||
password: password
|
||||
});
|
||||
return
|
||||
} catch (e) {
|
||||
if (isAxiosError<GenericErrorBody>(e)) {
|
||||
throw e;
|
||||
}
|
||||
throw new Error("Unexpected error")
|
||||
}
|
||||
}
|
||||
|
||||
async registerFirebaseToken(fcmToken: string): Promise<void> {
|
||||
try {
|
||||
await this.client.post("user/registerFcmToken", <RegisterFCMTokenReq>{
|
||||
userid: this.userid,
|
||||
token: fcmToken,
|
||||
});
|
||||
return
|
||||
} catch (e) {
|
||||
if (isAxiosError<GenericErrorBody>(e)) {
|
||||
throw e;
|
||||
}
|
||||
throw new Error("Unexpected error")
|
||||
}
|
||||
}
|
||||
|
||||
async getSessions(): Promise<Session[]> {
|
||||
try {
|
||||
const resp = await this.client.post<Session[]>("user/getSessions", <GetSessionsReq>{
|
||||
userid: this.userid,
|
||||
});
|
||||
return resp.data
|
||||
} catch (e) {
|
||||
if (isAxiosError<GenericErrorBody>(e)) {
|
||||
throw e;
|
||||
}
|
||||
throw new Error("Unexpected error")
|
||||
}
|
||||
}
|
||||
|
||||
async toggleGIFSave(url: string): Promise<GIF> {
|
||||
try {
|
||||
const resp = await this.client.patch<GIF>("user/toggleGIFSave", <ToggleGifSaveReq>{
|
||||
userid: this.userid,
|
||||
url: url
|
||||
});
|
||||
return resp.data
|
||||
} catch (e) {
|
||||
if (isAxiosError<GenericErrorBody>(e)) {
|
||||
throw e;
|
||||
}
|
||||
throw new Error("Unexpected error")
|
||||
}
|
||||
}
|
||||
|
||||
async changePhoneNumber(currentPassword: string, newPhone: string): Promise<CurrNewCodeTestingResp|void> {
|
||||
try {
|
||||
const resp = await this.client.patch<CurrNewCodeTestingResp>("user/changePhone", <ChangePhoneReq>{
|
||||
userid: this.userid,
|
||||
newPhone: newPhone,
|
||||
currentPassword: currentPassword,
|
||||
});
|
||||
if (resp.data.codeCurr != null) {
|
||||
return resp.data
|
||||
} else {
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
if (isAxiosError<GenericErrorBody>(e)) {
|
||||
throw e;
|
||||
}
|
||||
throw new Error("Unexpected error")
|
||||
}
|
||||
}
|
||||
|
||||
async verifyPhoneNumberChange(newPhone: string, vCodeCurrent: number, vCodeNew: number): Promise<void> {
|
||||
try {
|
||||
const resp = await this.client.patch("user/verifyPhoneChange", <VerifyPhoneChange>{
|
||||
userid: this.userid,
|
||||
newPhone: newPhone,
|
||||
vCodeCurrent: vCodeCurrent,
|
||||
vCodeNew: vCodeNew,
|
||||
});
|
||||
return resp.data
|
||||
} catch (e) {
|
||||
if (isAxiosError<GenericErrorBody>(e)) {
|
||||
throw e;
|
||||
}
|
||||
throw new Error("Unexpected error")
|
||||
}
|
||||
}
|
||||
}
|
||||
50
tests/userService.test.ts
Normal file
50
tests/userService.test.ts
Normal file
@@ -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", <RGB>{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)
|
||||
}
|
||||
});
|
||||
})
|
||||
Reference in New Issue
Block a user