Skip to content

Commit

Permalink
🎉 able to ban/unban users
Browse files Browse the repository at this point in the history
  • Loading branch information
casperiv0 committed Oct 10, 2021
1 parent 734a764 commit d3e69ff
Show file tree
Hide file tree
Showing 20 changed files with 418 additions and 34 deletions.
2 changes: 2 additions & 0 deletions packages/api/prisma/migrations/20211010084624_/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" ALTER COLUMN "banReason" SET DATA TYPE TEXT;
2 changes: 1 addition & 1 deletion packages/api/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ model User {
isDispatch Boolean @default(false)
isTow Boolean @default(false)
banned Boolean @default(false)
banReason Boolean?
banReason String?
avatarUrl String? @db.Text
steamId String? @db.VarChar(255)
whitelistStatus WhitelistStatus @default(ACCEPTED)
Expand Down
69 changes: 68 additions & 1 deletion packages/api/src/controllers/admin/manage/Users.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { Rank } from ".prisma/client";
import { PathParams, BodyParams, Context } from "@tsed/common";
import { Controller } from "@tsed/di";
import { BadRequest, NotFound } from "@tsed/exceptions";
import { UseBeforeEach } from "@tsed/platform-middlewares";
import { Get } from "@tsed/schema";
import { Get, JsonRequestBody, Post } from "@tsed/schema";
import { userProperties } from "../../../lib/auth";
import { prisma } from "../../../lib/prisma";
import { IsAuth, IsAdmin } from "../../../middlewares";
import { BAN_SCHEMA, validate } from "@snailycad/schemas";

@UseBeforeEach(IsAuth, IsAdmin)
@Controller("/users")
Expand All @@ -13,4 +18,66 @@ export class ManageUsersController {

return users;
}

@Get("/:id")
async getUserById(@PathParams("id") userId: string) {
const user = await prisma.user.findUnique({ where: { id: userId }, select: userProperties });

return user;
}

@Post("/:id")
async updateUserById(@PathParams("id") userId: string, @BodyParams() body: JsonRequestBody) {
const user = await prisma.user.findUnique({ where: { id: userId } });
body;
if (!user) {
throw new NotFound("notFound");
}

return "TODO";

// return user;
}

@Post("/:id/:type")
async banUserById(
@Context() ctx: Context,
@PathParams("id") userId: string,
@PathParams("type") banType: "ban" | "unban",
@BodyParams() body: JsonRequestBody,
) {
if (!["ban", "unban"].includes(banType)) {
throw new NotFound("notFound");
}

if (banType === "ban") {
const error = validate(BAN_SCHEMA, body.toJSON(), true);
if (error) {
throw new BadRequest(error);
}
}

const user = await prisma.user.findUnique({ where: { id: userId } });

if (!user) {
throw new NotFound("notFound");
}

if (user.rank === Rank.OWNER || ctx.get("user").id === user.id) {
throw new BadRequest("cannotBanSelfOrOwner");
}

const updated = await prisma.user.update({
where: {
id: user.id,
},
data: {
banReason: banType === "ban" ? body.get("reason") : null,
banned: banType === "ban",
},
select: userProperties,
});

return updated;
}
}
4 changes: 2 additions & 2 deletions packages/api/src/controllers/citizen/CitizenController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export class CitizenController {

@Post("/")
async createCitizen(@Context() ctx: Context, @BodyParams() body: JsonRequestBody) {
const error = validate(CREATE_CITIZEN_SCHEMA(true), body.toJSON(), true);
const error = validate(CREATE_CITIZEN_SCHEMA, body.toJSON(), true);

if (error) {
return new BadRequest(error);
Expand Down Expand Up @@ -113,7 +113,7 @@ export class CitizenController {
@Context() ctx: Context,
@BodyParams() body: JsonRequestBody,
) {
const error = validate(CREATE_CITIZEN_SCHEMA(true), body.toJSON(), true);
const error = validate(CREATE_CITIZEN_SCHEMA, body.toJSON(), true);
if (error) {
return new BadRequest(error);
}
Expand Down
11 changes: 10 additions & 1 deletion packages/api/src/middlewares/IsAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,16 @@ export class IsAuth implements MiddlewareMethods {
async use(@Req() req: Req, @Context() ctx: Context) {
const user = await getSessionUser(req);

const cad = await prisma.cad.findFirst();
const cad = await prisma.cad.findFirst({
select: {
name: true,
areaOfPlay: true,
maxPlateLength: true,
towWhitelisted: true,
whitelisted: true,
// features: true,
},
});

ctx.set("cad", cad);
ctx.set("user", user);
Expand Down
3 changes: 2 additions & 1 deletion packages/client/locales/en/citizen.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"address": "Address",
"dateOfBirth": "Date of Birth",
"image": "Image",
"age": "Age",
"driversLicense": "Drivers License",
"weaponLicense": "Firearms License",
"pilotLicense": "Pilot License",
Expand Down Expand Up @@ -57,4 +58,4 @@
"deleteMedicalRecord": "Delete Medical Record",
"alert_deleteMedicalRecord": "Are you sure you want to delete this medical record? This action cannot be undone."
}
}
}
5 changes: 3 additions & 2 deletions packages/client/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"off": "Off",
"error": "An unexpected error occurred",
"view": "View",
"create": "Create"
"create": "Create",
"reason": "Reason"
},
"Errors": {
"unknown": "An unexpected error occurred",
Expand All @@ -26,4 +27,4 @@
"plateAlreadyInUse": "That plate is already being used on a vehicle.",
"invalidImageType": "Only types image/png, image/jpeg, image/jpg and image/jpg are supported"
}
}
}
1 change: 1 addition & 0 deletions packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"@snailycad/config": "0.0.0",
"@snailycad/schemas": "0.0.2-dev",
"axios": "^0.22.0",
"date-fns": "^2.25.0",
"formik": "^2.2.9",
"next": "^11.1.2",
"next-intl": "^2.0.5",
Expand Down
103 changes: 103 additions & 0 deletions packages/client/src/components/admin/manage/BanArea.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { Button } from "components/Button";
import { Error } from "components/form/Error";
import { FormField } from "components/form/FormField";
import { Input } from "components/form/Input";
import { Loader } from "components/Loader";
import { useAuth } from "context/AuthContext";
import { Formik } from "formik";
import { handleValidate } from "lib/handleValidate";
import useFetch from "lib/useFetch";
import { User } from "types/prisma";
import { useTranslations } from "use-intl";
import { BAN_SCHEMA } from "@snailycad/schemas";

