Compare commits

...

2 Commits

Author SHA1 Message Date
grimhilt
e43ab6cfe1 change validator api 2023-04-13 10:58:58 +02:00
grimhilt
4e79ab12dc add button to set seen flag on front 2023-04-12 19:01:40 +02:00
13 changed files with 197 additions and 69 deletions

15
back/abl/Messages-abl.ts Normal file
View File

@ -0,0 +1,15 @@
import statusCode from "../utils/statusCodes";
import { Response } from "express";
export default class Message {
static async addFlag(body, res: Response) {
console.log("hit")
res.status(statusCode.OK).send();
}
static async removeFlag(body, res: Response) {
console.log("hit")
res.status(statusCode.OK).send();
}
}

View File

@ -5,10 +5,12 @@ import { Response } from "express";
export async function messages(body, res: Response) { export async function messages(body, res: Response) {
const { roomId } = body; const { roomId } = body;
getMessages(roomId).then((messages) => { getMessages(roomId)
res.status(statusCode.OK).json(messages); .then((messages) => {
}).catch((err) => { res.status(statusCode.OK).json(messages);
logger.err(err) })
res.status(statusCode.INTERNAL_SERVER_ERROR); .catch((err) => {
}); logger.err(err);
} res.status(statusCode.INTERNAL_SERVER_ERROR);
});
}

View File

@ -1,29 +1,18 @@
import statusCodes from "../utils/statusCodes";
import express from "express"; import express from "express";
const router = express.Router(); const router = express.Router();
import { rooms } from "../abl/rooms"; import { rooms } from "../abl/rooms";
import Message from "../abl/Messages-abl";
import { messages } from "../abl/messages"; import { messages } from "../abl/messages";
import { members } from "../abl/members"; import { members } from "../abl/members";
import {
validateCreateAccount,
validateGetAccounts,
validateGetMembers,
validateGetMessages,
validateGetRooms,
} from "../validator/validator";
import Account from "../abl/Account-abl"; import Account from "../abl/Account-abl";
import validator from "../validator/validator";
/** /**
* Return all mailboxes and folders for an user * Return all mailboxes and folders for an user
*/ */
router.get("/accounts", async (req, res) => { router.get("/accounts", async (req, res) => {
const valid = validateGetAccounts(req.params); await validator.validate("getAccounts", req.params, res, Account.getAll);
if (!valid) {
res.status(statusCodes.NOT_ACCEPTABLE).send({ error: validateGetAccounts.errors });
} else {
await Account.getAll(req.params, res);
}
}); });
/** /**
@ -31,48 +20,36 @@ router.get("/accounts", async (req, res) => {
*/ */
router.get("/:mailboxId/rooms", async (req, res) => { router.get("/:mailboxId/rooms", async (req, res) => {
// todo offet limit // todo offet limit
const valid = validateGetRooms(req.params); await validator.validate("getRooms", req.params, res, rooms);
if (!valid) {
res.status(statusCodes.NOT_ACCEPTABLE).send({ error: validateGetRooms.errors });
} else {
await rooms(req.params, res);
}
}); });
/** /**
* Return all messages from a room * Return all messages from a room
*/ */
router.get("/:roomId/messages", async (req, res) => { router.get("/:roomId/messages", async (req, res) => {
const valid = validateGetMessages(req.params); await validator.validate("getMessages", req.params, res, messages);
if (!valid) {
res.status(statusCodes.NOT_ACCEPTABLE).send({ error: validateGetMessages.errors });
} else {
await messages(req.params, res);
}
}); });
/** /**
* Return all members from a room * Return all members from a room
*/ */
router.get("/:roomId/members", async (req, res) => { router.get("/:roomId/members", async (req, res) => {
const valid = validateGetMembers(req.params); await validator.validate("getMembers", req.params, res, members);
if (!valid) {
res.status(statusCodes.NOT_ACCEPTABLE).send({ error: validateGetMembers.errors });
} else {
await members(req.params, res);
}
}); });
/** /**
* Register a new mailbox inside the app * Register a new mailbox inside the app
*/ */
router.post("/account", async (req, res) => { router.post("/account", async (req, res) => {
const valid = validateCreateAccount(req.body); await validator.validate("createAccount", req.body, res, Account.register);
if (!valid) { });
res.status(statusCodes.NOT_ACCEPTABLE).send({ error: validateCreateAccount.errors });
} else { router.post("/addFlag", async (req, res) => {
await Account.register(req.body, res); await validator.validate("addFlag", req.body, res, Message.addFlag);
} });
router.post("/removeFlag", async (req, res) => {
await validator.validate("removeFlag", req.body, res, Message.removeFlag);
}); });
export default router; export default router;

