-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Either a user joins the class directly - Or the user creates a request to join - Fix a bug regarding usernames/display names - Patch a security vulnerability regarding refresh tokens
- Loading branch information
Showing
12 changed files
with
329 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
CREATE MIGRATION m1n5yfow7yvygzyzix542fym6svcupjqrwdsj6zu54subohzbz52eq | ||
ONTO m1psgxwipft63xhdvv24e4bboazf26v6doqseu3nyzyoa3oecmjp4q | ||
{ | ||
CREATE SCALAR TYPE default::Status EXTENDING enum<Pending, Accepted, Rejected>; | ||
CREATE TYPE default::JoinRequest { | ||
CREATE REQUIRED LINK user: default::User { | ||
SET readonly := true; | ||
}; | ||
CREATE REQUIRED LINK wantsToJoin: default::Class { | ||
SET readonly := true; | ||
}; | ||
CREATE REQUIRED PROPERTY created: std::datetime { | ||
SET default := (std::datetime_current()); | ||
SET readonly := true; | ||
}; | ||
CREATE REQUIRED PROPERTY status: default::Status { | ||
SET default := 'Pending'; | ||
}; | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
CREATE MIGRATION m1g3bsb7rj3ypvusgnkekg6g7zetyzd7xvl564gqjkdb4zwjfphyyq | ||
ONTO m1n5yfow7yvygzyzix542fym6svcupjqrwdsj6zu54subohzbz52eq | ||
{ | ||
ALTER TYPE default::JoinRequest { | ||
CREATE LINK reviewedBy: default::User; | ||
CREATE PROPERTY reviewedAt: std::datetime; | ||
}; | ||
ALTER TYPE default::User { | ||
ALTER PROPERTY displayname { | ||
DROP CONSTRAINT std::exclusive; | ||
}; | ||
ALTER PROPERTY username { | ||
CREATE CONSTRAINT std::exclusive; | ||
}; | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
import e from "@edgedb"; | ||
import { | ||
DATABASE_READ_FAILED, | ||
DATABASE_WRITE_FAILED, | ||
UNAUTHORIZED, | ||
} from "constants/responses"; | ||
import Elysia, { t } from "elysia"; | ||
import { HttpStatusCode } from "elysia-http-status-code"; | ||
import { client } from "index"; | ||
import { auth } from "plugins/auth"; | ||
import { getAmountOfMembersOfClass } from "utils/db/classes"; | ||
import { doesRequestAlreadyExist } from "utils/db/requests"; | ||
import { promiseResult } from "utils/errors"; | ||
import { responseBuilder } from "utils/response"; | ||
|
||
export const createJoinRequest = new Elysia() | ||
.use(auth) | ||
.use(HttpStatusCode()) | ||
.post( | ||
"/", | ||
async ({ auth, body, httpStatus, set }) => { | ||
if (!auth.isAuthorized) { | ||
return UNAUTHORIZED; | ||
} | ||
|
||
const countResult = await promiseResult(() => | ||
getAmountOfMembersOfClass({ | ||
className: body.class, | ||
schoolName: body.school, | ||
}), | ||
); | ||
if (countResult.status === "error") { | ||
set.status = httpStatus.HTTP_500_INTERNAL_SERVER_ERROR; | ||
return DATABASE_READ_FAILED; | ||
} | ||
|
||
if (countResult.data === 0) { | ||
const joinClassquery = e.update(e.Class, (c) => { | ||
const classNameMatches = e.op(c.name, "=", body.class); | ||
const schoolNameMatches = e.op(c.school.name, "=", body.school); | ||
|
||
return { | ||
filter_single: e.op(classNameMatches, "and", schoolNameMatches), | ||
set: { | ||
students: { | ||
"+=": e.select(e.User, (u) => ({ | ||
filter_single: e.op(u.username, "=", auth.username), | ||
})), | ||
}, | ||
}, | ||
}; | ||
}); | ||
const joinResult = await promiseResult(() => | ||
joinClassquery.run(client), | ||
); | ||
if (joinResult.status === "error") { | ||
set.status = httpStatus.HTTP_500_INTERNAL_SERVER_ERROR; | ||
return DATABASE_WRITE_FAILED; | ||
} | ||
if (!joinResult.data) { | ||
set.status = httpStatus.HTTP_404_NOT_FOUND; | ||
return responseBuilder("error", { | ||
error: "Class or school not found", | ||
}); | ||
} | ||
|
||
return responseBuilder("success", { | ||
message: "Joined class successfully!", | ||
data: null, | ||
}); | ||
} | ||
|
||
const isUserAlreadyInClassQuery = e.count(e.select(e.Class, (c) => { | ||
const classNameMatches = e.op(c.name, "=", body.class); | ||
const schoolNameMatches = e.op(c.school.name, "=", body.school); | ||
const userMatches = e.op(c.students.username, "=", auth.username); | ||
|
||
return { | ||
filter_single: e.op( | ||
e.op(classNameMatches, "and", schoolNameMatches), | ||
"and", | ||
userMatches, | ||
), | ||
}; | ||
})); | ||
const isUserAlreadyInClassResult = await promiseResult(() => | ||
isUserAlreadyInClassQuery.run(client), | ||
); | ||
if (isUserAlreadyInClassResult.isError) { | ||
set.status = httpStatus.HTTP_500_INTERNAL_SERVER_ERROR; | ||
return DATABASE_READ_FAILED; | ||
} | ||
if (isUserAlreadyInClassResult.data > 0) { | ||
set.status = httpStatus.HTTP_400_BAD_REQUEST; | ||
return responseBuilder("error", { | ||
error: "User is already in class", | ||
}); | ||
} | ||
|
||
const doesReqAlreadyExistResult = await promiseResult(() => | ||
doesRequestAlreadyExist({ | ||
username: auth.username, | ||
class: body.class, | ||
school: body.school, | ||
}), | ||
); | ||
if (doesReqAlreadyExistResult.status === "error") { | ||
set.status = httpStatus.HTTP_500_INTERNAL_SERVER_ERROR; | ||
return DATABASE_READ_FAILED; | ||
} | ||
if (doesReqAlreadyExistResult.data) { | ||
return responseBuilder("error", { | ||
error: "Request already exists", | ||
}); | ||
} | ||
|
||
const joinRequestQuery = e.insert(e.JoinRequest, { | ||
user: e.select(e.User, (u) => ({ | ||
filter_single: e.op(u.username, "=", auth.username), | ||
})), | ||
wantsToJoin: e.select(e.Class, (c) => { | ||
const classNameMatches = e.op(c.name, "=", body.class); | ||
const schoolNameMatches = e.op(c.school.name, "=", body.school); | ||
return { | ||
filter_single: e.op(classNameMatches, "and", schoolNameMatches), | ||
}; | ||
}), | ||
}); | ||
const joinRequestResult = await promiseResult(() => | ||
joinRequestQuery.run(client), | ||
); | ||
if (joinRequestResult.status === "error") { | ||
set.status = httpStatus.HTTP_500_INTERNAL_SERVER_ERROR; | ||
return DATABASE_WRITE_FAILED; | ||
} | ||
|
||
return responseBuilder("success", { | ||
message: "Join request created successfully!", | ||
data: null, | ||
}); | ||
}, | ||
{ | ||
body: t.Object({ | ||
school: t.String({ | ||
minLength: 1, | ||
description: "The name of the school to join", | ||
}), | ||
class: t.String({ | ||
minLength: 1, | ||
description: "The name of the class to join", | ||
}), | ||
}), | ||
}, | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import Elysia from "elysia"; | ||
import { createJoinRequest } from "./create"; | ||
|
||
export const moderationRouter = new Elysia({ prefix: "/mod" }).use( | ||
createJoinRequest, | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
# Database Utilities | ||
|
||
This is a collection of complexer database operations. | ||
Only put your queries here if it is simpler to read and understand it as a function call instead of the direct query builder. | ||
|
||
## Rules | ||
|
||
- Always use the query builder to build your queries! | ||
- Always document your function with a js-doc comment! | ||
- The comment must specify if the database will be read or written to | ||
- The comment must specify if an error might be thrown | ||
- If the query fails, throw an error! | ||
- You may want to use the `promiseResult` utility to abstract errors into values |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import e from "@edgedb"; | ||
import { client } from "index"; | ||
import { promiseResult } from "utils/errors"; | ||
|
||
/** | ||
* Get the amount of members in a class | ||
* This only gives the accepted members - requests are ignored | ||
* It doesn't modify the database but reads from it | ||
* @throws Error if the database query fails | ||
*/ | ||
export async function getAmountOfMembersOfClass(props: { | ||
className: string; | ||
schoolName: string; | ||
}) { | ||
const selectClassMembersQuery = e.select(e.User, (u) => { | ||
const classNameMatches = e.op( | ||
u["<students[is Class]"].name, | ||
"=", | ||
props.className, | ||
); | ||
const schoolNameMatches = e.op( | ||
u["<students[is Class]"].school.name, | ||
"=", | ||
props.schoolName, | ||
); | ||
|
||
return { | ||
filter: e.op(classNameMatches, "and", schoolNameMatches), | ||
}; | ||
}); | ||
const countClassMembersQuery = e.count(selectClassMembersQuery); | ||
const result = await promiseResult(() => countClassMembersQuery.run(client)); | ||
|
||
if (result.status === "error") { | ||
throw new Error("Failed to get amount of class members"); | ||
} | ||
|
||
return result.data; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import e from "@edgedb"; | ||
import { client } from "index"; | ||
import { promiseResult } from "utils/errors"; | ||
|
||
interface DoesReqAlreadyExistProps { | ||
username: string; | ||
class: string; | ||
school: string; | ||
} | ||
|
||
/** | ||
* Check if a join-request already exists for a given user, class and school | ||
* It doesn't modify the database but reads from it | ||
* @throws Error if the database query fails | ||
* @readonly | ||
*/ | ||
export async function doesRequestAlreadyExist(props: DoesReqAlreadyExistProps) { | ||
const selectRequestQuery = e.select(e.JoinRequest, (jr) => { | ||
const userMatches = e.op(jr.user.username, "=", props.username); | ||
const classMatches = e.op(jr.wantsToJoin.name, "=", props.class); | ||
const schoolMatches = e.op(jr.wantsToJoin.school.name, "=", props.school); | ||
|
||
return { | ||
filter: e.op( | ||
userMatches, | ||
"and", | ||
e.op(classMatches, "and", schoolMatches), | ||
), | ||
}; | ||
}); | ||
const countRequestQuery = e.count(selectRequestQuery); | ||
const result = await promiseResult(() => countRequestQuery.run(client)); | ||
|
||
if (result.status === "error") { | ||
throw new Error("Failed to check if request already exists"); | ||
} | ||
|
||
return result.data > 0; | ||
} |
Oops, something went wrong.