interface Props {
user: User;
setUser: React.Dispatch<React.SetStateAction<User | null>>;
}

export const BanArea = ({ user, setUser }: Props) => {
const common = useTranslations("Common");
const { state, execute } = useFetch();
const { user: session } = useAuth();

const formDisabled = user.rank === "OWNER" || user.id === session?.id;

async function onSubmit(values: { reason: string }) {
if (formDisabled) return;

const { json } = await execute(`/admin/manage/users/${user.id}/ban`, {
method: "POST",
data: values,
});

if (json.id) {
setUser({ ...user, ...json });
}
}

async function handleUnban() {
if (formDisabled) return;

const { json } = await execute(`/admin/manage/users/${user.id}/unban`, {
method: "POST",
});

if (json.id) {
setUser({ ...user, ...json });
}
}

const validate = handleValidate(BAN_SCHEMA);

return (
<div className="bg-gray-200 mt-10 rounded-md p-3">
<h1 className="text-2xl font-semibold">Ban area</h1>

{user.banned && user.rank !== "OWNER" ? (
<div className="mt-1">
<p>
<span className="font-semibold">Ban Reason: </span> {user.banReason}
</p>

<Button
className="flex items-center mt-2"
disabled={state === "loading"}
onClick={handleUnban}
>
{state === "loading" ? <Loader className="mr-3" /> : null}
Unban
</Button>
</div>
) : (
<Formik validate={validate} onSubmit={onSubmit} initialValues={{ reason: "" }}>
{({ handleChange, handleSubmit, values, errors }) => (
<form className="mt-3" onSubmit={handleSubmit}>
<FormField fieldId="reason" label={common("reason")}>
<Input
hasError={!!errors.reason}
className="bg-gray-100"
value={values.reason}
onChange={handleChange}
id="reason"
disabled={formDisabled}
/>
<Error>{errors.reason} </Error>
</FormField>

<Button
className="flex items-center"
type="submit"
disabled={formDisabled || state === "loading"}
variant="danger"
>
{state === "loading" ? <Loader className="border-red-300 mr-3" /> : null}
Ban User
</Button>
</form>
)}
</Formik>
)}
</div>
);
};
25 changes: 18 additions & 7 deletions packages/client/src/components/form/FormRow.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,32 @@
import { classNames } from "lib/classNames";
import type * as React from "react";