View File

@ -0,0 +1,20 @@
{
"type": "object",
"properties": {
"mailboxId": {
"type": "number"
},
"messageId": {
"type": "number"
},
"flag": {
"type": "string"
}
},
"required": [
"mailboxId",
"messageId",
"flag"
],
"additionalProperties": false
}

View File

@ -8,9 +8,55 @@ import getAccountSchema from "./schemas/getAccounts-schema.json";
import getRoomSchema from "./schemas/getRooms-schema.json"; import getRoomSchema from "./schemas/getRooms-schema.json";
import getMessagesSchema from "./schemas/getMessages-schema.json"; import getMessagesSchema from "./schemas/getMessages-schema.json";
import getMembersSchema from "./schemas/getMembers-schema.json"; import getMembersSchema from "./schemas/getMembers-schema.json";
import setFlagSchema from "./schemas/setFlag-schema.json";
import { Request, Response } from "express";
import statusCodes from "../utils/statusCodes";
import logger from "../system/Logger";
export const validateCreateAccount = ajv.compile(createAccountSchema); export const validateCreateAccount = ajv.compile(createAccountSchema);
export const validateGetAccounts = ajv.compile(getAccountSchema); export const validateGetAccounts = ajv.compile(getAccountSchema);
export const validateGetRooms = ajv.compile(getRoomSchema); export const validateGetRooms = ajv.compile(getRoomSchema);
export const validateGetMessages = ajv.compile(getMessagesSchema); export const validateGetMessages = ajv.compile(getMessagesSchema);
export const validateGetMembers = ajv.compile(getMembersSchema); export const validateGetMembers = ajv.compile(getMembersSchema);
export const validateSetFlag = ajv.compile(setFlagSchema);
class Validator {
_getSchema(name: string): any {
switch (name) {
case "createAccount":
return validateCreateAccount;
case "getAccounts":
return validateGetAccounts;
case "getRooms":
return validateGetRooms;
case "getMessages":
return validateGetMessages;
case "getMembers":
return validateGetMembers;
case "addFlag":
case "removeFlag":
return validateSetFlag;
default:
logger.err(`Schema ${name} not found`);
break;
}
}
async validate(
schemaName: string,
args: any,
res: Response,
callback: (body: any, res: Response) => Promise<void>,
): Promise<void> {
const validator = this._getSchema(schemaName);
const valid = validator(args);
if (!valid) {
res.status(statusCodes.NOT_ACCEPTABLE).send({ error: validator.errors });
} else {
await callback(args, res);
}
}
}
const validator = new Validator();
export default validator;

View File

