Merge branch 'main' into create-messages

This commit is contained in:
2023-07-31 14:39:45 -04:00
11 changed files with 196 additions and 121 deletions

View File

@@ -8,12 +8,5 @@ services:
volumes:
- .:/app
restart: always
depends_on:
- mongodb
environment:
- NODE_ENV=production
mongodb:
image: mongo
restart: always
ports:
- '27017:27017'

42
package-lock.json generated
View File

@@ -15,9 +15,11 @@
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"discreetly-claimcodes": "^1.1.3",
"discreetly-interfaces": "^0.1.21",
"discreetly-interfaces": "^0.1.22",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"express-basic-auth": "^1.2.1",
"helmet": "^7.0.0",
"mongodb": "^5.7.0",
"poseidon-lite": "^0.2.0",
"prisma-cache-middleware": "^0.1.4",
@@ -1407,6 +1409,22 @@
"node": "^4.5.0 || >= 5.9"
}
},
"node_modules/basic-auth": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
"integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
"dependencies": {
"safe-buffer": "5.1.2"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/basic-auth/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"node_modules/bfj": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/bfj/-/bfj-7.0.2.tgz",
@@ -1887,9 +1905,9 @@
"integrity": "sha512-2QnlhYUPIGLl11XgxIxl6ZKIJZoS2T1ABIHbqjBbec0YYQ2qfWZ7JWH53OZm0mqeO8Dbjon5zK3YNoGiuYJ1Gg=="
},
"node_modules/discreetly-interfaces": {
"version": "0.1.21",
"resolved": "https://registry.npmjs.org/discreetly-interfaces/-/discreetly-interfaces-0.1.21.tgz",
"integrity": "sha512-REQ+QUAWprx7j2RPMZi+55Xlfr9pgUHAHnQ9V14FMjnhE6iXl4SWHvg5nskU5EKxrMn2C6AhEd/Aj1T4uwMWHA==",
"version": "0.1.22",
"resolved": "https://registry.npmjs.org/discreetly-interfaces/-/discreetly-interfaces-0.1.22.tgz",
"integrity": "sha512-p7HuKOE0Zan3Nf3Rg6auzsxNeKrtH4GMRoSMhsb+6YGO9AlF6ffnlHV4L4SSxLbJr2XInGEf0TUzsKkXwkQKoQ==",
"dependencies": {
"poseidon-lite": "^0.2.0",
"rlnjs": "^3.1.4"
@@ -2383,6 +2401,14 @@
"node": ">= 0.10.0"
}
},
"node_modules/express-basic-auth": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/express-basic-auth/-/express-basic-auth-1.2.1.tgz",
"integrity": "sha512-L6YQ1wQ/mNjVLAmK3AG1RK6VkokA1BIY6wmiH304Xtt/cLTps40EusZsU1Uop+v9lTDPxdtzbFmdXfFO3KEnwA==",
"dependencies": {
"basic-auth": "^2.0.1"
}
},
"node_modules/express/node_modules/body-parser": {
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
@@ -2852,6 +2878,14 @@
"minimalistic-assert": "^1.0.1"
}
},
"node_modules/helmet": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/helmet/-/helmet-7.0.0.tgz",
"integrity": "sha512-MsIgYmdBh460ZZ8cJC81q4XJknjG567wzEmv46WOBblDb6TUd3z8/GhgmsM9pn8g2B80tAJ4m5/d3Bi1KrSUBQ==",
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/hoopy": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz",

View File

@@ -11,8 +11,7 @@
"start": "node dist/server.cjs",
"watch": "rollup --config rollup.config.mjs --watch",
"serve": "nodemon -q dist/server.jcs",
"dev": "concurrently \"npm run watch\" \"npm run serve\"",
"docker": "docker-compose up --build"
"dev": "concurrently \"npm run watch\" \"npm run serve\""
},
"engines": {
"node": "18.x.x"
@@ -31,6 +30,8 @@
"discreetly-interfaces": "^0.1.22",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"express-basic-auth": "^1.2.1",
"helmet": "^7.0.0",
"mongodb": "^5.7.0",
"poseidon-lite": "^0.2.0",
"prisma-cache-middleware": "^0.1.4",
@@ -52,10 +53,10 @@
"eslint": "^8.45.0",
"nodemon": "^3.0.1",
"prisma": "^5.0.0",
"rollup": "^3.26.2",
"rollup-plugin-cleaner": "^1.0.0",
"rollup-plugin-include-sourcemaps": "^0.7.0",
"rollup-plugin-typescript2": "^0.35.0",
"rollup": "^3.26.2",
"ts-node": "^10.9.1",
"typescript": "^5.1.6"
}