interface Props {
type Props = JSX.IntrinsicElements["div"] & {
children: React.ReactNode;
justify?: boolean;
flexLike?: boolean;
}
};

export const FormRow = ({ justify = true, flexLike = false, children }: Props) => {
export const FormRow = ({
justify = true,
flexLike = false,
children,
className = "",
...rest
}: Props) => {
const cols = Array.isArray(children)
? `grid grid-cols-1 sm:grid-cols-${children.length}`
? `grid grid-cols-1 sm:grid-cols-${children.length / 2} md:grid-cols-${children.length}`
: "grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4";

return (
<div
className={`mb-3 w-full ${flexLike ? "grid grid-cols-1 sm:flex" : cols} gap-2 ${
justify && "justify-between"
}`}
{...rest}
className={classNames(
"mb-3 w-full gap-2",
flexLike ? "grid grid-cols-1 sm:flex" : cols,
justify && "justify-between",
className,
)}
>
{children}
</div>
Expand Down
4 changes: 2 additions & 2 deletions packages/client/src/components/form/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ type Props = JSX.IntrinsicElements["input"] & {
export const Input = React.forwardRef<HTMLInputElement, Props>(({ hasError, ...rest }, ref) => (
<input
ref={ref}
{...rest}
className={`
w-full p-1.5 px-3 bg-white rounded-md border-[1.5px] border-gray-200
outline-none focus:border-gray-800
hover:border-dark-gray
disabled:cursor-not-allowed disabled:opacity-80
transition-all ${rest.className} ${hasError && "border-red-500"} `}
{...rest}
/>
));

Expand All @@ -35,7 +35,7 @@ export const PasswordInput = (props: Exclude<Props, "type">) => {
<button
type="button"
onClick={handleClick}
className="p-1 px-3 rounded-md absolute top-1/2 right-2 -translate-y-1/2 bg-gray-300"
className="p-1 px-3 rounded-md absolute top-1/2 right-1 -translate-y-1/2 bg-gray-300"
>
{type === "password" ? "show" : "hide"}
</button>
Expand Down
6 changes: 5 additions & 1 deletion packages/client/src/lib/useFetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { AxiosRequestConfig, AxiosError } from "axios";
import { handleRequest } from "./fetch";
import toast from "react-hot-toast";
import { useTranslations } from "use-intl";
import Common from "../../locales/en/common.json";

type NullableAbortController = AbortController | null;
type State = "loading" | "error";
Expand Down Expand Up @@ -33,8 +34,11 @@ export default function useFetch(
const error = response instanceof Error ? parseError(response as AxiosError) : null;

if (error) {
const hasKey = Object.keys(Common.Errors).some((e) => e === error);
const key = hasKey ? error : "unknown";

setState("error");
toast.error(t(error));
toast.error(t(key));

return {
json: {},
Expand Down
6 changes: 6 additions & 0 deletions packages/client/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,9 @@ const IMAGE_URL = "http://localhost:8080/";
export function makeImageUrl(type: "citizens" | "users", id: string) {
return `${IMAGE_URL}${type}/${id}`;
}

export function calculateAge(dateOfBirth: string | Date) {
return ((Date.now() - new Date(dateOfBirth).getTime()) / (60 * 60 * 24 * 365.25 * 1000))
.toString()
.split(".")[0];
}
Loading

0 comments on commit d3e69ff

Please sign in to comment.