replace rest api with rpc system
This commit is contained in:
parent
8ba70833f3
commit
bca4280afb
26 changed files with 636 additions and 595 deletions
|
@ -1,7 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { overlayStore, OverlayType } from "../../stores";
|
import { overlayStore, OverlayType } from "../../stores";
|
||||||
import request from "../../request";
|
import { methods, remoteCall } from "../../request";
|
||||||
import { apiRoute } from "../../storage";
|
|
||||||
import { maybeModalScale } from "../../animations";
|
import { maybeModalScale } from "../../animations";
|
||||||
|
|
||||||
let username = "";
|
let username = "";
|
||||||
|
@ -12,10 +11,7 @@
|
||||||
|
|
||||||
const create = async () => {
|
const create = async () => {
|
||||||
buttonsEnabled = false;
|
buttonsEnabled = false;
|
||||||
const { ok } = await request("POST", apiRoute("users/register"), false, {
|
const { ok } = await remoteCall(methods.createUser, username, password);
|
||||||
username,
|
|
||||||
password
|
|
||||||
});
|
|
||||||
if (ok) {
|
if (ok) {
|
||||||
overlayStore.toast("Account created");
|
overlayStore.toast("Account created");
|
||||||
loginInstead();
|
loginInstead();
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { overlayStore } from "../../stores";
|
import { overlayStore } from "../../stores";
|
||||||
import request from "../../request";
|
import { methods, remoteCall } from "../../request";
|
||||||
import { apiRoute } from "../../storage";
|
|
||||||
import { maybeModalFade, maybeModalScale } from "../../animations";
|
import { maybeModalFade, maybeModalScale } from "../../animations";
|
||||||
|
|
||||||
let channelName = "";
|
let channelName = "";
|
||||||
|
@ -10,9 +9,7 @@
|
||||||
|
|
||||||
const create = async () => {
|
const create = async () => {
|
||||||
createButtonEnabled = false;
|
createButtonEnabled = false;
|
||||||
const { ok } = await request("POST", apiRoute("channels"), true, {
|
const { ok } = await remoteCall(methods.createChannel, channelName);
|
||||||
name: channelName
|
|
||||||
});
|
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
overlayStore.toast("Couldn't create channel");
|
overlayStore.toast("Couldn't create channel");
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { maybeModalFade, maybeModalScale } from "../../animations";
|
import { maybeModalFade, maybeModalScale } from "../../animations";
|
||||||
import { overlayStore } from "../../stores";
|
import { overlayStore } from "../../stores";
|
||||||
import request from "../../request";
|
import { methods, remoteCall } from "../../request";
|
||||||
import { apiRoute } from "../../storage";
|
|
||||||
|
|
||||||
export let channel;
|
export let channel;
|
||||||
|
|
||||||
|
@ -12,9 +11,7 @@
|
||||||
|
|
||||||
const save = async () => {
|
const save = async () => {
|
||||||
buttonsEnabled = false;
|
buttonsEnabled = false;
|
||||||
const { ok } = await request("PUT", apiRoute(`channels/${channel.id}`), true, {
|
const { ok } = await remoteCall(methods.updateChannelName, channel.id, channelName);
|
||||||
name: channelName
|
|
||||||
});
|
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
overlayStore.toast("Couldn't edit channel");
|
overlayStore.toast("Couldn't edit channel");
|
||||||
}
|
}
|
||||||
|
@ -22,7 +19,7 @@
|
||||||
};
|
};
|
||||||
const deleteChannel = async () => {
|
const deleteChannel = async () => {
|
||||||
buttonsEnabled = false;
|
buttonsEnabled = false;
|
||||||
const { ok } = await request("DELETE", apiRoute(`channels/${channel.id}`), true);
|
const { ok } = await remoteCall(methods.deleteChannel, channel.id);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
overlayStore.toast("Couldn't delete channel");
|
overlayStore.toast("Couldn't delete channel");
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { overlayStore } from "../../stores";
|
import { overlayStore } from "../../stores";
|
||||||
import request from "../../request";
|
import { methods, remoteCall } from "../../request";
|
||||||
import { apiRoute } from "../../storage";
|
|
||||||
import { maybeModalFade, maybeModalScale } from "../../animations";
|
import { maybeModalFade, maybeModalScale } from "../../animations";
|
||||||
|
|
||||||
export let message;
|
export let message;
|
||||||
|
@ -12,9 +11,7 @@
|
||||||
|
|
||||||
const save = async () => {
|
const save = async () => {
|
||||||
buttonsEnabled = false;
|
buttonsEnabled = false;
|
||||||
const { ok } = await request("PUT", apiRoute(`messages/${message.id}`), true, {
|
const { ok } = await remoteCall(methods.updateMessageContent, message.id, messageContent);
|
||||||
content: messageContent
|
|
||||||
});
|
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
overlayStore.toast("Couldn't edit message");
|
overlayStore.toast("Couldn't edit message");
|
||||||
}
|
}
|
||||||
|
@ -22,7 +19,7 @@
|
||||||
};
|
};
|
||||||
const deleteMessage = async () => {
|
const deleteMessage = async () => {
|
||||||
buttonsEnabled = false;
|
buttonsEnabled = false;
|
||||||
const { ok } = await request("DELETE", apiRoute(`messages/${message.id}`), true);
|
const { ok } = await remoteCall(methods.deleteMessage, message.id);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
overlayStore.toast("Couldn't delete message");
|
overlayStore.toast("Couldn't delete message");
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
<script>
|
<script>
|
||||||
import { overlayStore, OverlayType } from "../../stores";
|
import { overlayStore, OverlayType } from "../../stores";
|
||||||
import request from "../../request";
|
import { remoteCall } from "../../request";
|
||||||
import { apiRoute } from "../../storage";
|
|
||||||
import { authWithToken } from "../../auth";
|
import { authWithToken } from "../../auth";
|
||||||
import { maybeModalScale } from "../../animations";
|
import { maybeModalScale } from "../../animations";
|
||||||
|
import { methods } from "../../request";
|
||||||
|
|
||||||
let username = "";
|
let username = "";
|
||||||
let password = "";
|
let password = "";
|
||||||
|
@ -13,10 +13,7 @@
|
||||||
|
|
||||||
const login = async () => {
|
const login = async () => {
|
||||||
buttonsEnabled = false;
|
buttonsEnabled = false;
|
||||||
const { ok, json } = await request("POST", apiRoute("users/login"), false, {
|
const { ok, json } = await remoteCall(methods.loginUser, username, password);
|
||||||
username,
|
|
||||||
password
|
|
||||||
});
|
|
||||||
if (ok && json && json.token) {
|
if (ok && json && json.token) {
|
||||||
authWithToken(json.token, true);
|
authWithToken(json.token, true);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -18,6 +18,8 @@ export const GatewayPayloadType = {
|
||||||
Authenticate: 1,
|
Authenticate: 1,
|
||||||
Ready: 2,
|
Ready: 2,
|
||||||
Ping: 3,
|
Ping: 3,
|
||||||
|
RPCRequest: 4, // client
|
||||||
|
RPCResponse: 5,
|
||||||
|
|
||||||
ChannelCreate: 110,
|
ChannelCreate: 110,
|
||||||
ChannelUpdate: 111,
|
ChannelUpdate: 111,
|
||||||
|
@ -58,6 +60,8 @@ export default {
|
||||||
reconnectTimeout: null,
|
reconnectTimeout: null,
|
||||||
handlers: new Map(),
|
handlers: new Map(),
|
||||||
disableReconnect: false,
|
disableReconnect: false,
|
||||||
|
serial: 0,
|
||||||
|
waitingSerials: new Map(),
|
||||||
init(token) {
|
init(token) {
|
||||||
timeline.addCheckpoint("Gateway connection start");
|
timeline.addCheckpoint("Gateway connection start");
|
||||||
if (!token) {
|
if (!token) {
|
||||||
|
@ -106,12 +110,19 @@ export default {
|
||||||
|
|
||||||
this.user = payload.d.user;
|
this.user = payload.d.user;
|
||||||
this.channels = payload.d.channels;
|
this.channels = payload.d.channels;
|
||||||
|
this.authenticated = true;
|
||||||
this.reconnectDelay = 400;
|
this.reconnectDelay = 400;
|
||||||
|
|
||||||
log("ready");
|
log("ready");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case GatewayPayloadType.RPCResponse: {
|
||||||
|
if (this.waitingSerials.get(payload.s)) {
|
||||||
|
(this.waitingSerials.get(payload.s))(payload.d);
|
||||||
|
this.waitingSerials.delete(payload.s);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dispatch(payload.t, payload.d);
|
this.dispatch(payload.t, payload.d);
|
||||||
|
@ -183,6 +194,17 @@ export default {
|
||||||
this.handlers.delete(event);
|
this.handlers.delete(event);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
sendRPCRequest(calls) {
|
||||||
|
return new Promise((resolve, _reject) => {
|
||||||
|
this.waitingSerials.set(this.serial, resolve);
|
||||||
|
this.send({
|
||||||
|
t: GatewayPayloadType.RPCRequest,
|
||||||
|
d: calls,
|
||||||
|
s: this.serial
|
||||||
|
});
|
||||||
|
this.serial++;
|
||||||
|
});
|
||||||
|
},
|
||||||
close() {
|
close() {
|
||||||
this.disableReconnect = true;
|
this.disableReconnect = true;
|
||||||
if (this.ws)
|
if (this.ws)
|
||||||
|
|
|
@ -1,7 +1,27 @@
|
||||||
import { getItem } from "./storage";
|
import gateway from "./gateway";
|
||||||
|
import { apiRoute, getItem } from "./storage";
|
||||||
// TODO: circular dependency
|
// TODO: circular dependency
|
||||||
import { overlayStore, OverlayType } from "./stores";
|
import { overlayStore, OverlayType } from "./stores";
|
||||||
|
|
||||||
|
export const methods = {
|
||||||
|
// methodName: [ methodId, requiresAuthentication ]
|
||||||
|
createUser: [ 0, false ],
|
||||||
|
loginUser: [ 1, false ],
|
||||||
|
getUserSelf: [ 2, true ],
|
||||||
|
promoteUserSelf: [ 3, true ],
|
||||||
|
createChannel: [ 4, true ],
|
||||||
|
updateChannelName: [ 5, true ],
|
||||||
|
deleteChannel: [ 6, true ],
|
||||||
|
getChannel: [ 7, true ],
|
||||||
|
getChannels: [ 8, true ],
|
||||||
|
createChannelMessage: [ 9, true ],
|
||||||
|
getChannelMessages: [ 10, true ],
|
||||||
|
putChannelTyping: [ 11, true ],
|
||||||
|
deleteMessage: [ 12, true ],
|
||||||
|
updateMessageContent: [ 13, true ],
|
||||||
|
getMessage: [ 14, true ]
|
||||||
|
};
|
||||||
|
|
||||||
export function compatibleFetch(endpoint, options) {
|
export function compatibleFetch(endpoint, options) {
|
||||||
if (window.fetch && typeof window.fetch === "function") {
|
if (window.fetch && typeof window.fetch === "function") {
|
||||||
return fetch(endpoint, options);
|
return fetch(endpoint, options);
|
||||||
|
@ -32,8 +52,8 @@ export function compatibleFetch(endpoint, options) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function doRequest(method, endpoint, auth=true, body=null, _keyEntryDepth=false) {
|
export default function doRequest(method, endpoint, auth=true, body=null) {
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise(async (resolve, _reject) => {
|
||||||
const options = {
|
const options = {
|
||||||
method,
|
method,
|
||||||
};
|
};
|
||||||
|
@ -60,43 +80,32 @@ export default function doRequest(method, endpoint, auth=true, body=null, _keyEn
|
||||||
const res = await compatibleFetch(endpoint, options);
|
const res = await compatibleFetch(endpoint, options);
|
||||||
const json = res.status === 204 ? {} : await res.json();
|
const json = res.status === 204 ? {} : await res.json();
|
||||||
|
|
||||||
if (res.status === 403 && json.code && json.code === 6006 && !_keyEntryDepth) {
|
|
||||||
// This endpoint is password-protected
|
|
||||||
overlayStore.push(OverlayType.Prompt, {
|
|
||||||
heading: "Enter Key For Resource",
|
|
||||||
valueName: "Key",
|
|
||||||
async onSubmit(value) {
|
|
||||||
const response = await doRequest(method, endpoint, auth, {
|
|
||||||
...(body || {}),
|
|
||||||
requestKey: value
|
|
||||||
}, true);
|
|
||||||
resolve(response);
|
|
||||||
},
|
|
||||||
onClose() {
|
|
||||||
resolve({
|
|
||||||
success: true,
|
|
||||||
json,
|
|
||||||
ok: res.ok,
|
|
||||||
status: res.status
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return resolve({
|
return resolve({
|
||||||
success: true,
|
|
||||||
json,
|
json,
|
||||||
ok: res.ok,
|
ok: res.ok,
|
||||||
status: res.status
|
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return resolve({
|
return resolve({
|
||||||
success: false,
|
|
||||||
json: null,
|
json: null,
|
||||||
ok: false,
|
ok: false,
|
||||||
status: null
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function remoteCall([methodId, requiresAuthentication], ...args) {
|
||||||
|
const calls = [[methodId, ...args]];
|
||||||
|
if (requiresAuthentication && gateway.authenticated) {
|
||||||
|
const replies = await gateway.sendRPCRequest(calls);
|
||||||
|
const ok = Array.isArray(replies) && replies[0] && !replies[0].code;
|
||||||
|
return {
|
||||||
|
json: ok ? replies[0] : null,
|
||||||
|
ok
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await doRequest("POST", apiRoute("rpc"), requiresAuthentication, calls);
|
||||||
|
response.ok = response.ok && Array.isArray(response.json) && response.json[0] && !response.json[0].code;
|
||||||
|
response.json = response.ok ? response.json[0] : null;
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import gateway, { GatewayEventType, GatewayPayloadType, GatewayPresenceStatus } from "./gateway";
|
import gateway, { GatewayEventType, GatewayPayloadType, GatewayPresenceStatus } from "./gateway";
|
||||||
import logger from "./logging";
|
import logger from "./logging";
|
||||||
import request from "./request";
|
import { methods, remoteCall } from "./request";
|
||||||
import { apiRoute, getItem, setItem } from "./storage";
|
import { getItem, setItem } from "./storage";
|
||||||
|
|
||||||
const storeLog = logger("Store");
|
const storeLog = logger("Store");
|
||||||
|
|
||||||
|
@ -339,9 +339,8 @@ class MessageStore extends Store {
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const oldestMessage = this.value[0];
|
const oldestMessage = this.value[0];
|
||||||
const endpoint = oldestMessage ? `channels/${this.channelId}/messages/?before=${oldestMessage.id}` : `channels/${this.channelId}/messages`;
|
const res = await remoteCall(methods.getChannelMessages, this.channelId, null, oldestMessage ? oldestMessage.id : null);
|
||||||
const res = await request("GET", apiRoute(endpoint), true, null);
|
if (res.ok) {
|
||||||
if (res.success && res.ok && res.json) {
|
|
||||||
if (res.json.length < 1)
|
if (res.json.length < 1)
|
||||||
return;
|
return;
|
||||||
if (beforeCommitToStore)
|
if (beforeCommitToStore)
|
||||||
|
@ -540,7 +539,7 @@ class TypingStore extends Store {
|
||||||
this.startedTyping(userInfoStore.value, selectedChannel.value.id, 6500);
|
this.startedTyping(userInfoStore.value, selectedChannel.value.id, 6500);
|
||||||
if (this.ownNeedsUpdate) {
|
if (this.ownNeedsUpdate) {
|
||||||
this.ownNeedsUpdate = false;
|
this.ownNeedsUpdate = false;
|
||||||
await request("POST", apiRoute(`channels/${selectedChannel.value.id}/typing`), true, {});
|
await remoteCall(methods.putChannelTyping, selectedChannel.value.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -696,12 +695,9 @@ export const sendMessageAction = createAction("sendMessageAction", async ({chann
|
||||||
const messagesStoreForChannel = messagesStoreProvider.getStore(channelId);
|
const messagesStoreForChannel = messagesStoreProvider.getStore(channelId);
|
||||||
messagesStoreForChannel.addMessage(optimisticMessage);
|
messagesStoreForChannel.addMessage(optimisticMessage);
|
||||||
|
|
||||||
const res = await request("POST", apiRoute(`channels/${channelId}/messages`), true, {
|
const res = await remoteCall(methods.createChannelMessage, channelId, content, optimisticMessageId, null);
|
||||||
content: optimisticMessage.content,
|
|
||||||
optimistic_id: optimisticMessageId
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.success && res.ok) {
|
if (res.ok) {
|
||||||
messagesStoreForChannel.setMessage(optimisticMessageId, res.json);
|
messagesStoreForChannel.setMessage(optimisticMessageId, res.json);
|
||||||
} else {
|
} else {
|
||||||
messagesStoreForChannel.deleteMessage({
|
messagesStoreForChannel.deleteMessage({
|
||||||
|
|
17
src/auth.ts
17
src/auth.ts
|
@ -1,4 +1,4 @@
|
||||||
import { NextFunction, Request, Response } from "express";
|
import e, { NextFunction, Request, Response } from "express";
|
||||||
import { sign, verify } from "jsonwebtoken";
|
import { sign, verify } from "jsonwebtoken";
|
||||||
import { query } from "./database";
|
import { query } from "./database";
|
||||||
import { errors } from "./errors";
|
import { errors } from "./errors";
|
||||||
|
@ -116,18 +116,23 @@ export async function loginAttempt(username: string, password: string): Promise<
|
||||||
return await signToken(existingUser.rows[0].id);
|
return await signToken(existingUser.rows[0].id);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function authenticateRoute() {
|
export function authenticateRoute(errorOnBadAuth = true) {
|
||||||
return async (req: Request, res: Response, next: NextFunction) => {
|
return async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const pass = (user: User | null = null) => {
|
const pass = (user: User | null = null) => {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
res.status(403).send({
|
if (errorOnBadAuth) {
|
||||||
...errors.BAD_AUTH
|
res.status(403).send(errors.BAD_AUTH);
|
||||||
});
|
return;
|
||||||
return;
|
} else {
|
||||||
|
req.authenticated = false;
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
req.user = user;
|
req.user = user;
|
||||||
req.publicUser = getPublicUserObject(user);
|
req.publicUser = getPublicUserObject(user);
|
||||||
|
req.authenticated = true;
|
||||||
next();
|
next();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
export const errors = {
|
export const errors = {
|
||||||
|
INVALID_RPC_CALL: { code: 6000, message: "Invalid RPC call. Please see 'detail' property." },
|
||||||
INVALID_DATA: { code: 6001, message: "Invalid data" },
|
INVALID_DATA: { code: 6001, message: "Invalid data" },
|
||||||
BAD_LOGIN_CREDENTIALS: { code: 6002, message: "Bad login credentials provided" },
|
BAD_LOGIN_CREDENTIALS: { code: 6002, message: "Bad login credentials provided" },
|
||||||
BAD_AUTH: { code: 6003, message: "Bad authentication" },
|
BAD_AUTH: { code: 6003, message: "Bad authentication" },
|
||||||
|
@ -7,7 +8,7 @@ export const errors = {
|
||||||
BAD_REQUEST_KEY: { code: 6006, message: "Bad request key" },
|
BAD_REQUEST_KEY: { code: 6006, message: "Bad request key" },
|
||||||
GOT_NO_DATABASE_DATA: { code: 7001, message: "Unexpectedly got no data from database" },
|
GOT_NO_DATABASE_DATA: { code: 7001, message: "Unexpectedly got no data from database" },
|
||||||
FEATURE_DISABLED: { code: 7002, message: "This feature is disabled" },
|
FEATURE_DISABLED: { code: 7002, message: "This feature is disabled" },
|
||||||
INTERNAL_ERROR: { code: 7003, message: "Internal server error" }
|
INTERNAL_ERROR: { code: 7003, message: "Internal server error" },
|
||||||
};
|
};
|
||||||
|
|
||||||
export const gatewayErrors = {
|
export const gatewayErrors = {
|
||||||
|
@ -21,4 +22,5 @@ export const gatewayErrors = {
|
||||||
TOO_MANY_SESSIONS: { code: 4008, message: "Too many sessions" },
|
TOO_MANY_SESSIONS: { code: 4008, message: "Too many sessions" },
|
||||||
NOT_AUTHENTICATED: { code: 4009, message: "Not authenticated" },
|
NOT_AUTHENTICATED: { code: 4009, message: "Not authenticated" },
|
||||||
GOT_NO_DATABASE_DATA: { code: 4010, message: "Unexpectedly got no data from database" },
|
GOT_NO_DATABASE_DATA: { code: 4010, message: "Unexpectedly got no data from database" },
|
||||||
|
INTERNAL_ERROR: { code: 4011, message: "Internal server error" },
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,6 +3,8 @@ export enum GatewayPayloadType {
|
||||||
Authenticate, // client
|
Authenticate, // client
|
||||||
Ready,
|
Ready,
|
||||||
Ping, // client
|
Ping, // client
|
||||||
|
RPCRequest, // client
|
||||||
|
RPCResponse,
|
||||||
|
|
||||||
ChannelCreate = 110,
|
ChannelCreate = 110,
|
||||||
ChannelUpdate,
|
ChannelUpdate,
|
||||||
|
|
|
@ -6,11 +6,12 @@ import { query } from "../database";
|
||||||
import { gatewayErrors } from "../errors";
|
import { gatewayErrors } from "../errors";
|
||||||
import { GatewayPayload } from "../types/gatewaypayload";
|
import { GatewayPayload } from "../types/gatewaypayload";
|
||||||
import { GatewayPayloadType, GatewayPresenceStatus } from "./gatewaypayloadtype";
|
import { GatewayPayloadType, GatewayPresenceStatus } from "./gatewaypayloadtype";
|
||||||
import { GatewayPresenceEntry } from "./gatewaypresence";
|
import { GatewayPresenceEntry } from "../types/gatewaypresence";
|
||||||
|
import { processMethodBatch } from "../rpc/rpc";
|
||||||
|
|
||||||
const GATEWAY_BATCH_INTERVAL = 50000;
|
const GATEWAY_BATCH_INTERVAL = 50000;
|
||||||
const GATEWAY_PING_INTERVAL = 40000;
|
const GATEWAY_PING_INTERVAL = 40000;
|
||||||
const MAX_CLIENT_MESSAGES_PER_BATCH = 6; // TODO: how well does this work for weak connections?
|
const MAX_CLIENT_MESSAGES_PER_BATCH = 30; // TODO: how well does this work for weak connections?
|
||||||
const MAX_GATEWAY_SESSIONS_PER_USER = 5;
|
const MAX_GATEWAY_SESSIONS_PER_USER = 5;
|
||||||
|
|
||||||
// mapping between a dispatch id and a websocket client
|
// mapping between a dispatch id and a websocket client
|
||||||
|
@ -167,6 +168,9 @@ function ensureFormattedGatewayPayload(payload: any): GatewayPayload | null {
|
||||||
foundT = true;
|
foundT = true;
|
||||||
} else if (k === "d") {
|
} else if (k === "d") {
|
||||||
foundD = true;
|
foundD = true;
|
||||||
|
} else if (k === "s" && typeof v === "number") {
|
||||||
|
// found serial
|
||||||
|
continue;
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -303,7 +307,7 @@ export default function(server: Server) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const stringData = rawData.toString();
|
const stringData = rawData.toString();
|
||||||
if (stringData.length > 2048) {
|
if (stringData.length > 4500) {
|
||||||
return closeWithError(ws, gatewayErrors.PAYLOAD_TOO_LARGE);
|
return closeWithError(ws, gatewayErrors.PAYLOAD_TOO_LARGE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -405,6 +409,23 @@ export default function(server: Server) {
|
||||||
ws.state.alive = true;
|
ws.state.alive = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case GatewayPayloadType.RPCRequest: {
|
||||||
|
if (!ws.state.ready || !ws.state.user) {
|
||||||
|
return closeWithError(ws, gatewayErrors.NOT_AUTHENTICATED);
|
||||||
|
}
|
||||||
|
|
||||||
|
processMethodBatch(ws.state.user, payload.d).then((results) => {
|
||||||
|
sendPayload(ws, {
|
||||||
|
t: GatewayPayloadType.RPCResponse,
|
||||||
|
d: results,
|
||||||
|
s: payload.s
|
||||||
|
});
|
||||||
|
}).catch(e => {
|
||||||
|
console.error("gateway: unexpected error while handling RPCRequest", e);
|
||||||
|
return closeWithError(ws, gatewayErrors.INTERNAL_ERROR);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
return closeWithBadPayload(ws, "t: unknown type");
|
return closeWithBadPayload(ws, "t: unknown type");
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,253 +0,0 @@
|
||||||
import express from "express";
|
|
||||||
import { body, param, validationResult } from "express-validator";
|
|
||||||
import { authenticateRoute } from "../../../auth";
|
|
||||||
import { query } from "../../../database";
|
|
||||||
import { getMessageById, getMessagesByChannelFirstPage, getMessagesByChannelPage } from "../../../database/templates";
|
|
||||||
import { errors } from "../../../errors";
|
|
||||||
import { dispatch, dispatchChannelSubscribe } from "../../../gateway";
|
|
||||||
import { GatewayPayloadType } from "../../../gateway/gatewaypayloadtype";
|
|
||||||
import sendMessage from "../../../impl";
|
|
||||||
import serverConfig from "../../../serverconfig";
|
|
||||||
|
|
||||||
const router = express.Router();
|
|
||||||
|
|
||||||
router.post(
|
|
||||||
"/",
|
|
||||||
authenticateRoute(),
|
|
||||||
body("name").isLength({ min: 1, max: 32 }).isAlphanumeric("en-US", { ignore: " _-" }),
|
|
||||||
async (req, res) => {
|
|
||||||
const validationErrors = validationResult(req);
|
|
||||||
if (!validationErrors.isEmpty()) {
|
|
||||||
return res.status(400).json({ ...errors.INVALID_DATA, errors: validationErrors.array() });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (serverConfig.superuserRequirement.createChannel && !req.user.is_superuser) {
|
|
||||||
return res.status(403).json({ ...errors.FORBIDDEN_DUE_TO_MISSING_PERMISSIONS });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { name } = req.body;
|
|
||||||
const result = await query("INSERT INTO channels(name, owner_id) VALUES ($1, $2) RETURNING id, name, owner_id", [name, req.user.id]);
|
|
||||||
if (!result || result.rowCount < 1) {
|
|
||||||
return res.status(500).json({
|
|
||||||
...errors.GOT_NO_DATABASE_DATA
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch("*", {
|
|
||||||
t: GatewayPayloadType.ChannelCreate,
|
|
||||||
d: result.rows[0]
|
|
||||||
});
|
|
||||||
|
|
||||||
// When a new channel is created, we will currently subscribe every client
|
|
||||||
// on the gateway (this will be changed when the concept of "communities" is added)
|
|
||||||
dispatchChannelSubscribe("*", `channel:${result.rows[0].id}`);
|
|
||||||
|
|
||||||
res.status(201).send(result.rows[0]);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
router.put(
|
|
||||||
"/:id",
|
|
||||||
authenticateRoute(),
|
|
||||||
body("name").isLength({ min: 1, max: 32 }).isAlphanumeric("en-US", { ignore: " _-" }),
|
|
||||||
param("id").isNumeric(),
|
|
||||||
async (req, res) => {
|
|
||||||
const validationErrors = validationResult(req);
|
|
||||||
if (!validationErrors.isEmpty()) {
|
|
||||||
return res.status(400).json({ ...errors.INVALID_DATA, errors: validationErrors.array() });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { name } = req.body;
|
|
||||||
const id = parseInt(req.params.id); // TODO: ??
|
|
||||||
|
|
||||||
const permissionCheckResult = await query("SELECT owner_id FROM channels WHERE id = $1", [id]);
|
|
||||||
if (!permissionCheckResult || permissionCheckResult.rowCount < 1) {
|
|
||||||
return res.status(404).json({
|
|
||||||
...errors.NOT_FOUND
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (permissionCheckResult.rows[0].owner_id !== req.user.id && !req.user.is_superuser) {
|
|
||||||
return res.status(403).json({
|
|
||||||
...errors.FORBIDDEN_DUE_TO_MISSING_PERMISSIONS
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await query("UPDATE channels SET name = $1 WHERE id = $2", [name, id]);
|
|
||||||
if (!result || result.rowCount < 1) {
|
|
||||||
return res.status(500).json({
|
|
||||||
...errors.GOT_NO_DATABASE_DATA
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatePayload = {
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
owner_id: permissionCheckResult.rows[0].owner_id
|
|
||||||
};
|
|
||||||
|
|
||||||
dispatch(`channel:${id}`, {
|
|
||||||
t: GatewayPayloadType.ChannelUpdate,
|
|
||||||
d: updatePayload
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send(updatePayload);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
router.delete(
|
|
||||||
"/:id",
|
|
||||||
authenticateRoute(),
|
|
||||||
param("id").isNumeric(),
|
|
||||||
async (req, res) => {
|
|
||||||
const validationErrors = validationResult(req);
|
|
||||||
if (!validationErrors.isEmpty()) {
|
|
||||||
return res.status(400).json({ ...errors.INVALID_DATA, errors: validationErrors.array() });
|
|
||||||
}
|
|
||||||
|
|
||||||
const id = parseInt(req.params.id); // TODO: ??
|
|
||||||
|
|
||||||
const permissionCheckResult = await query("SELECT owner_id FROM channels WHERE id = $1", [id]);
|
|
||||||
if (!permissionCheckResult || permissionCheckResult.rowCount < 1) {
|
|
||||||
return res.status(404).json({
|
|
||||||
...errors.NOT_FOUND
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (permissionCheckResult.rows[0].owner_id !== req.user.id && !req.user.is_superuser) {
|
|
||||||
return res.status(403).json({
|
|
||||||
...errors.FORBIDDEN_DUE_TO_MISSING_PERMISSIONS
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await query("DELETE FROM channels WHERE id = $1", [id]);
|
|
||||||
if (!result || result.rowCount < 1) {
|
|
||||||
return res.status(500).json({
|
|
||||||
...errors.GOT_NO_DATABASE_DATA
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(`channel:${id}`, {
|
|
||||||
t: GatewayPayloadType.ChannelDelete,
|
|
||||||
d: {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(204).send("");
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
router.get(
|
|
||||||
"/:id",
|
|
||||||
authenticateRoute(),
|
|
||||||
param("id").isNumeric(),
|
|
||||||
async (req, res) => {
|
|
||||||
const validationErrors = validationResult(req);
|
|
||||||
if (!validationErrors.isEmpty()) {
|
|
||||||
return res.status(400).json({ ...errors.INVALID_DATA, errors: validationErrors.array() });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { id } = req.params;
|
|
||||||
const result = await query("SELECT id, name, owner_id FROM channels WHERE id = $1", [id]);
|
|
||||||
if (!result || result.rowCount < 1) {
|
|
||||||
return res.status(404).json({
|
|
||||||
...errors.NOT_FOUND
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send(result.rows[0]);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
router.get(
|
|
||||||
"/",
|
|
||||||
authenticateRoute(),
|
|
||||||
async (req, res) => {
|
|
||||||
const result = await query("SELECT id, name, owner_id FROM channels");
|
|
||||||
|
|
||||||
return res.status(200).send(result ? result.rows : []);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
router.post(
|
|
||||||
"/:id/messages",
|
|
||||||
authenticateRoute(),
|
|
||||||
param("id").isNumeric(),
|
|
||||||
body("content").isLength({ min: 1, max: 4000 }),
|
|
||||||
body("optimistic_id").optional().isNumeric(),
|
|
||||||
body("nick_username").optional().isString().isLength({ min: 1, max: 64 }),
|
|
||||||
async (req, res) => {
|
|
||||||
const validationErrors = validationResult(req);
|
|
||||||
if (!validationErrors.isEmpty()) {
|
|
||||||
return res.status(400).json({ ...errors.INVALID_DATA, errors: validationErrors.array() });
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(201).send(await sendMessage(req.user, parseInt(req.params.id), parseInt(req.body.optimistic_id), req.body.content, req.body.nick_username));
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
router.get(
|
|
||||||
"/:id/messages",
|
|
||||||
authenticateRoute(),
|
|
||||||
param("id").isNumeric(),
|
|
||||||
param("count").optional().isInt({ min: 10, max: 50 }),
|
|
||||||
async (req, res) => {
|
|
||||||
const validationErrors = validationResult(req);
|
|
||||||
if (!validationErrors.isEmpty()) {
|
|
||||||
return res.status(400).json({ ...errors.INVALID_DATA, errors: validationErrors.array() });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { before, count } = req.query;
|
|
||||||
|
|
||||||
let limit = typeof count === "string" ? parseInt(count || "25") : 25;
|
|
||||||
if (Number.isNaN(limit)) {
|
|
||||||
return res.status(400).json({ ...errors.INVALID_DATA });
|
|
||||||
}
|
|
||||||
|
|
||||||
const channelId = parseInt(req.params.id);
|
|
||||||
|
|
||||||
let finalRows = [];
|
|
||||||
|
|
||||||
if (before) {
|
|
||||||
const result = await query(getMessagesByChannelPage(limit), [before, channelId]);
|
|
||||||
finalRows = result ? result.rows : [];
|
|
||||||
} else {
|
|
||||||
const result = await query(getMessagesByChannelFirstPage(limit), [channelId]);
|
|
||||||
finalRows = result ? result.rows : [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send(finalRows);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
router.post(
|
|
||||||
"/:id/typing",
|
|
||||||
authenticateRoute(),
|
|
||||||
param("id").isNumeric(),
|
|
||||||
async (req, res) => {
|
|
||||||
const validationErrors = validationResult(req);
|
|
||||||
if (!validationErrors.isEmpty()) {
|
|
||||||
return res.status(400).json({ ...errors.INVALID_DATA, errors: validationErrors.array() });
|
|
||||||
}
|
|
||||||
|
|
||||||
const channelId = parseInt(req.params.id);
|
|
||||||
|
|
||||||
dispatch(`channel:${channelId}`, {
|
|
||||||
t: GatewayPayloadType.TypingStart,
|
|
||||||
d: {
|
|
||||||
user: {
|
|
||||||
id: req.publicUser.id,
|
|
||||||
username: req.publicUser.username
|
|
||||||
},
|
|
||||||
channel: {
|
|
||||||
id: channelId
|
|
||||||
},
|
|
||||||
time: 7500
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(201).send("");
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export default router;
|
|
|
@ -1,125 +0,0 @@
|
||||||
import express from "express";
|
|
||||||
import { body, param, validationResult } from "express-validator";
|
|
||||||
import { authenticateRoute } from "../../../auth";
|
|
||||||
import { query } from "../../../database";
|
|
||||||
import { getMessageById } from "../../../database/templates";
|
|
||||||
import { errors } from "../../../errors";
|
|
||||||
import { dispatch } from "../../../gateway";
|
|
||||||
import { GatewayPayloadType } from "../../../gateway/gatewaypayloadtype";
|
|
||||||
|
|
||||||
const router = express.Router();
|
|
||||||
|
|
||||||
router.delete(
|
|
||||||
"/:id",
|
|
||||||
authenticateRoute(),
|
|
||||||
param("id").isNumeric(),
|
|
||||||
async (req, res) => {
|
|
||||||
const validationErrors = validationResult(req);
|
|
||||||
if (!validationErrors.isEmpty()) {
|
|
||||||
return res.status(400).json({ ...errors.INVALID_DATA, errors: validationErrors.array() });
|
|
||||||
}
|
|
||||||
|
|
||||||
const id = parseInt(req.params.id); // TODO: ??
|
|
||||||
|
|
||||||
const permissionCheckResult = await query("SELECT author_id, channel_id FROM messages WHERE id = $1", [id]);
|
|
||||||
if (!permissionCheckResult || permissionCheckResult.rowCount < 1) {
|
|
||||||
return res.status(404).json({
|
|
||||||
...errors.NOT_FOUND
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (permissionCheckResult.rows[0].author_id !== req.user.id && !req.user.is_superuser) {
|
|
||||||
return res.status(403).json({
|
|
||||||
...errors.FORBIDDEN_DUE_TO_MISSING_PERMISSIONS
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await query("DELETE FROM messages WHERE id = $1", [id]);
|
|
||||||
if (!result || result.rowCount < 1) {
|
|
||||||
return res.status(500).json({
|
|
||||||
...errors.GOT_NO_DATABASE_DATA
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(`channel:${permissionCheckResult.rows[0].channel_id}`, {
|
|
||||||
t: GatewayPayloadType.MessageDelete,
|
|
||||||
d: {
|
|
||||||
id,
|
|
||||||
channel_id: permissionCheckResult.rows[0].channel_id
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(204).send("");
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
router.put(
|
|
||||||
"/:id",
|
|
||||||
authenticateRoute(),
|
|
||||||
body("content").isLength({ min: 1, max: 4000 }),
|
|
||||||
param("id").isNumeric(),
|
|
||||||
async (req, res) => {
|
|
||||||
const validationErrors = validationResult(req);
|
|
||||||
if (!validationErrors.isEmpty()) {
|
|
||||||
return res.status(400).json({ ...errors.INVALID_DATA, errors: validationErrors.array() });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { content } = req.body;
|
|
||||||
const id = parseInt(req.params.id); // TODO: ??
|
|
||||||
|
|
||||||
const permissionCheckResult = await query(getMessageById, [id]);
|
|
||||||
if (!permissionCheckResult || permissionCheckResult.rowCount < 1) {
|
|
||||||
return res.status(404).json({
|
|
||||||
...errors.NOT_FOUND
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (permissionCheckResult.rows[0].author_id !== req.user.id && !req.user.is_superuser) {
|
|
||||||
return res.status(403).json({
|
|
||||||
...errors.FORBIDDEN_DUE_TO_MISSING_PERMISSIONS
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await query("UPDATE messages SET content = $1 WHERE id = $2", [content, id]);
|
|
||||||
if (!result || result.rowCount < 1) {
|
|
||||||
return res.status(500).json({
|
|
||||||
...errors.GOT_NO_DATABASE_DATA
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const returnObject = {
|
|
||||||
...permissionCheckResult.rows[0],
|
|
||||||
content
|
|
||||||
};
|
|
||||||
|
|
||||||
dispatch(`channel:${permissionCheckResult.rows[0].channel_id}`, {
|
|
||||||
t: GatewayPayloadType.MessageUpdate,
|
|
||||||
d: returnObject
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send(returnObject);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
router.get(
|
|
||||||
"/:id",
|
|
||||||
authenticateRoute(),
|
|
||||||
param("id").isNumeric(),
|
|
||||||
async (req, res) => {
|
|
||||||
const validationErrors = validationResult(req);
|
|
||||||
if (!validationErrors.isEmpty()) {
|
|
||||||
return res.status(400).json({ ...errors.INVALID_DATA, errors: validationErrors.array() });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { id } = req.params;
|
|
||||||
const result = await query(getMessageById, [id]);
|
|
||||||
if (!result || result.rowCount < 1) {
|
|
||||||
return res.status(404).json({
|
|
||||||
...errors.NOT_FOUND
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send(result.rows[0]);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
export default router;
|
|
16
src/routes/api/v1/rpc.ts
Normal file
16
src/routes/api/v1/rpc.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { errors } from "../../../errors";
|
||||||
|
import express from "express";
|
||||||
|
import { authenticateRoute } from "../../../auth";
|
||||||
|
import { processMethodBatch } from "../../../rpc/rpc";
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
"/",
|
||||||
|
authenticateRoute(false),
|
||||||
|
async (req, res) => {
|
||||||
|
res.json(await processMethodBatch(req.authenticated ? req.user : null, req.body));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
|
@ -1,126 +0,0 @@
|
||||||
import { errors } from "../../../errors";
|
|
||||||
import { query } from "../../../database";
|
|
||||||
import express from "express";
|
|
||||||
import { body, validationResult } from "express-validator";
|
|
||||||
import { compare, hash, hashSync } from "bcrypt";
|
|
||||||
import { authenticateRoute, loginAttempt, signToken } from "../../../auth";
|
|
||||||
|
|
||||||
const router = express.Router();
|
|
||||||
|
|
||||||
const superuserKey = process.env.SUPERUSER_KEY ? hashSync(process.env.SUPERUSER_KEY, 10) : null;
|
|
||||||
const authRequestKey = process.env.AUTH_REQUEST_KEY ? hashSync(process.env.AUTH_REQUEST_KEY, 10) : null;
|
|
||||||
|
|
||||||
router.post(
|
|
||||||
"/register",
|
|
||||||
body("username").isLength({ min: 3, max: 32 }).isAlphanumeric("en-US", { ignore: " _-" }),
|
|
||||||
body("password").isLength({ min: 8, max: 1000 }),
|
|
||||||
async (req, res) => {
|
|
||||||
if (process.env.DISABLE_ACCOUNT_CREATION === "true") {
|
|
||||||
return res.status(403).json({ ...errors.FEATURE_DISABLED });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authRequestKey) {
|
|
||||||
if (!req.body.requestKey) {
|
|
||||||
return res.status(403).json({ ...errors.BAD_REQUEST_KEY });
|
|
||||||
}
|
|
||||||
const result = await compare(req.body.requestKey, authRequestKey);
|
|
||||||
if (!result) {
|
|
||||||
return res.status(403).json({ ...errors.BAD_REQUEST_KEY });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const validationErrors = validationResult(req);
|
|
||||||
if (!validationErrors.isEmpty()) {
|
|
||||||
return res.status(400).json({ ...errors.INVALID_DATA, errors: validationErrors.array() });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { username, password } = req.body;
|
|
||||||
|
|
||||||
const existingUser = await query("SELECT * FROM users WHERE username = $1", [username]);
|
|
||||||
if (existingUser && existingUser.rowCount > 0) {
|
|
||||||
return res.status(400).json({
|
|
||||||
...errors.INVALID_DATA,
|
|
||||||
errors: [ { location: "body", msg: "Username already exists", param: "username" } ]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const hashedPassword = await hash(password, 10);
|
|
||||||
const insertedUser = await query("INSERT INTO users(username, password, is_superuser) VALUES ($1, $2, $3) RETURNING id, username, is_superuser", [username, hashedPassword, false]);
|
|
||||||
if (!insertedUser || insertedUser.rowCount < 1) {
|
|
||||||
return res.status(500).json({
|
|
||||||
...errors.GOT_NO_DATABASE_DATA
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(201).send(insertedUser.rows[0]);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
router.post(
|
|
||||||
"/login",
|
|
||||||
body("username").isLength({ min: 3, max: 32 }).isAlphanumeric("en-US", { ignore: " _-" }),
|
|
||||||
body("password").isLength({ min: 8, max: 1000 }),
|
|
||||||
async (req, res) => {
|
|
||||||
if (authRequestKey) {
|
|
||||||
if (!req.body.requestKey) {
|
|
||||||
return res.status(403).json({ ...errors.BAD_REQUEST_KEY });
|
|
||||||
}
|
|
||||||
const result = await compare(req.body.requestKey, authRequestKey);
|
|
||||||
if (!result) {
|
|
||||||
return res.status(403).json({ ...errors.BAD_REQUEST_KEY });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const validationErrors = validationResult(req);
|
|
||||||
if (!validationErrors.isEmpty()) {
|
|
||||||
return res.status(400).json({ ...errors.BAD_LOGIN_CREDENTIALS });
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = await loginAttempt(req.body.username, req.body.password);
|
|
||||||
if (!token) {
|
|
||||||
return res.status(400).json({ ...errors.BAD_LOGIN_CREDENTIALS });
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({ token });
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
router.get(
|
|
||||||
"/self",
|
|
||||||
authenticateRoute(),
|
|
||||||
(req, res) => {
|
|
||||||
return res.status(200).send(req.publicUser);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
router.post(
|
|
||||||
"/self/promote",
|
|
||||||
authenticateRoute(),
|
|
||||||
body("requestKey").isLength({ min: 1, max: 3000 }),
|
|
||||||
async (req, res) => {
|
|
||||||
if (!superuserKey) {
|
|
||||||
return res.status(403).json({ ...errors.FEATURE_DISABLED });
|
|
||||||
}
|
|
||||||
|
|
||||||
const validationErrors = validationResult(req);
|
|
||||||
if (!validationErrors.isEmpty()) {
|
|
||||||
return res.status(403).json({ ...errors.BAD_REQUEST_KEY });
|
|
||||||
}
|
|
||||||
|
|
||||||
const matches = await compare(req.body.requestKey, superuserKey);
|
|
||||||
|
|
||||||
if (matches) {
|
|
||||||
const updateUserResult = await query("UPDATE users SET is_superuser = true WHERE id = $1", [req.user.id]);
|
|
||||||
if (!updateUserResult || updateUserResult.rowCount < 1) {
|
|
||||||
return res.status(500).json({
|
|
||||||
...errors.GOT_NO_DATABASE_DATA
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return res.status(200).json({});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(403).json({ ...errors.BAD_REQUEST_KEY });
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export default router;
|
|
166
src/rpc/apis/channels.ts
Normal file
166
src/rpc/apis/channels.ts
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
import express from "express";
|
||||||
|
import { channelNameRegex, method, number, string, unsignedNumber, withOptional, withRegexp } from "../rpc";
|
||||||
|
import { query } from "../../database";
|
||||||
|
import { getMessagesByChannelFirstPage, getMessagesByChannelPage } from "../../database/templates";
|
||||||
|
import { errors } from "../../errors";
|
||||||
|
import { dispatch, dispatchChannelSubscribe } from "../../gateway";
|
||||||
|
import { GatewayPayloadType } from "../../gateway/gatewaypayloadtype";
|
||||||
|
import sendMessage from "../../impl";
|
||||||
|
import serverConfig from "../../serverconfig";
|
||||||
|
|
||||||
|
method(
|
||||||
|
"createChannel",
|
||||||
|
[withRegexp(channelNameRegex, string(1, 32))],
|
||||||
|
async (user: User, name: string) => {
|
||||||
|
if (serverConfig.superuserRequirement.createChannel && !user.is_superuser) {
|
||||||
|
return errors.FORBIDDEN_DUE_TO_MISSING_PERMISSIONS;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await query("INSERT INTO channels(name, owner_id) VALUES ($1, $2) RETURNING id, name, owner_id", [name, user.id]);
|
||||||
|
if (!result || result.rowCount < 1) {
|
||||||
|
return errors.GOT_NO_DATABASE_DATA;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch("*", {
|
||||||
|
t: GatewayPayloadType.ChannelCreate,
|
||||||
|
d: result.rows[0]
|
||||||
|
});
|
||||||
|
|
||||||
|
// When a new channel is created, we will currently subscribe every client
|
||||||
|
// on the gateway (this will be changed when the concept of "communities" is added)
|
||||||
|
dispatchChannelSubscribe("*", `channel:${result.rows[0].id}`);
|
||||||
|
|
||||||
|
return result.rows[0];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
method(
|
||||||
|
"updateChannelName",
|
||||||
|
[unsignedNumber(), withRegexp(channelNameRegex, string(1, 32))],
|
||||||
|
async (user: User, id: number, name: string) => {
|
||||||
|
const permissionCheckResult = await query("SELECT owner_id FROM channels WHERE id = $1", [id]);
|
||||||
|
if (!permissionCheckResult || permissionCheckResult.rowCount < 1) {
|
||||||
|
return errors.NOT_FOUND;
|
||||||
|
}
|
||||||
|
if (permissionCheckResult.rows[0].owner_id !== user.id && !user.is_superuser) {
|
||||||
|
return errors.FORBIDDEN_DUE_TO_MISSING_PERMISSIONS;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await query("UPDATE channels SET name = $1 WHERE id = $2", [name, id]);
|
||||||
|
if (!result || result.rowCount < 1) {
|
||||||
|
return errors.GOT_NO_DATABASE_DATA;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatePayload = {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
owner_id: permissionCheckResult.rows[0].owner_id
|
||||||
|
};
|
||||||
|
|
||||||
|
dispatch(`channel:${id}`, {
|
||||||
|
t: GatewayPayloadType.ChannelUpdate,
|
||||||
|
d: updatePayload
|
||||||
|
});
|
||||||
|
|
||||||
|
return updatePayload;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
method(
|
||||||
|
"deleteChannel",
|
||||||
|
[unsignedNumber()],
|
||||||
|
async (user: User, id: number) => {
|
||||||
|
const permissionCheckResult = await query("SELECT owner_id FROM channels WHERE id = $1", [id]);
|
||||||
|
if (!permissionCheckResult || permissionCheckResult.rowCount < 1) {
|
||||||
|
return errors.NOT_FOUND;
|
||||||
|
}
|
||||||
|
if (permissionCheckResult.rows[0].owner_id !== user.id && !user.is_superuser) {
|
||||||
|
return errors.FORBIDDEN_DUE_TO_MISSING_PERMISSIONS;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await query("DELETE FROM channels WHERE id = $1", [id]);
|
||||||
|
if (!result || result.rowCount < 1) {
|
||||||
|
return errors.GOT_NO_DATABASE_DATA;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(`channel:${id}`, {
|
||||||
|
t: GatewayPayloadType.ChannelDelete,
|
||||||
|
d: {id}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {id};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
method(
|
||||||
|
"getChannel",
|
||||||
|
[unsignedNumber()],
|
||||||
|
async (_user: User, id: number) => {
|
||||||
|
const result = await query("SELECT id, name, owner_id FROM channels WHERE id = $1", [id]);
|
||||||
|
if (!result || result.rowCount < 1) {
|
||||||
|
return errors.NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.rows[0];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
method(
|
||||||
|
"getChannels",
|
||||||
|
[],
|
||||||
|
async (_user: User) => {
|
||||||
|
const result = await query("SELECT id, name, owner_id FROM channels");
|
||||||
|
|
||||||
|
return (result && result.rows) ? result.rows : [];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
method(
|
||||||
|
"createChannelMessage",
|
||||||
|
[unsignedNumber(), string(1, 4000), withOptional(unsignedNumber()), withOptional(string(1, 64))],
|
||||||
|
async (user: User, id: number, content: string, optimistic_id: number | null, nick_username: string | null) => {
|
||||||
|
return await sendMessage(user, id, optimistic_id, content, nick_username);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
method(
|
||||||
|
"getChannelMessages",
|
||||||
|
[unsignedNumber(), withOptional(number(5, 100)), withOptional(unsignedNumber())],
|
||||||
|
async (_user: User, channelId: number, count: number | null, before: number | null) => {
|
||||||
|
let limit = count ?? 25;
|
||||||
|
|
||||||
|
let finalRows = [];
|
||||||
|
|
||||||
|
if (before !== null) {
|
||||||
|
const result = await query(getMessagesByChannelPage(limit), [before, channelId]);
|
||||||
|
finalRows = result && result.rows ? result.rows : [];
|
||||||
|
} else {
|
||||||
|
const result = await query(getMessagesByChannelFirstPage(limit), [channelId]);
|
||||||
|
finalRows = result && result.rows ? result.rows : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalRows;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
method(
|
||||||
|
"putChannelTyping",
|
||||||
|
[unsignedNumber()],
|
||||||
|
async (user: User, channelId: number) => {
|
||||||
|
dispatch(`channel:${channelId}`, {
|
||||||
|
t: GatewayPayloadType.TypingStart,
|
||||||
|
d: {
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
username: user.username
|
||||||
|
},
|
||||||
|
channel: {
|
||||||
|
id: channelId
|
||||||
|
},
|
||||||
|
time: 7500
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { time: 7500 };
|
||||||
|
}
|
||||||
|
);
|
79
src/rpc/apis/messages.ts
Normal file
79
src/rpc/apis/messages.ts
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
import { method, string, unsignedNumber } from "./../rpc";
|
||||||
|
import { query } from "../../database";
|
||||||
|
import { getMessageById } from "../../database/templates";
|
||||||
|
import { errors } from "../../errors";
|
||||||
|
import { dispatch } from "../../gateway";
|
||||||
|
import { GatewayPayloadType } from "../../gateway/gatewaypayloadtype";
|
||||||
|
|
||||||
|
method(
|
||||||
|
"deleteMessage",
|
||||||
|
[unsignedNumber()],
|
||||||
|
async (user: User, id: number) => {
|
||||||
|
const permissionCheckResult = await query("SELECT author_id, channel_id FROM messages WHERE id = $1", [id]);
|
||||||
|
if (!permissionCheckResult || permissionCheckResult.rowCount < 1) {
|
||||||
|
return errors.NOT_FOUND;
|
||||||
|
}
|
||||||
|
if (permissionCheckResult.rows[0].author_id !== user.id && !user.is_superuser) {
|
||||||
|
return errors.FORBIDDEN_DUE_TO_MISSING_PERMISSIONS;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await query("DELETE FROM messages WHERE id = $1", [id]);
|
||||||
|
if (!result || result.rowCount < 1) {
|
||||||
|
return errors.GOT_NO_DATABASE_DATA;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(`channel:${permissionCheckResult.rows[0].channel_id}`, {
|
||||||
|
t: GatewayPayloadType.MessageDelete,
|
||||||
|
d: {
|
||||||
|
id,
|
||||||
|
channel_id: permissionCheckResult.rows[0].channel_id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
method(
|
||||||
|
"updateMessageContent",
|
||||||
|
[unsignedNumber(), string(1, 4000)],
|
||||||
|
async (user: User, id: number, content: string) => {
|
||||||
|
const permissionCheckResult = await query(getMessageById, [id]);
|
||||||
|
if (!permissionCheckResult || permissionCheckResult.rowCount < 1) {
|
||||||
|
return errors.NOT_FOUND;
|
||||||
|
}
|
||||||
|
if (permissionCheckResult.rows[0].author_id !== user.id && !user.is_superuser) {
|
||||||
|
return errors.FORBIDDEN_DUE_TO_MISSING_PERMISSIONS;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await query("UPDATE messages SET content = $1 WHERE id = $2", [content, id]);
|
||||||
|
if (!result || result.rowCount < 1) {
|
||||||
|
return errors.GOT_NO_DATABASE_DATA;
|
||||||
|
}
|
||||||
|
|
||||||
|
const returnObject = {
|
||||||
|
...permissionCheckResult.rows[0],
|
||||||
|
content
|
||||||
|
};
|
||||||
|
|
||||||
|
dispatch(`channel:${permissionCheckResult.rows[0].channel_id}`, {
|
||||||
|
t: GatewayPayloadType.MessageUpdate,
|
||||||
|
d: returnObject
|
||||||
|
});
|
||||||
|
|
||||||
|
return returnObject;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
method(
|
||||||
|
"getMessage",
|
||||||
|
[unsignedNumber()],
|
||||||
|
async (user: User, id: number) => {
|
||||||
|
const result = await query(getMessageById, [id]);
|
||||||
|
if (!result || result.rowCount < 1) {
|
||||||
|
return errors.NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.rows[0];
|
||||||
|
}
|
||||||
|
)
|
76
src/rpc/apis/users.ts
Normal file
76
src/rpc/apis/users.ts
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
import { errors } from "../../errors";
|
||||||
|
import { query } from "../../database";
|
||||||
|
import { compare, hash, hashSync } from "bcrypt";
|
||||||
|
import { getPublicUserObject, loginAttempt } from "../../auth";
|
||||||
|
import { method, methodButWarningDoesNotAuthenticate, string, usernameRegex, withRegexp } from "./../rpc";
|
||||||
|
|
||||||
|
const superuserKey = process.env.SUPERUSER_KEY ? hashSync(process.env.SUPERUSER_KEY, 10) : null;
|
||||||
|
|
||||||
|
methodButWarningDoesNotAuthenticate(
|
||||||
|
"createUser",
|
||||||
|
[withRegexp(usernameRegex, string(3, 32)), string(8, 1000)],
|
||||||
|
async (username: string, password: string) => {
|
||||||
|
if (process.env.DISABLE_ACCOUNT_CREATION === "true") {
|
||||||
|
return errors.FEATURE_DISABLED;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingUser = await query("SELECT * FROM users WHERE username = $1", [username]);
|
||||||
|
if (existingUser && existingUser.rowCount > 0) {
|
||||||
|
return {
|
||||||
|
...errors.INVALID_DATA,
|
||||||
|
errors: [ { index: 0, msg: "Username already exists" } ]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const hashedPassword = await hash(password, 10);
|
||||||
|
const insertedUser = await query("INSERT INTO users(username, password, is_superuser) VALUES ($1, $2, $3) RETURNING id, username, is_superuser", [username, hashedPassword, false]);
|
||||||
|
if (!insertedUser || insertedUser.rowCount < 1) {
|
||||||
|
return errors.GOT_NO_DATABASE_DATA;
|
||||||
|
}
|
||||||
|
|
||||||
|
return insertedUser.rows[0];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
methodButWarningDoesNotAuthenticate(
|
||||||
|
"loginUser",
|
||||||
|
[withRegexp(usernameRegex, string(3, 32)), string(8, 1000)],
|
||||||
|
async (username: string, password: string) => {
|
||||||
|
const token = await loginAttempt(username, password);
|
||||||
|
if (!token) {
|
||||||
|
return errors.BAD_LOGIN_CREDENTIALS;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { token };
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
method(
|
||||||
|
"getUserSelf",
|
||||||
|
[],
|
||||||
|
(user: User) => {
|
||||||
|
return getPublicUserObject(user);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
method(
|
||||||
|
"promoteUserSelf",
|
||||||
|
[string(1, 1000)],
|
||||||
|
async (user: User, key: string) => {
|
||||||
|
if (!superuserKey) {
|
||||||
|
return errors.FEATURE_DISABLED;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matches = await compare(key, superuserKey);
|
||||||
|
|
||||||
|
if (matches) {
|
||||||
|
const updateUserResult = await query("UPDATE users SET is_superuser = true WHERE id = $1", [user.id]);
|
||||||
|
if (!updateUserResult || updateUserResult.rowCount < 1) {
|
||||||
|
return errors.GOT_NO_DATABASE_DATA;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.BAD_REQUEST_KEY;
|
||||||
|
}
|
||||||
|
)
|
13
src/rpc/index.ts
Normal file
13
src/rpc/index.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import "./rpc";
|
||||||
|
import "./apis/users";
|
||||||
|
import "./apis/channels";
|
||||||
|
import "./apis/messages";
|
||||||
|
import { methodNameToId, methods } from "./rpc";
|
||||||
|
|
||||||
|
console.log("--- begin rpc method map ---")
|
||||||
|
const methodMap: any = Object.fromEntries(methodNameToId);
|
||||||
|
for (const key of Object.keys(methodMap)) {
|
||||||
|
methodMap[key] = [methodMap[key], methods.get(methodMap[key])?.requiresAuthentication];
|
||||||
|
}
|
||||||
|
console.log(methodMap);
|
||||||
|
console.log("--- end rpc method map ---");
|
155
src/rpc/rpc.ts
Normal file
155
src/rpc/rpc.ts
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
import { errors } from "../errors";
|
||||||
|
|
||||||
|
export const alphanumericRegex = new RegExp(/^[a-z0-9]+$/i);
|
||||||
|
export const usernameRegex = new RegExp(/^[a-z0-9_]+$/i);
|
||||||
|
export const channelNameRegex = new RegExp(/^[a-z0-9_\- ]+$/i);
|
||||||
|
|
||||||
|
const defaultStringMaxLength = 3000;
|
||||||
|
|
||||||
|
export const unsignedNumber = (): RPCArgument => ({ type: RPCArgumentType.Number, minValue: 0 });
|
||||||
|
export const number = (minValue?: number, maxValue?: number): RPCArgument => ({ type: RPCArgumentType.Number, minValue, maxValue });
|
||||||
|
export const string = (minLength = 0, maxLength = defaultStringMaxLength): RPCArgument => ({ type: RPCArgumentType.String, minLength, maxLength });
|
||||||
|
export const withRegexp = (regexp: RegExp, arg: RPCArgument): RPCArgument => ({ minLength: 0, maxLength: defaultStringMaxLength, ...arg, regexp });
|
||||||
|
export const withOptional = (arg: RPCArgument): RPCArgument => ({ ...arg, isOptional: true });
|
||||||
|
|
||||||
|
|
||||||
|
enum RPCArgumentType {
|
||||||
|
Number,
|
||||||
|
String
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RPCArgument {
|
||||||
|
type: RPCArgumentType
|
||||||
|
isOptional?: boolean
|
||||||
|
|
||||||
|
// strings
|
||||||
|
minLength?: number
|
||||||
|
maxLength?: number
|
||||||
|
regexp?: RegExp
|
||||||
|
|
||||||
|
// numbers
|
||||||
|
minValue?: number
|
||||||
|
maxValue?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RPCMethod {
|
||||||
|
args: RPCArgument[],
|
||||||
|
func: ((...args: any[]) => any)
|
||||||
|
requiresAuthentication: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const methods: Map<number, RPCMethod> = new Map();
|
||||||
|
export const methodNameToId: Map<string, number> = new Map();
|
||||||
|
let lastMethodId = 0;
|
||||||
|
|
||||||
|
export const method = (name: string, args: RPCArgument[], func: ((...args: any[]) => any), requiresAuthentication: boolean = true) => {
|
||||||
|
let id = lastMethodId++;
|
||||||
|
methodNameToId.set(name, id);
|
||||||
|
methods.set(id, { args, func, requiresAuthentication });
|
||||||
|
return id;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const methodButWarningDoesNotAuthenticate = (name: string, args: RPCArgument[], func: ((...args: any[]) => any)) => {
|
||||||
|
return method(name, args, func, false);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const userInvokeMethod = async (user: User | null, methodId: number, args: any[]) => {
|
||||||
|
const methodData = methods.get(methodId);
|
||||||
|
if (!methodData) return {
|
||||||
|
...errors.INVALID_RPC_CALL,
|
||||||
|
detail: "The method was not found."
|
||||||
|
};
|
||||||
|
|
||||||
|
const argSchema = methodData.args;
|
||||||
|
if (argSchema.length !== args.length) return {
|
||||||
|
...errors.INVALID_RPC_CALL,
|
||||||
|
detail: "Invalid number of arguments provided to method."
|
||||||
|
};
|
||||||
|
|
||||||
|
const validationErrors = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < argSchema.length; i++) {
|
||||||
|
const argument = args[i];
|
||||||
|
const schema = argSchema[i];
|
||||||
|
if (schema.isOptional && (argument === undefined || argument === null)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
switch (schema.type) {
|
||||||
|
case RPCArgumentType.Number: {
|
||||||
|
if (typeof argument !== "number") {
|
||||||
|
validationErrors.push({ index: i, msg: `Expected type number, got type ${typeof argument}.` });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (schema.minValue !== undefined && argument < schema.minValue) {
|
||||||
|
validationErrors.push({ index: i, msg: `Provided number is below minimum value of ${schema.minValue}.` });
|
||||||
|
}
|
||||||
|
if (schema.maxValue !== undefined && argument > schema.maxValue) {
|
||||||
|
validationErrors.push({ index: i, msg: `Provided number is above maximum value of ${schema.maxValue}.` });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case RPCArgumentType.String: {
|
||||||
|
if (typeof argument !== "string") {
|
||||||
|
validationErrors.push({ index: i, msg: `Expected type string, got type ${typeof argument}.` });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ((schema.minLength !== undefined && argument.length < schema.minLength) || (schema.maxLength !== undefined && argument.length > schema.maxLength)) {
|
||||||
|
validationErrors.push({ index: i, msg: `Must be between ${schema.minLength} and ${schema.maxLength} characters long.` });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (schema.regexp && !schema.regexp?.test(argument)) {
|
||||||
|
validationErrors.push({ index: i, msg: `Contains invalid characters.` });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validationErrors.length !== 0) {
|
||||||
|
return {
|
||||||
|
...errors.INVALID_DATA,
|
||||||
|
errors: validationErrors
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
return await ((methodData.func)(user, ...args));
|
||||||
|
} else if (!user && !methodData.requiresAuthentication) {
|
||||||
|
return await ((methodData.func)(...args));
|
||||||
|
} else {
|
||||||
|
return errors.BAD_AUTH;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const processMethodBatch = async (user: User | null, calls: any) => {
|
||||||
|
if (!Array.isArray(calls) || !calls.length || calls.length > 5) {
|
||||||
|
return {
|
||||||
|
...errors.INVALID_RPC_CALL,
|
||||||
|
detail: "Expected RPC batch: an array of arrays with at least a single element and at most 5 elements, where each inner array represents a method call."
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const responses = new Array(calls.length);
|
||||||
|
const promises = new Array(calls.length);
|
||||||
|
calls.forEach((call, index) => {
|
||||||
|
if (!Array.isArray(call) || !call.length || call.length > 8) {
|
||||||
|
responses[index] = {
|
||||||
|
...errors.INVALID_RPC_CALL,
|
||||||
|
detail: "Invalid method call. Expected inner array with at least one element and at most 8 elements."
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const promise = userInvokeMethod(user, call[0], call.slice(1, call.length));
|
||||||
|
promise.then(value => responses[index] = value);
|
||||||
|
promises[index] = promise;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.all(promises);
|
||||||
|
return responses;
|
||||||
|
} catch(e) {
|
||||||
|
console.error("exception while invoking RPC method", e);
|
||||||
|
return errors.INTERNAL_ERROR;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
import express, { Application, ErrorRequestHandler, json } from "express";
|
import express, { Application, ErrorRequestHandler, json } from "express";
|
||||||
import usersRouter from "./routes/api/v1/users";
|
import "./rpc";
|
||||||
import channelsRouter from "./routes/api/v1/channels";
|
import rpcRouter from "./routes/api/v1/rpc";
|
||||||
import messagesRouter from "./routes/api/v1/messages";
|
|
||||||
import matrixRouter from "./routes/matrix";
|
import matrixRouter from "./routes/matrix";
|
||||||
import { errors } from "./errors";
|
import { errors } from "./errors";
|
||||||
|
|
||||||
|
@ -9,9 +8,7 @@ const ENABLE_MATRIX_LAYER = false;
|
||||||
|
|
||||||
export default function(app: Application) {
|
export default function(app: Application) {
|
||||||
app.use(json());
|
app.use(json());
|
||||||
app.use("/api/v1/users", usersRouter);
|
app.use("/api/v1/rpc", rpcRouter);
|
||||||
app.use("/api/v1/channels", channelsRouter);
|
|
||||||
app.use("/api/v1/messages", messagesRouter);
|
|
||||||
app.use("/", express.static("frontend/public"));
|
app.use("/", express.static("frontend/public"));
|
||||||
if (ENABLE_MATRIX_LAYER) {
|
if (ENABLE_MATRIX_LAYER) {
|
||||||
app.use("/", matrixRouter);
|
app.use("/", matrixRouter);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
export default {
|
export default {
|
||||||
superuserRequirement: {
|
superuserRequirement: {
|
||||||
createChannel: true
|
createChannel: false
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
3
src/types/express.d.ts
vendored
3
src/types/express.d.ts
vendored
|
@ -1,6 +1,7 @@
|
||||||
declare namespace Express {
|
declare namespace Express {
|
||||||
export interface Request {
|
export interface Request {
|
||||||
user: User,
|
user: User,
|
||||||
publicUser: User
|
publicUser: User,
|
||||||
|
authenticated: boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
1
src/types/gatewaypayload.d.ts
vendored
1
src/types/gatewaypayload.d.ts
vendored
|
@ -3,4 +3,5 @@ import { GatewayPayloadType } from "../gateway/gatewaypayloadtype";
|
||||||
declare interface GatewayPayload {
|
declare interface GatewayPayload {
|
||||||
t: GatewayPayloadType;
|
t: GatewayPayloadType;
|
||||||
d: any;
|
d: any;
|
||||||
|
s?: number; // Serial. Used for RPCRequest/RPCResponse
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { GatewayPresenceStatus } from "./gatewaypayloadtype"
|
import { GatewayPresenceStatus } from "../gateway/gatewaypayloadtype"
|
||||||
|
|
||||||
export interface GatewayPresenceEntry {
|
export interface GatewayPresenceEntry {
|
||||||
user: {
|
user: {
|
Loading…
Reference in a new issue