View File

@@ -21,6 +21,7 @@ model Rooms {
roomId String @unique
name String
rateLimit Int @default(1000) // epoch length in ms
banRateLimit Int @default(1000000) // starting number of epochs banned for
userMessageLimit Int @default(1) // per epoch
membershipType RoomMembershipType @default(IDENTITY_LIST)
identities String[] @default([])

View File

@@ -6,7 +6,7 @@ import { Group } from '@semaphore-protocol/group';
const v = new RLNVerifier(vkey);
async function verifyProof(msg: MessageI, room: RoomI): Promise<boolean> {
async function verifyProof(msg: MessageI, room: RoomI, epochErrorRange = 5): Promise<boolean> {
console.log('check room', room);
const timestamp = Date.now();
const rateLimit = room.rateLimit ? room.rateLimit : 1000;
@@ -15,7 +15,7 @@ async function verifyProof(msg: MessageI, room: RoomI): Promise<boolean> {
const msgHash = str2BigInt(msg.message);
// Check that the epoch falls within the range for the room
const epoch = BigInt(msg.epoch);
if (epoch < currentEpoch - 1 || epoch > currentEpoch + 1) {
if (epoch < currentEpoch - epochErrorRange || epoch > currentEpoch + epochErrorRange) {
// Too old or too far in the future
console.warn('Epoch out of range:', epoch, 'currentEpoch:', currentEpoch);
return false;

View File

@@ -10,7 +10,6 @@ import type { ClaimCodeT } from 'discreetly-claimcodes';
const prisma = new PrismaClient();
interface CodeStatus {
claimed: boolean;
roomIds: string[];
@@ -21,21 +20,23 @@ interface ClaimCode {
}
export function getRoomByID(id: string): Promise<RoomI> {
return prisma.rooms.findUnique({
where: {
roomId: id
},
select: {
id: true,
roomId: true,
name: true,
identities: true,
rateLimit: true,
userMessageLimit: true,
}
}).then((room) => {
return room
})
return prisma.rooms
.findUnique({
where: {
roomId: id
},
select: {
id: true,
roomId: true,
name: true,
identities: true,
rateLimit: true,
userMessageLimit: true
}
})
.then((room) => {
return room;
})
.catch((err) => {
console.error(err);
throw err; // Add this line to throw the error
@@ -78,13 +79,21 @@ export function createRoom(
userMessageLimit: number = 1,
numClaimCodes: number = 0,
approxNumMockUsers: number = 20
) {
): boolean {
function genMockUsers(numMockUsers: number): string[] {
// Generates random number of mock users between 0.5 x numMockusers and 2 x numMockUsers
const newNumMockUsers = randn_bm(numMockUsers / 2, numMockUsers * 2);
const mockUsers: string[] = [];
for (let i = 0; i < newNumMockUsers; i++) {
mockUsers.push(genId(serverConfig.id, 'Mock User ' + i).toString());
mockUsers.push(
genId(
serverConfig.id,
// Generates a random string of length 10
Math.random()
.toString(36)
.substring(2, 2 + 10) + i
).toString()
);
}
return mockUsers;
}
@@ -118,36 +127,39 @@ export function createRoom(
prisma.rooms
.upsert(roomData)
.then(() => { })
.then(() => {
return true;
})
.catch((err) => console.error(err));
return false;
}
export function findClaimCode(code: string): Promise<CodeStatus> {
return prisma.claimCodes.findUnique({
where: { claimcode: code },
where: { claimcode: code }
});
}
export function updateClaimCode(code: string): Promise<ClaimCode> {
return prisma.claimCodes.update({
where: { claimcode: code },
data: { claimed: true },
data: { claimed: true }
});
}
export function updateRoomIdentities(idc: string, roomIds: string[]): Promise<any> {
export function updateRoomIdentities(idc: string, roomIds: string[]): Promise<unknown> {
return prisma.rooms.updateMany({
where: { id: { in: roomIds } },
data: {
identities: {
push: idc
},
},
}
}
});
}
export function findUpdatedRooms(roomIds: string[]): Promise<RoomI[]> {
return prisma.rooms.findMany({
where: { id: { in: roomIds } },
where: { id: { in: roomIds } }
});
}

View File

@@ -1,14 +1,20 @@
import type { Express } from 'express';
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import type { Express, RequestHandler } from 'express';
import { PrismaClient } from '@prisma/client';
import { serverConfig } from '../config/serverConfig';
import { pp } from '../utils.js';
import { getRoomByID, getRoomsByIdentity, findClaimCode, updateClaimCode, updateRoomIdentities, findUpdatedRooms } from '../data/db';
import { RoomI, genId } from 'discreetly-interfaces';
import {
getRoomByID,
getRoomsByIdentity,
findClaimCode,
updateClaimCode,
updateRoomIdentities,
findUpdatedRooms,
createRoom
} from '../data/db';
import { RoomI } from 'discreetly-interfaces';
// TODO! Properly handle authentication for admin endpoints
// TODO api endpoint that creates new rooms and generates invite codes for them
export function initEndpoints(app: Express) {
export function initEndpoints(app: Express, adminAuth: RequestHandler) {
const prisma = new PrismaClient();
app.get(['/', '/api'], (req, res) => {
@@ -16,33 +22,6 @@ export function initEndpoints(app: Express) {
res.json(serverConfig);
});
app.get('/logclaimcodes', (req, res) => {
pp('Express: fetching claim codes');
prisma.claimCodes
.findMany()
.then((claimCodes) => {
console.log(claimCodes);
res.status(401).send('Unauthorized');
})
.catch((err) => {
console.error(err);
res.status(500).json({ error: 'Internal Server Error' });
});
});
app.get('/api/rooms', (req, res) => {
pp(String('Express: fetching all rooms'));
prisma.rooms
.findMany()
.then((rooms) => {
res.status(200).json(rooms);
})
.catch((err) => {
console.error(err);
res.status(500).json({ error: 'Internal Server Error' });
});
});
app.get('/api/room/:id', (req, res) => {
pp(String('Express: fetching room info for ' + req.params.id));
getRoomByID(req.params.id)
@@ -62,61 +41,79 @@ export function initEndpoints(app: Express) {
res.json(getRoomsByIdentity(req.params.idc));
});
app.post("/join", (req, res) => {
const { code, idc } = req.body;
app.post('/join', (req, res) => {
const { code, idc }: { code: string; idc: string } = req.body;
findClaimCode(code)
.then((codeStatus) => {
if (codeStatus && codeStatus.claimed === false) {
return updateClaimCode(code)
.then((claimCode) => {
const roomIds = claimCode.roomIds.map((room) => room);
return updateRoomIdentities(idc, roomIds)
.then(() => {
return findUpdatedRooms(roomIds)
.then((updatedRooms: RoomI[]) => {
return res.status(200).json({
status: "valid",
roomIds: updatedRooms.map((room) => room.roomId),
});
});
return updateClaimCode(code).then((claimCode) => {
const roomIds = claimCode.roomIds.map((room) => room);
return updateRoomIdentities(idc, roomIds).then(() => {
return findUpdatedRooms(roomIds).then((updatedRooms: RoomI[]) => {
return res.status(200).json({
status: 'valid',
roomIds: updatedRooms.map((room) => room.roomId as string)
});
});
});
});
} else {
res.status(400).json({ message: "Claim code already used" });
res.status(400).json({ message: 'Claim code already used' });
}
})
.catch((err: Error) => {
console.error(err);
res.status(500).json({ error: "Internal Server Error" });
res.status(500).json({ error: 'Internal Server Error' });
});
});
app.post('/room/add', (req, res) => {
/* ~~~~ ADMIN ENDPOINTS ~~~~ */
app.post('/room/add', adminAuth, (req, res) => {
interface RoomData {
password: string;
roomName: string;
rateLimit: number;
userMessageLimit: number;
numClaimCodes?: number;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const { password, roomName } = req.body.data as RoomData;
if (password === process.env.PASSWORD) {
prisma.rooms
.create({
data: {
roomId: genId(BigInt(serverConfig.id), roomName).toString(),
name: roomName
}
})
.then((newRoom) => {
res.status(200).json(newRoom);
})
.catch((error: Error) => {
console.error(error);
res.status(500).send('Error creating new room');
});
const roomMetadata = req.body.data as RoomData;
const roomName = roomMetadata.roomName;
const rateLimit = roomMetadata.rateLimit;
const userMessageLimit = roomMetadata.userMessageLimit;
const numClaimCodes = roomMetadata.numClaimCodes || 0;
const result = createRoom(roomName, rateLimit, userMessageLimit, numClaimCodes);
if (result) {
// TODO should return roomID and claim codes if they are generated
res.status(200).json({ message: 'Room created successfully' });
} else {
res.status(401).send('Unauthorized');
return res.status(500).json({ error: 'Internal Server Error' });
}
});
app.get('/logclaimcodes', adminAuth, (req, res) => {
pp('Express: fetching claim codes');
prisma.claimCodes
.findMany()
.then((claimCodes) => {
res.status(401).json(claimCodes);
})
.catch((err) => {
console.error(err);
res.status(500).json({ error: 'Internal Server Error' });
});
});
app.get('/api/rooms', adminAuth, (req, res) => {
pp(String('Express: fetching all rooms'));
prisma.rooms
.findMany()
.then((rooms) => {
res.status(200).json(rooms);
})
.catch((err) => {
console.error(err);
res.status(500).json({ error: 'Internal Server Error' });
});
});
}

16
src/endpoints/utils.ts Normal file
View File

@@ -0,0 +1,16 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
export function listEndpoints(app) {
const table = [];
for (const r of app._router.stack) {
if (r.route && r.route.path) {
const methods = Object.keys(r.route.methods).join(', ').toUpperCase();
table.push({
Path: r.route.path,
Methods: methods
});
}
}
console.table(table);
}

View File

@@ -1,13 +1,19 @@
import express from 'express';
import { Server } from 'http';
import { Server as SocketIOServer } from 'socket.io';
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import basicAuth from 'express-basic-auth';
import { Server as SocketIOServer } from 'socket.io';
import { serverConfig } from './config/serverConfig';
import { pp, shim } from './utils';
import mock from './data/mock';
import { websocketSetup as initWebsockets } from './websockets/index';
import { initEndpoints } from './endpoints/index';
import { createMessage } from './data/messages';
import { generateRandomClaimCode } from 'discreetly-claimcodes';
import { listEndpoints } from './endpoints/utils';
// TODO https://www.npmjs.com/package/winston
const app = express();
const socket_server = new Server(app);
@@ -19,6 +25,19 @@ app.use(
origin: '*'
})
);
app.use(helmet());
app.disable('x-powered-by');
const admin_password = process.env.PASSWORD
? process.env.PASSWORD
: // eslint-disable-next-line @typescript-eslint/no-unsafe-call
(generateRandomClaimCode(4) as string);
const adminAuth = basicAuth({
users: {
admin: admin_password
}
});
const io = new SocketIOServer(socket_server, {
cors: {
@@ -44,11 +63,14 @@ function initAppListeners() {
if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') {
console.log('~~~~DEVELOPMENT MODE~~~~');
initWebsockets(io);
initEndpoints(app);
initEndpoints(app, adminAuth);
listEndpoints(app);
initAppListeners();
mock(io);
// TODO! This is dangerous and only for development
console.log('Admin password: ' + admin_password);
} else {
initWebsockets(io);
initEndpoints(app);
initEndpoints(app, adminAuth);
initAppListeners();
}

View File

@@ -16,7 +16,7 @@ export function websocketSetup(io: SocketIOServer) {
socket.on('validateMessage', (msg: MessageI) => {
pp({ 'VALIDATING MESSAGE ID': msg.id.slice(0, 11), 'MSG:': msg.message });
let valid: boolean;
getRoomByID(msg.roomId.toString())
getRoomByID(String(msg.roomId))
.then((room: RoomI) => {
if (!room) {
pp('INVALID ROOM', 'warn');
@@ -25,7 +25,7 @@ export function websocketSetup(io: SocketIOServer) {
verifyProof(msg, room)
.then((v) => {
valid = v;
createMessage(msg.roomId.toString(), msg);
createMessage(String(msg.roomId), msg);
io.emit('messageBroadcast', msg);
})
.catch((err) => {
@@ -35,7 +35,6 @@ export function websocketSetup(io: SocketIOServer) {
pp('INVALID MESSAGE', 'warn');
return;
}
})
.catch((err) => pp(err, 'error'));
});

View File

@@ -16,7 +16,7 @@
"./src/server.ts"
],
"include": [
"src/**/*",
"src/types/index.ts"
"./src/**/*",
"./src/types/index.ts"
]
}