@ -9,7 +9,6 @@ const props = defineProps({
const iframe = ref<HTMLIFrameElement>(); const iframe = ref<HTMLIFrameElement>();
// todo dompurify
// background vs color // background vs color
const htmlDefault = (html: string) => { const htmlDefault = (html: string) => {
return ` return `
@ -33,7 +32,9 @@ function setIframeContent(content: string | undefined) {
if (!content) return; if (!content) return;
const doc = iframe.value.contentDocument || iframe.value.contentWindow?.document; const doc = iframe.value.contentDocument || iframe.value.contentWindow?.document;
if (!doc) return; if (!doc) return;
const html = DOMPurify.sanitize(content); // todo dompurify for image
const html = DOMPurify.sanitize(content, { FORBID_TAGS: ["style"] });
doc.open(); doc.open();
doc.write(htmlDefault(html)); doc.write(htmlDefault(html));
doc.close(); doc.close();

View File

@ -1,13 +1,17 @@
<script setup lang="ts"> <script setup lang="ts">
import { defineProps, ref, PropType } from "vue"; import { defineProps, PropType } from "vue";
import { decodeEmojis } from "../../../utils/string"; import { decodeEmojis } from "../../../utils/string";
import { removeDuplicates } from "../../../utils/array"; import { removeDuplicates } from "../../../utils/array";
import { Address, Message } from "@/store/models/model"; import { Address, Message } from "@/store/models/model";
import Content from "./Content.vue"; import Content from "./Content.vue";
import Options from "./Options.vue";
import { isSeenFc } from "@/utils/flagsUtils";
const props = defineProps({ const props = defineProps({
msg: Object as PropType<Message>, msg: Object as PropType<Message>,
members: Array as PropType<Address[]>, members: Array as PropType<Address[]>,
mailboxId: Number,
roomId: Number,
}); });
const displayAddresses = (addressIds: string[] | undefined): string => { const displayAddresses = (addressIds: string[] | undefined): string => {
@ -22,15 +26,13 @@ const displayAddresses = (addressIds: string[] | undefined): string => {
}; };
const classes = (): string => { const classes = (): string => {
const flags = props.msg?.flags?.split(","); const flags = props.msg?.flags;
// not flags implies no seen flag
if (!flags) return "msg-notSeen";
// Important takes the priority on Seen flag // Important takes the priority on Seen flag
if (flags.includes("\\Important")) { if (flags?.includes("\\Important")) {
return "msg-important"; return "msg-important";
} else if (!flags.includes("\\Seen")) { }
if (!isSeenFc(flags)) {
return "msg-notSeen"; return "msg-notSeen";
} }
return "msg-basic"; return "msg-basic";
@ -64,7 +66,7 @@ const classes = (): string => {
</div> </div>
<div class="content" :class="[classes()]"> <div class="content" :class="[classes()]">
<Content :content="props.msg?.content" /> <Content :content="props.msg?.content" />
<div class="options">options {{ props?.msg?.flags }}</div> <Options class="options" :mailboxId="props.mailboxId" :roomId="props.roomId" :msg="props.msg" />
</div> </div>
</div> </div>
</template> </template>

View File

@ -1,23 +1,49 @@
<script setup lang="ts"> <script setup lang="ts">
import { defineProps, onMounted, ref, watch, PropType } from "vue"; import { defineProps, PropType } from "vue";
import { decodeEmojis } from "../../../utils/string"; import { Message } from "@/store/models/model";
import { removeDuplicates } from "../../../utils/array"; import API from "@/services/imapAPI";
import DOMPurify from "dompurify"; import store from "@/store/store";
import { Address, Message } from "@/store/models/model"; import { isSeenFc } from "@/utils/flagsUtils";
const props = defineProps({ const props = defineProps({
msg: Object as PropType<Message>, msg: Object as PropType<Message>,
mailboxId: Number,
roomId: Number,
}); });
const setFlag = (flag: string) => {
// todo loading
if (!props.mailboxId || !props.msg) return;
let apiCall = isSeenFc(props.msg?.flags) ? API.removeFlag : API.addFlag;
apiCall({
mailboxId: props.mailboxId,
messageId: props.msg?.id,
flag: flag,
})
.then((res) => {
if (isSeenFc(props.msg?.flags)) {
store.commit("removeFlag", { roomId: props.roomId, messageId: props.msg?.id, flag: flag });
} else {
store.commit("addFlag", { roomId: props.roomId, messageId: props.msg?.id, flag: flag });
}
})
.catch((err) => {
console.log(err);
});
};
</script> </script>
<template> <template>
<div> <div>
<div>mark as not read</div> <div class="button" @click="setFlag('\\Seen')">
{{ isSeenFc(props.msg?.flags) ? "Mark as not read" : "Mark as read" }}
</div>
<div>flag favorite</div> <div>flag favorite</div>
<div>reply</div> <div>reply</div>
<div>delete from all</div> <div>delete from all</div>
<div>delete from remote</div> <div>delete from remote</div>
<div>transfer</div> <div>transfer</div>
<div>see source</div> <div>see source</div>
<div>{{ props.msg?.flags }}</div>
</div> </div>
</template> </template>
@ -25,4 +51,16 @@ const props = defineProps({
div { div {
text-align: center; text-align: center;
} }
.button {
border: solid 1px;
border-radius: 6px;
display: initial;
padding: 1px 5px;
cursor: pointer;
}
.button:hover {
background-color: var(--selected);
}
</style> </style>

View File

@ -16,4 +16,10 @@ export default {
getMembers(roomId: number) { getMembers(roomId: number) {
return API().get(`/mail/${roomId}/members`); return API().get(`/mail/${roomId}/members`);
}, },
addFlag(data: { mailboxId: number; messageId: number; flag: string }) {
return API().post(`/mail/addFlag`, data);
},
removeFlag(data: { mailboxId: number; messageId: number; flag: string }) {
return API().post(`/mail/removeFlag`, data);
},
}; };

View File

@ -14,7 +14,7 @@ export interface Message {
subject: string; subject: string;
content: string; content: string;
date: string; date: string;
flags: string; flags: string[];
} }
export enum LoadingState { export enum LoadingState {
@ -23,6 +23,7 @@ export enum LoadingState {
loaded = 2, loaded = 2,
} }
// todo store messages outside of the room
export interface Room { export interface Room {
id: number; id: number;
roomName: string; roomName: string;

View File

@ -46,6 +46,8 @@ export interface State {
activeRoom: number; activeRoom: number;
} }
const roomOnId = (state: State, roomId: number) => state.rooms.find((room: Room) => room.id == roomId);
// // define injection key todo // // define injection key todo
// export const key: InjectionKey<Store<State>> = Symbol() // export const key: InjectionKey<Store<State>> = Symbol()
@ -94,20 +96,34 @@ const store = createStore<State>({
}, },
addMessages(state, payload) { addMessages(state, payload) {
// todo add if not exist // todo add if not exist
const room = state.rooms.find((room) => room.id == payload.roomId); const room = roomOnId(state, payload.roomId);
if (!room) return; if (!room) return;
payload.messages.forEach((message: Message) => { payload.messages.forEach((message: any) => {
message.flags = message.flags?.split(",") ?? [];
room.messages.push(message); room.messages.push(message);
}); });
}, },
addAddress(state, payload) { addAddress(state, payload) {
// todo add if not exist // todo add if not exist
const room = state.rooms.find((room) => room.id == payload.roomId); const room = roomOnId(state, payload.roomId);
if (!room) return; if (!room) return;
payload.addresses.forEach((address: Address) => { payload.addresses.forEach((address: Address) => {
room.members.push(address); room.members.push(address);
}); });
}, },
addFlag(state, payload) {
// todo if seen notif
const msg = roomOnId(state, payload.roomId)?.messages.find((msg) => msg.id == payload.messageId);
if (msg) {
msg.flags.push(payload.flag);
}
},
removeFlag(state, payload) {
const msg = roomOnId(state, payload.roomId)?.messages.find((msg) => msg.id == payload.messageId);
if (msg) {
msg.flags?.splice(msg.flags?.indexOf(payload.flag), 1);
}
},
}, },
getters: { getters: {
rooms: (state) => (): Room[] => { rooms: (state) => (): Room[] => {
@ -119,20 +135,19 @@ const store = createStore<State>({
room: room:
(state) => (state) =>
(roomId: number): Room | undefined => { (roomId: number): Room | undefined => {
const room = state.rooms.find((room) => room.id == roomId); return roomOnId(state, roomId);
return room;
}, },
address: address:
(state) => (state) =>
(roomId: number, addressId: number): Address | undefined => { (roomId: number, addressId: number): Address | undefined => {
const room = state.rooms.find((room) => room.id == roomId); const room = roomOnId(state, roomId);
const address = room?.members.find((address) => address.id == addressId); const address = room?.members.find((address) => address.id == addressId);
return address; return address;
}, },
messages: messages:
(state) => (state) =>
(roomId: number): Message[] => { (roomId: number): Message[] => {
const room = state.rooms.find((room) => room.id == roomId); const room = roomOnId(state, roomId);
if (!room) return []; if (!room) return [];
if (room.messageLoading === LoadingState.notLoaded) { if (room.messageLoading === LoadingState.notLoaded) {
store.dispatch("fetchMessages", { roomId: room.id, room: room }); store.dispatch("fetchMessages", { roomId: room.id, room: room });

View File

@ -0,0 +1,3 @@
export function isSeenFc(flags: string[] | undefined): boolean {
return flags?.includes("\\Seen") ?? false;
}

View File

@ -47,6 +47,8 @@ function openMessageView(id) {
:key="index" :key="index"
:msg="message" :msg="message"
:members="room?.members" :members="room?.members"
:mailboxId="room.mailboxId"
:roomId="room.id"
@open-message-view="(id) => openMessageView(id)" @open-message-view="(id) => openMessageView(id)"
/> />
</div> </div>