;
8 | }
9 |
10 | export function Infofield({ childrenProps, label, ...props }: Props) {
11 | return (
12 |
13 | {label}:
14 |
15 | {props.children}
16 |
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/apps/api/prisma/migrations/20221224115442_persistent_tones/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateEnum
2 | CREATE TYPE "ActiveToneType" AS ENUM ('LEO', 'EMS_FD', 'SHARED');
3 |
4 | -- CreateTable
5 | CREATE TABLE "ActiveTone" (
6 | "id" TEXT NOT NULL,
7 | "type" "ActiveToneType" NOT NULL,
8 | "description" TEXT,
9 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
10 | "createdById" TEXT NOT NULL,
11 |
12 | CONSTRAINT "ActiveTone_pkey" PRIMARY KEY ("id")
13 | );
14 |
15 | -- CreateIndex
16 | CREATE UNIQUE INDEX "ActiveTone_type_key" ON "ActiveTone"("type");
17 |
18 | -- AddForeignKey
19 | ALTER TABLE "ActiveTone" ADD CONSTRAINT "ActiveTone_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
20 |
--------------------------------------------------------------------------------
/apps/api/src/lib/auth/setUserPreferencesCookies.ts:
--------------------------------------------------------------------------------
1 | import type { Res } from "@tsed/common";
2 | import { setCookie } from "utils/set-cookie";
3 |
4 | interface Options {
5 | res: Res;
6 | locale: string | null;
7 | isDarkTheme: boolean;
8 | }
9 |
10 | export function setUserPreferencesCookies(options: Options) {
11 | const ONE_YEAR_MS = 365 * 24 * 60 * 60 * 1000;
12 | setCookie({
13 | name: "sn_locale",
14 | res: options.res,
15 | value: options.locale ?? "",
16 | expires: options.locale ? ONE_YEAR_MS : 0,
17 | httpOnly: false,
18 | });
19 | setCookie({
20 | name: "sn_isDarkTheme",
21 | res: options.res,
22 | value: String(options.isDarkTheme),
23 | expires: ONE_YEAR_MS,
24 | httpOnly: false,
25 | });
26 | }
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .next
3 | dist
4 | .env
5 |
6 | # uploaded images
7 | apps/api/public/**/*.png
8 | apps/api/public/**/*.svg
9 | apps/api/public/**/*.jpg
10 | apps/api/public/**/*.jpeg
11 | apps/api/public/**/*.gif
12 | apps/api/public/**/*.webp
13 |
14 | # custom sounds & custom favicon
15 | apps/client/public/favicon.png
16 | apps/client/public/sounds/*.mp3
17 |
18 | # eslint
19 | .eslintcache
20 |
21 | # typescript
22 | tsconfig.tsbuildinfo
23 |
24 | # misc
25 | .husky/_
26 | .turbo
27 | apps/**/coverage
28 | packages/**/coverage
29 | import-officers.mjs
30 | sample-data.json
31 | .data
32 | .dev-data
33 | # Sentry
34 | .sentryclirc
35 |
36 | # storybook
37 | storybook-static
38 | build-storybook.log
39 |
40 | # Prisma generated types
41 | **/prisma/index.ts
--------------------------------------------------------------------------------
/apps/client/src/pages/api-docs.tsx:
--------------------------------------------------------------------------------
1 | import { Loader } from "@snailycad/ui";
2 | import { getAPIUrl } from "@snailycad/utils/api-url";
3 | import type { GetServerSideProps } from "next";
4 |
5 | export default function ApiDocs() {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 | );
13 | }
14 |
15 | export const getServerSideProps: GetServerSideProps = async () => {
16 | const apiURL = getAPIUrl();
17 | const destination = apiURL.replace("/v1", "/api-docs");
18 |
19 | return {
20 | redirect: {
21 | destination,
22 | permanent: true,
23 | },
24 | };
25 | };
26 |
--------------------------------------------------------------------------------
/packages/ui/src/components/stories/fields/switch-field.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from "@storybook/react";
2 | import { SwitchField } from "../../fields/switch-field";
3 |
4 | const meta = {
5 | title: "Inputs/SwitchField",
6 | component: SwitchField,
7 | tags: ["autodocs"],
8 | } satisfies Meta;
9 |
10 | export default meta;
11 | type Story = StoryObj;
12 |
13 | export const Default: Story = {
14 | args: {
15 | children: "Dark Mode",
16 | },
17 | };
18 |
19 | export const Disabled: Story = {
20 | args: {
21 | children: "Dark Mode",
22 | isDisabled: true,
23 | },
24 | };
25 |
26 | export const HiddenLabel: Story = {
27 | args: {
28 | "aria-label": "Add helpful label here",
29 | },
30 | };
31 |
--------------------------------------------------------------------------------
/apps/api/src/utils/jwt.ts:
--------------------------------------------------------------------------------
1 | import process from "node:process";
2 | import { sign, verify } from "jsonwebtoken";
3 |
4 | export function signJWT(value: any, expiresInSeconds: number | string) {
5 | const secret = process.env.JWT_SECRET;
6 |
7 | if (!secret) {
8 | throw new Error("No JWT_SECRET env var was found");
9 | }
10 |
11 | return sign(value, secret, { expiresIn: expiresInSeconds });
12 | }
13 |
14 | export function verifyJWT(value: string) {
15 | const secret = process.env.JWT_SECRET;
16 |
17 | if (!secret) {
18 | throw new Error("No JWT_SECRET env var was found");
19 | }
20 |
21 | try {
22 | return verify(value, secret) as { userId: string; sessionId: string; exp: number; iat: number };
23 | } catch {
24 | return null;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/apps/api/prisma/migrations/20220526080330_/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterEnum
2 | ALTER TYPE "ValueType" ADD VALUE 'CALL_TYPE';
3 |
4 | -- AlterTable
5 | ALTER TABLE "Call911" ADD COLUMN "typeId" TEXT;
6 |
7 | -- CreateTable
8 | CREATE TABLE "CallTypeValue" (
9 | "id" TEXT NOT NULL,
10 | "priority" INTEGER,
11 | "valueId" TEXT NOT NULL,
12 |
13 | CONSTRAINT "CallTypeValue_pkey" PRIMARY KEY ("id")
14 | );
15 |
16 | -- AddForeignKey
17 | ALTER TABLE "CallTypeValue" ADD CONSTRAINT "CallTypeValue_valueId_fkey" FOREIGN KEY ("valueId") REFERENCES "Value"("id") ON DELETE CASCADE ON UPDATE CASCADE;
18 |
19 | -- AddForeignKey
20 | ALTER TABLE "Call911" ADD CONSTRAINT "Call911_typeId_fkey" FOREIGN KEY ("typeId") REFERENCES "CallTypeValue"("id") ON DELETE SET NULL ON UPDATE CASCADE;
21 |
--------------------------------------------------------------------------------
/apps/client/locales/zh-CN/bleeter.json:
--------------------------------------------------------------------------------
1 | {
2 | "Bleeter": {
3 | "bleeter": "Bleeter",
4 | "noPosts": "目前还没有 Bleeter 帖子。请稍后再查看。",
5 | "createBleet": "创建 Bleet",
6 | "editBleet": "编辑 Bleet",
7 | "deleteBleet": "删除 Bleet",
8 | "viewBleet": "查看 Bleet",
9 | "headerImage": "标题图像",
10 | "bleetTitle": "Bleet 标题",
11 | "bleetBody": "Bleet 正文",
12 | "followers": "关注者",
13 | "following": "正在关注",
14 | "follow": "关注",
15 | "unfollow": "取消关注",
16 | "editProfile": "编辑个人资料",
17 | "posts": "帖子",
18 | "myProfile": "我的个人资料",
19 | "save": "保存",
20 | "getStarted": "开始使用",
21 | "unVerifyProfile": "取消验证个人资料",
22 | "verifyProfile": "验证个人资料",
23 | "alert_deleteBleet": "您确定要删除 \"{title}\" 吗?此操作无法撤消。"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/apps/client/src/hooks/leo/use-get-user-officers.ts:
--------------------------------------------------------------------------------
1 | import type { GetMyOfficersData } from "@snailycad/types/api";
2 | import { useQuery } from "@tanstack/react-query";
3 | import useFetch from "lib/useFetch";
4 |
5 | export function useUserOfficers(options?: { enabled?: boolean }) {
6 | const { execute } = useFetch();
7 |
8 | const { data, isLoading } = useQuery({
9 | refetchOnWindowFocus: false,
10 | ...(options ?? {}),
11 | queryKey: ["/leo"],
12 | queryFn: async () => {
13 | const { json } = await execute({ path: "/leo" });
14 |
15 | if (Array.isArray(json.officers)) {
16 | return json.officers;
17 | }
18 |
19 | return [];
20 | },
21 | });
22 |
23 | return { userOfficers: data ?? [], isLoading };
24 | }
25 |
--------------------------------------------------------------------------------
/apps/api/prisma/migrations/20230718154053_weapon_flags/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterEnum
2 | ALTER TYPE "ValueType" ADD VALUE 'WEAPON_FLAG';
3 |
4 | -- CreateTable
5 | CREATE TABLE "_weaponFlags" (
6 | "A" TEXT NOT NULL,
7 | "B" TEXT NOT NULL
8 | );
9 |
10 | -- CreateIndex
11 | CREATE UNIQUE INDEX "_weaponFlags_AB_unique" ON "_weaponFlags"("A", "B");
12 |
13 | -- CreateIndex
14 | CREATE INDEX "_weaponFlags_B_index" ON "_weaponFlags"("B");
15 |
16 | -- AddForeignKey
17 | ALTER TABLE "_weaponFlags" ADD CONSTRAINT "_weaponFlags_A_fkey" FOREIGN KEY ("A") REFERENCES "Value"("id") ON DELETE CASCADE ON UPDATE CASCADE;
18 |
19 | -- AddForeignKey
20 | ALTER TABLE "_weaponFlags" ADD CONSTRAINT "_weaponFlags_B_fkey" FOREIGN KEY ("B") REFERENCES "Weapon"("id") ON DELETE CASCADE ON UPDATE CASCADE;
21 |
--------------------------------------------------------------------------------
/apps/api/src/lib/discord/utils.ts:
--------------------------------------------------------------------------------
1 | import { prisma } from "lib/data/prisma";
2 |
3 | export function encode(obj: Record) {
4 | let string = "";
5 |
6 | for (const [key, value] of Object.entries(obj)) {
7 | if (!value) continue;
8 | string += `&${encodeURIComponent(key)}=${encodeURIComponent(`${value}`)}`;
9 | }
10 |
11 | return string.substring(1);
12 | }
13 |
14 | export async function isDiscordIdInUse(discordId: string, userId: string) {
15 | const existing = await prisma.user.findFirst({
16 | where: {
17 | discordId,
18 | },
19 | });
20 |
21 | return existing && userId !== existing.id;
22 | }
23 |
24 | export function parseDiscordGuildIds(guildId: string) {
25 | const guildIds = guildId.split(",");
26 | return guildIds;
27 | }
28 |
--------------------------------------------------------------------------------
/apps/api/src/lib/images/get-image-webp-path.ts:
--------------------------------------------------------------------------------
1 | import type { Buffer } from "node:buffer";
2 | import process from "node:process";
3 | import sharp from "sharp";
4 | import { randomUUID } from "node:crypto";
5 |
6 | export interface RawImageToWebPOptions {
7 | buffer: Buffer;
8 | id?: string;
9 | pathType: "cad" | "citizens" | "users" | "bleeter" | "units" | "values" | "pets";
10 | }
11 |
12 | export async function getImageWebPPath(options: RawImageToWebPOptions) {
13 | const sharpImage = sharp(options.buffer).webp();
14 | const buffer = await sharpImage.toBuffer();
15 |
16 | const id = options.id ?? randomUUID();
17 | const fileName = `${id}.webp`;
18 | const path = `${process.cwd()}/public/${options.pathType}/${fileName}`;
19 |
20 | return { buffer, fileName, path };
21 | }
22 |
--------------------------------------------------------------------------------
/packages/config/src/index.ts:
--------------------------------------------------------------------------------
1 | export enum Cookie {
2 | AccessToken = "snaily-cad-session",
3 | RefreshToken = "snaily-cad-refresh-token",
4 | }
5 |
6 | export const API_TOKEN_HEADER = "snaily-cad-api-token" as const;
7 |
8 | /** the header which is used by a user to connect to the API with their token (Not JWT) */
9 | export const USER_API_TOKEN_HEADER = "snaily-cad-user-api-token" as const;
10 |
11 | export type AllowedFileExtension = (typeof allowedFileExtensions)[number];
12 | export const allowedFileExtensions = [
13 | "image/png",
14 | "image/gif",
15 | "image/jpeg",
16 | "image/jpg",
17 | "image/webp",
18 | ] as const;
19 | export const IMAGES_REGEX = /https:\/\/(i.imgur.com|cdn.discordapp.com)\/.+/gi;
20 |
21 | export * from "./socket-events";
22 | export * from "./routes";
23 |
--------------------------------------------------------------------------------
/apps/api/prisma/migrations/20220102070719_/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "Call911" ADD COLUMN "ended" BOOLEAN DEFAULT false;
3 |
4 | -- CreateTable
5 | CREATE TABLE "_Call911ToLeoIncident" (
6 | "A" TEXT NOT NULL,
7 | "B" TEXT NOT NULL
8 | );
9 |
10 | -- CreateIndex
11 | CREATE UNIQUE INDEX "_Call911ToLeoIncident_AB_unique" ON "_Call911ToLeoIncident"("A", "B");
12 |
13 | -- CreateIndex
14 | CREATE INDEX "_Call911ToLeoIncident_B_index" ON "_Call911ToLeoIncident"("B");
15 |
16 | -- AddForeignKey
17 | ALTER TABLE "_Call911ToLeoIncident" ADD FOREIGN KEY ("A") REFERENCES "Call911"("id") ON DELETE CASCADE ON UPDATE CASCADE;
18 |
19 | -- AddForeignKey
20 | ALTER TABLE "_Call911ToLeoIncident" ADD FOREIGN KEY ("B") REFERENCES "LeoIncident"("id") ON DELETE CASCADE ON UPDATE CASCADE;
21 |
--------------------------------------------------------------------------------
/apps/client/src/hooks/ems-fd/use-get-user-deputies.ts:
--------------------------------------------------------------------------------
1 | import type { GetMyDeputiesData } from "@snailycad/types/api";
2 | import { useQuery } from "@tanstack/react-query";
3 | import useFetch from "lib/useFetch";
4 |
5 | export function useGetUserDeputies(options?: { enabled?: boolean }) {
6 | const { execute } = useFetch();
7 |
8 | const { data, isLoading } = useQuery({
9 | refetchOnWindowFocus: false,
10 | ...(options ?? {}),
11 | queryKey: ["/ems-fd"],
12 | queryFn: async () => {
13 | const { json } = await execute({ path: "/ems-fd" });
14 |
15 | if (Array.isArray(json.deputies)) {
16 | return json.deputies;
17 | }
18 |
19 | return [];
20 | },
21 | });
22 |
23 | return { userDeputies: data ?? [], isLoading };
24 | }
25 |
--------------------------------------------------------------------------------
/apps/client/src/hooks/use-invalidate-query.ts:
--------------------------------------------------------------------------------
1 | import { useQueryClient } from "@tanstack/react-query";
2 |
3 | /**
4 | * this hook is used to invalidate a query by passing parts of the `queryKey`. This is useful when you
5 | * want to invalidate a query where you don't have access to the full `queryKey`
6 | */
7 | export function useInvalidateQuery(queryKeyParts: T) {
8 | const queryClient = useQueryClient();
9 |
10 | const queries = queryClient.getQueryCache().findAll();
11 | const query = queries.find((q) => queryKeyParts.every((k) => q.queryKey.includes(k)));
12 |
13 | async function invalidateQuery() {
14 | await queryClient.invalidateQueries({ queryKey: query?.queryKey });
15 | return queryKeyParts;
16 | }
17 |
18 | return { invalidateQuery };
19 | }
20 |
--------------------------------------------------------------------------------
/apps/api/src/utils/file.ts:
--------------------------------------------------------------------------------
1 | import type { PlatformMulterFile } from "@tsed/common";
2 | import { BadRequest } from "@tsed/exceptions";
3 | import { ExtendedBadRequest } from "~/exceptions/extended-bad-request";
4 |
5 | export function parseImportFile(file: PlatformMulterFile | undefined) {
6 | if (!file) {
7 | throw new ExtendedBadRequest({ file: "No file provided." });
8 | }
9 |
10 | if (file.mimetype !== "application/json") {
11 | throw new BadRequest("invalidImageType");
12 | }
13 |
14 | const rawBody = file.buffer.toString("utf8").trim();
15 | let body = null;
16 |
17 | try {
18 | body = JSON.parse(rawBody);
19 | } catch {
20 | body = null;
21 | }
22 |
23 | if (!body) {
24 | throw new BadRequest("couldNotParseBody");
25 | }
26 |
27 | return body;
28 | }
29 |
--------------------------------------------------------------------------------
/apps/api/prisma/migrations/20220519162327_/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE "_officerRankDepartments" (
3 | "A" TEXT NOT NULL,
4 | "B" TEXT NOT NULL
5 | );
6 |
7 | -- CreateIndex
8 | CREATE UNIQUE INDEX "_officerRankDepartments_AB_unique" ON "_officerRankDepartments"("A", "B");
9 |
10 | -- CreateIndex
11 | CREATE INDEX "_officerRankDepartments_B_index" ON "_officerRankDepartments"("B");
12 |
13 | -- AddForeignKey
14 | ALTER TABLE "_officerRankDepartments" ADD CONSTRAINT "_officerRankDepartments_A_fkey" FOREIGN KEY ("A") REFERENCES "DepartmentValue"("id") ON DELETE CASCADE ON UPDATE CASCADE;
15 |
16 | -- AddForeignKey
17 | ALTER TABLE "_officerRankDepartments" ADD CONSTRAINT "_officerRankDepartments_B_fkey" FOREIGN KEY ("B") REFERENCES "Value"("id") ON DELETE CASCADE ON UPDATE CASCADE;
18 |
--------------------------------------------------------------------------------
/apps/api/src/lib/discord/config.ts:
--------------------------------------------------------------------------------
1 | import process from "node:process";
2 | import { REST } from "@discordjs/rest";
3 |
4 | export const DISCORD_API_VERSION = "10" as const;
5 | export const DISCORD_API_URL = `https://discord.com/api/v${DISCORD_API_VERSION}`;
6 | export const GUILD_ID = process.env.DISCORD_SERVER_ID;
7 | export const BOT_TOKEN = process.env.DISCORD_BOT_TOKEN;
8 |
9 | let cacheREST: REST | undefined;
10 | export function getRest(): REST {
11 | if (!BOT_TOKEN || BOT_TOKEN === "undefined") {
12 | throw new Error("mustSetBotTokenGuildId");
13 | }
14 |
15 | cacheREST ??= new REST({ version: DISCORD_API_VERSION }).setToken(BOT_TOKEN);
16 |
17 | if (process.env.NODE_ENV === "development") {
18 | cacheREST.on("restDebug", console.info);
19 | }
20 |
21 | return cacheREST;
22 | }
23 |
--------------------------------------------------------------------------------
/apps/api/src/utils/generate-string.ts:
--------------------------------------------------------------------------------
1 | import { customAlphabet } from "nanoid";
2 |
3 | interface Options {
4 | extraChars?: string;
5 | type: "letters-only" | "numbers-only" | "all";
6 | }
7 |
8 | export const NUMBERS = "0123456789";
9 | export const LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
10 |
11 | export function generateString(length: number, options?: Options) {
12 | const { type = "all", extraChars = "" } = options ?? {};
13 | const alphabet = [];
14 |
15 | if (type === "numbers-only") {
16 | alphabet.push(...NUMBERS);
17 | } else if (type === "letters-only") {
18 | alphabet.push(...LETTERS);
19 | } else {
20 | alphabet.push(...NUMBERS, ...LETTERS);
21 | }
22 |
23 | const generate = customAlphabet([...alphabet, extraChars].join(""));
24 | return generate(length);
25 | }
26 |
--------------------------------------------------------------------------------
/apps/client/src/state/ems-fd-state.ts:
--------------------------------------------------------------------------------
1 | import type { CombinedEmsFdUnit, EmsFdDeputy } from "@snailycad/types";
2 | import { shallow } from "zustand/shallow";
3 | import { createWithEqualityFn } from "zustand/traditional";
4 |
5 | export type ActiveDeputy = EmsFdDeputy | CombinedEmsFdUnit;
6 |
7 | interface EmsFdState {
8 | activeDeputy: ActiveDeputy | null;
9 | setActiveDeputy(deputy: ActiveDeputy | null): void;
10 |
11 | deputies: EmsFdDeputy[];
12 | setDeputies(deputies: EmsFdDeputy[]): void;
13 | }
14 |
15 | export const useEmsFdState = createWithEqualityFn()(
16 | (set) => ({
17 | activeDeputy: null,
18 | setActiveDeputy: (deputy) => set({ activeDeputy: deputy }),
19 |
20 | deputies: [],
21 | setDeputies: (deputies) => set({ deputies }),
22 | }),
23 | shallow,
24 | );
25 |
--------------------------------------------------------------------------------
/packages/utils/src/editor/slate-data-to-string.ts:
--------------------------------------------------------------------------------
1 | import { Editor, Element as SlateElement, type Descendant } from "slate";
2 |
3 | export function slateDataToString(data: Descendant[] | null) {
4 | const string: string[] = [];
5 | if (!data) return null;
6 |
7 | for (const item of data) {
8 | if (Editor.isEditor(item)) continue;
9 |
10 | if (SlateElement.isElement(item) && item.type === "bulleted-list") {
11 | const children = item.children?.flatMap((c) => c.children).map((v) => v?.text) ?? [];
12 |
13 | string.push(children.join(" "));
14 | continue;
15 | }
16 |
17 | if (SlateElement.isElement(item)) {
18 | item.children?.forEach((child) => {
19 | string.push(child.text.trim());
20 | });
21 | }
22 | }
23 |
24 | return string.join(" ");
25 | }
26 |
--------------------------------------------------------------------------------
/apps/api/prisma/migrations/20220430073214_/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateEnum
2 | CREATE TYPE "DiscordWebhookType" AS ENUM ('CALL_911', 'PANIC_BUTTON', 'UNIT_STATUS', 'BOLO');
3 |
4 | -- CreateTable
5 | CREATE TABLE "DiscordWebhook" (
6 | "id" TEXT NOT NULL,
7 | "type" "DiscordWebhookType" NOT NULL,
8 | "webhookId" TEXT,
9 | "channelId" TEXT NOT NULL,
10 | "extraMessage" TEXT,
11 | "miscCadSettingsId" TEXT,
12 |
13 | CONSTRAINT "DiscordWebhook_pkey" PRIMARY KEY ("id")
14 | );
15 |
16 | -- CreateIndex
17 | CREATE UNIQUE INDEX "DiscordWebhook_type_key" ON "DiscordWebhook"("type");
18 |
19 | -- AddForeignKey
20 | ALTER TABLE "DiscordWebhook" ADD CONSTRAINT "DiscordWebhook_miscCadSettingsId_fkey" FOREIGN KEY ("miscCadSettingsId") REFERENCES "MiscCadSettings"("id") ON DELETE CASCADE ON UPDATE CASCADE;
21 |
--------------------------------------------------------------------------------
/packages/schemas/src/admin/import/vehicles.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 | import { INSPECTION_STATUS_REGEX, TAX_STATUS_REGEX } from "../../citizen";
3 |
4 | export const VEHICLE_SCHEMA = z.object({
5 | plate: z.string().min(2).max(255),
6 | userId: z.string().nullish(),
7 | modelId: z.string().min(2).max(255),
8 | ownerId: z.string().min(2).max(255),
9 | registrationStatusId: z.string().min(2).max(255),
10 | insuranceStatus: z.string().max(255).nullish(),
11 | color: z.string().min(2).max(255),
12 | reportedStolen: z.boolean().nullish(),
13 | taxStatus: z.string().regex(TAX_STATUS_REGEX).nullish(),
14 | inspectionStatus: z.string().regex(INSPECTION_STATUS_REGEX).nullish(),
15 | flags: z.array(z.string()).nullish(),
16 | });
17 |
18 | export const VEHICLE_SCHEMA_ARR = z.array(VEHICLE_SCHEMA).min(1);
19 |
--------------------------------------------------------------------------------
/packages/schemas/src/tow.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | export const TOW_SCHEMA = z.object({
4 | location: z.string().min(2).max(255),
5 | description: z.string().nullish(),
6 | descriptionData: z.any().nullish(),
7 | creatorId: z.string().max(255).nullish(),
8 | name: z.string().max(255).nullish(),
9 | postal: z.string().max(255).nullish(),
10 | plate: z.string().max(255).optional(),
11 | call911Id: z.string().max(255).optional(),
12 | deliveryAddressId: z.string().max(255).optional(),
13 | callCountyService: z.boolean().optional(),
14 | });
15 |
16 | export const UPDATE_TOW_SCHEMA = TOW_SCHEMA.pick({
17 | location: true,
18 | description: true,
19 | descriptionData: true,
20 | postal: true,
21 | name: true,
22 | }).extend({
23 | assignedUnitId: z.string().max(255).nullable(),
24 | });
25 |
--------------------------------------------------------------------------------
/apps/api/src/lib/leo/records/create-citizen-violations.ts:
--------------------------------------------------------------------------------
1 | import type { Feature } from "@prisma/client";
2 | import { upsertRecord } from "./upsert-record";
3 | import type { z } from "zod";
4 | import type { CREATE_TICKET_SCHEMA } from "@snailycad/schemas";
5 |
6 | interface Options {
7 | cad: { features?: Record };
8 | data: z.infer[];
9 | citizenId: string;
10 | }
11 |
12 | export async function createCitizenViolations(options: Options) {
13 | try {
14 | await Promise.all(
15 | options.data.map((violation) =>
16 | upsertRecord({
17 | data: { ...violation, citizenId: options.citizenId },
18 | recordId: null,
19 | cad: options.cad,
20 | }),
21 | ),
22 | );
23 | } catch {
24 | /* empty */
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/apps/api/prisma/migrations/20211231080948_/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE "RecordLog" (
3 | "id" TEXT NOT NULL,
4 | "citizenId" TEXT NOT NULL,
5 | "recordId" TEXT,
6 | "warrantId" TEXT,
7 |
8 | CONSTRAINT "RecordLog_pkey" PRIMARY KEY ("id")
9 | );
10 |
11 | -- AddForeignKey
12 | ALTER TABLE "RecordLog" ADD CONSTRAINT "RecordLog_citizenId_fkey" FOREIGN KEY ("citizenId") REFERENCES "Citizen"("id") ON DELETE CASCADE ON UPDATE CASCADE;
13 |
14 | -- AddForeignKey
15 | ALTER TABLE "RecordLog" ADD CONSTRAINT "RecordLog_recordId_fkey" FOREIGN KEY ("recordId") REFERENCES "Record"("id") ON DELETE SET NULL ON UPDATE CASCADE;
16 |
17 | -- AddForeignKey
18 | ALTER TABLE "RecordLog" ADD CONSTRAINT "RecordLog_warrantId_fkey" FOREIGN KEY ("warrantId") REFERENCES "Warrant"("id") ON DELETE SET NULL ON UPDATE CASCADE;
19 |
--------------------------------------------------------------------------------
/packages/ui/.storybook/main.ts:
--------------------------------------------------------------------------------
1 | import type { StorybookConfig } from "@storybook/react-vite";
2 | import { mergeConfig } from "vite";
3 | const config: StorybookConfig = {
4 | stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
5 | addons: [
6 | "@storybook/addon-links",
7 | "@storybook/addon-essentials",
8 | "@storybook/addon-interactions",
9 | "@storybook/addon-themes",
10 | "@storybook/addon-a11y",
11 | ],
12 | framework: {
13 | name: "@storybook/react-vite",
14 | options: {},
15 | },
16 | docs: {
17 | autodocs: "tag",
18 | },
19 | async viteFinal(config) {
20 | return mergeConfig(config, {
21 | define: { "process.env": {} },
22 | rollupOptions: {
23 | external: "@snailycad/utils",
24 | },
25 | });
26 | },
27 | };
28 | export default config;
29 |
--------------------------------------------------------------------------------
/packages/ui/src/components/stories/helpers/label.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from "@storybook/react";
2 | import { Label } from "../../label";
3 |
4 | const meta = {
5 | title: "Helpers/Label",
6 | component: Label,
7 | tags: ["autodocs"],
8 | } satisfies Meta;
9 |
10 | export default meta;
11 | type Story = StoryObj;
12 |
13 | export const Default: Story = {
14 | args: {
15 | label: "Password",
16 | labelProps: {},
17 | },
18 | };
19 |
20 | export const Optional: Story = {
21 | args: {
22 | label: "Email",
23 | labelProps: {},
24 | isOptional: true,
25 | },
26 | };
27 |
28 | export const Description: Story = {
29 | args: {
30 | label: "Email",
31 | labelProps: {},
32 | description: "We will never share your email with anyone else.",
33 | },
34 | };
35 |
--------------------------------------------------------------------------------
/apps/client/src/hooks/usePermission.ts:
--------------------------------------------------------------------------------
1 | import { Permissions, hasPermission, getPermissions } from "@snailycad/permissions";
2 | import type { User } from "@snailycad/types";
3 | import { useAuth } from "context/AuthContext";
4 |
5 | export { Permissions };
6 | export function usePermission() {
7 | const { user } = useAuth();
8 |
9 | function _hasPermission(permissionsToCheck: Permissions[], userToCheck: User | null = user) {
10 | if (!userToCheck) return false;
11 |
12 | return hasPermission({
13 | permissionsToCheck,
14 | userToCheck,
15 | });
16 | }
17 |
18 | function _getPermissions(userToCheck: User | null = user) {
19 | if (!userToCheck) return false;
20 |
21 | return getPermissions(userToCheck);
22 | }
23 |
24 | return { hasPermissions: _hasPermission, getPermissions: _getPermissions };
25 | }
26 |
--------------------------------------------------------------------------------
/apps/api/prisma/migrations/20220516165653_/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "DiscordRoles" ADD COLUMN "courthouseRolePermissions" TEXT[];
3 |
4 | -- CreateTable
5 | CREATE TABLE "_courthouseRoles" (
6 | "A" TEXT NOT NULL,
7 | "B" TEXT NOT NULL
8 | );
9 |
10 | -- CreateIndex
11 | CREATE UNIQUE INDEX "_courthouseRoles_AB_unique" ON "_courthouseRoles"("A", "B");
12 |
13 | -- CreateIndex
14 | CREATE INDEX "_courthouseRoles_B_index" ON "_courthouseRoles"("B");
15 |
16 | -- AddForeignKey
17 | ALTER TABLE "_courthouseRoles" ADD CONSTRAINT "_courthouseRoles_A_fkey" FOREIGN KEY ("A") REFERENCES "DiscordRole"("id") ON DELETE CASCADE ON UPDATE CASCADE;
18 |
19 | -- AddForeignKey
20 | ALTER TABLE "_courthouseRoles" ADD CONSTRAINT "_courthouseRoles_B_fkey" FOREIGN KEY ("B") REFERENCES "DiscordRoles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
21 |
--------------------------------------------------------------------------------
/apps/api/prisma/migrations/20220903062552_/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateEnum
2 | CREATE TYPE "ToAddDefaultPermissionsKey" AS ENUM ('MANAGE_WARRANTS_PERMISSIONS');
3 |
4 | -- CreateTable
5 | CREATE TABLE "ToAddDefaultPermissions" (
6 | "id" TEXT NOT NULL,
7 | "key" "ToAddDefaultPermissionsKey" NOT NULL,
8 | "userId" TEXT NOT NULL,
9 | "permissions" TEXT[],
10 | "addedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
11 |
12 | CONSTRAINT "ToAddDefaultPermissions_pkey" PRIMARY KEY ("id")
13 | );
14 |
15 | -- CreateIndex
16 | CREATE UNIQUE INDEX "ToAddDefaultPermissions_key_userId_key" ON "ToAddDefaultPermissions"("key", "userId");
17 |
18 | -- AddForeignKey
19 | ALTER TABLE "ToAddDefaultPermissions" ADD CONSTRAINT "ToAddDefaultPermissions_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
20 |
--------------------------------------------------------------------------------
/apps/api/prisma/migrations/20231122144146_use_updated_at_units/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "CombinedEmsFdUnit" DROP COLUMN "lastStatusChangeTimestamp",
3 | ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
4 | ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
5 |
6 | -- AlterTable
7 | ALTER TABLE "CombinedLeoUnit" DROP COLUMN "lastStatusChangeTimestamp",
8 | ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
9 | ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
10 |
11 | -- AlterTable
12 | ALTER TABLE "EmsFdDeputy" DROP COLUMN "lastStatusChangeTimestamp";
13 |
14 | -- AlterTable
15 | ALTER TABLE "MiscCadSettings" DROP COLUMN "inactivityTimeout";
16 |
17 | -- AlterTable
18 | ALTER TABLE "Officer" DROP COLUMN "lastStatusChangeTimestamp";
19 |
--------------------------------------------------------------------------------
/apps/client/src/hooks/shared/useTemporaryItem.ts:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | export type GetIdFromObjFunc = (obj: Obj) => Id;
4 | export function useTemporaryItem(
5 | data: Obj[],
6 | getIdFromObj?: GetIdFromObjFunc,
7 | ) {
8 | const [tempId, setTempId] = React.useState(null);
9 |
10 | const tempItem = React.useMemo(() => {
11 | if (!tempId) return null;
12 |
13 | const item = data.find((obj) => {
14 | const id = getIdFromObj?.(obj) ?? obj["id"];
15 | return id === tempId;
16 | });
17 |
18 | return item ?? null;
19 | // eslint-disable-next-line react-hooks/exhaustive-deps
20 | }, [tempId, data]);
21 |
22 | const state = {
23 | tempId,
24 | setTempId,
25 | };
26 |
27 | return [tempItem, state] as const;
28 | }
29 |
--------------------------------------------------------------------------------
/packages/ui/src/components/stories/status/full-date.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from "@storybook/react";
2 |
3 | import { FullDate } from "../../full-date";
4 |
5 | const meta = {
6 | title: "Status/FullDate",
7 | component: FullDate,
8 | tags: ["autodocs"],
9 | } satisfies Meta;
10 |
11 | export default meta;
12 | type Story = StoryObj;
13 |
14 | export const Default: Story = {
15 | args: {
16 | children: new Date("2022-03-20 12:00:00").getTime(),
17 | },
18 | };
19 |
20 | export const Relative: Story = {
21 | args: {
22 | children: new Date("2022-03-20 12:00:00").getTime(),
23 | relative: true,
24 | },
25 | };
26 |
27 | export const OnlyShowTheDate: Story = {
28 | args: {
29 | children: new Date("2000-03-20 12:00:00").getTime(),
30 | onlyDate: true,
31 | },
32 | };
33 |
--------------------------------------------------------------------------------
/apps/api/prisma/migrations/20220221182451_/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterEnum
2 | ALTER TYPE "Feature" ADD VALUE 'ACTIVE_INCIDENTS';
3 |
4 | -- DropForeignKey
5 | ALTER TABLE "LeoIncident" DROP CONSTRAINT "LeoIncident_creatorId_fkey";
6 |
7 | -- AlterTable
8 | ALTER TABLE "LeoIncident" ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT false,
9 | ALTER COLUMN "creatorId" DROP NOT NULL;
10 |
11 | -- AlterTable
12 | ALTER TABLE "Officer" ADD COLUMN "activeIncidentId" TEXT;
13 |
14 | -- AddForeignKey
15 | ALTER TABLE "Officer" ADD CONSTRAINT "Officer_activeIncidentId_fkey" FOREIGN KEY ("activeIncidentId") REFERENCES "LeoIncident"("id") ON DELETE SET NULL ON UPDATE CASCADE;
16 |
17 | -- AddForeignKey
18 | ALTER TABLE "LeoIncident" ADD CONSTRAINT "LeoIncident_creatorId_fkey" FOREIGN KEY ("creatorId") REFERENCES "Officer"("id") ON DELETE SET NULL ON UPDATE CASCADE;
19 |
--------------------------------------------------------------------------------
/apps/api/src/middlewares/active-officer.ts:
--------------------------------------------------------------------------------
1 | import { Context, Middleware, Req, type MiddlewareMethods, Res, Next } from "@tsed/common";
2 | import { Unauthorized } from "@tsed/exceptions";
3 | import { getSessionUser } from "lib/auth/getSessionUser";
4 | import { getActiveOfficer } from "lib/leo/activeOfficer";
5 |
6 | @Middleware()
7 | export class ActiveOfficer implements MiddlewareMethods {
8 | async use(@Req() req: Req, @Res() res: Res, @Context() ctx: Context, @Next() next: Next) {
9 | const user = await getSessionUser({ req, res }).catch(() => null);
10 | if (!user && !req.originalUrl.includes("/v1/records")) {
11 | throw new Unauthorized("Unauthorized");
12 | }
13 |
14 | const officer = user && (await getActiveOfficer({ req, user, ctx }));
15 |
16 | ctx.set("activeOfficer", officer);
17 |
18 | next();
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/apps/client/src/components/editor/elements/leaf.tsx:
--------------------------------------------------------------------------------
1 | import type { RenderLeafProps } from "slate-react";
2 |
3 | export function EditorLeaf({ attributes, children, leaf }: RenderLeafProps) {
4 | const style = {
5 | color: leaf["text-color"],
6 | backgroundColor: leaf["background-color"],
7 | };
8 |
9 | const elementProps = {
10 | ...attributes,
11 | style,
12 | };
13 |
14 | if (leaf.bold) {
15 | children = {children};
16 | }
17 |
18 | if (leaf.italic) {
19 | children = {children};
20 | }
21 |
22 | if (leaf.underline) {
23 | children = {children};
24 | }
25 |
26 | if (leaf.strikethrough) {
27 | children = {children};
28 | }
29 |
30 | return {children};
31 | }
32 |
--------------------------------------------------------------------------------
/apps/api/prisma/migrations/20230128075737_custom_business_roles/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE "_businessValueBusinessRoles" (
3 | "A" TEXT NOT NULL,
4 | "B" TEXT NOT NULL
5 | );
6 |
7 | -- CreateIndex
8 | CREATE UNIQUE INDEX "_businessValueBusinessRoles_AB_unique" ON "_businessValueBusinessRoles"("A", "B");
9 |
10 | -- CreateIndex
11 | CREATE INDEX "_businessValueBusinessRoles_B_index" ON "_businessValueBusinessRoles"("B");
12 |
13 | -- AddForeignKey
14 | ALTER TABLE "_businessValueBusinessRoles" ADD CONSTRAINT "_businessValueBusinessRoles_A_fkey" FOREIGN KEY ("A") REFERENCES "Business"("id") ON DELETE CASCADE ON UPDATE CASCADE;
15 |
16 | -- AddForeignKey
17 | ALTER TABLE "_businessValueBusinessRoles" ADD CONSTRAINT "_businessValueBusinessRoles_B_fkey" FOREIGN KEY ("B") REFERENCES "EmployeeValue"("id") ON DELETE CASCADE ON UPDATE CASCADE;
18 |
--------------------------------------------------------------------------------
/apps/client/src/state/leo-state.ts:
--------------------------------------------------------------------------------
1 | import type { AssignedWarrantOfficer, BaseCitizen, Warrant } from "@snailycad/types";
2 | import type { GetActiveOfficerData } from "@snailycad/types/api";
3 | import { shallow } from "zustand/shallow";
4 | import { createWithEqualityFn } from "zustand/traditional";
5 |
6 | export type ActiveOfficer = GetActiveOfficerData;
7 |
8 | export interface ActiveWarrant extends Warrant {
9 | citizen: BaseCitizen;
10 | assignedOfficers: AssignedWarrantOfficer[];
11 | }
12 |
13 | interface LeoState {
14 | activeOfficer: ActiveOfficer | null;
15 | setActiveOfficer(officer: ActiveOfficer | null): void;
16 | }
17 |
18 | export const useLeoState = createWithEqualityFn()(
19 | (set) => ({
20 | activeOfficer: null,
21 | setActiveOfficer: (officer) => set({ activeOfficer: officer }),
22 | }),
23 | shallow,
24 | );
25 |
--------------------------------------------------------------------------------
/apps/api/prisma/migrations/20230120161140_address_flag_values/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterEnum
2 | ALTER TYPE "ValueType" ADD VALUE 'ADDRESS_FLAG';
3 |
4 | -- CreateTable
5 | CREATE TABLE "_citizenAddressFlags" (
6 | "A" TEXT NOT NULL,
7 | "B" TEXT NOT NULL
8 | );
9 |
10 | -- CreateIndex
11 | CREATE UNIQUE INDEX "_citizenAddressFlags_AB_unique" ON "_citizenAddressFlags"("A", "B");
12 |
13 | -- CreateIndex
14 | CREATE INDEX "_citizenAddressFlags_B_index" ON "_citizenAddressFlags"("B");
15 |
16 | -- AddForeignKey
17 | ALTER TABLE "_citizenAddressFlags" ADD CONSTRAINT "_citizenAddressFlags_A_fkey" FOREIGN KEY ("A") REFERENCES "Citizen"("id") ON DELETE CASCADE ON UPDATE CASCADE;
18 |
19 | -- AddForeignKey
20 | ALTER TABLE "_citizenAddressFlags" ADD CONSTRAINT "_citizenAddressFlags_B_fkey" FOREIGN KEY ("B") REFERENCES "Value"("id") ON DELETE CASCADE ON UPDATE CASCADE;
21 |
--------------------------------------------------------------------------------
/apps/client/src/state/search/name-search-state.ts:
--------------------------------------------------------------------------------
1 | import type { PostLeoSearchCitizenData } from "@snailycad/types/api";
2 | import { shallow } from "zustand/shallow";
3 | import { createWithEqualityFn } from "zustand/traditional";
4 |
5 | export type NameSearchResult = PostLeoSearchCitizenData[number];
6 |
7 | interface NameSearchState {
8 | results: NameSearchResult[] | null | boolean;
9 | setResults(v: NameSearchResult[] | null | boolean): void;
10 |
11 | currentResult: NameSearchResult | null;
12 | setCurrentResult(v: NameSearchResult | null): void;
13 | }
14 |
15 | export const useNameSearch = createWithEqualityFn()(
16 | (set) => ({
17 | results: null,
18 | setResults: (v) => set({ results: v }),
19 |
20 | currentResult: null,
21 | setCurrentResult: (v) => set({ currentResult: v }),
22 | }),
23 | shallow,
24 | );
25 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:20-slim AS base
2 |
3 | WORKDIR /snailycad
4 |
5 | # Install pnpm globally and set config in one layer
6 | RUN npm install -g pnpm && pnpm config set httpTimeout 1200000
7 |
8 | # Copy the rest of the source code
9 | COPY . ./
10 |
11 | FROM base AS deps
12 |
13 | RUN pnpm install --frozen-lockfile
14 |
15 | FROM deps AS build
16 |
17 | ENV NODE_ENV="production"
18 |
19 | # Build all packages (this will also build the API and Client)
20 | RUN pnpm turbo run build --filter="{packages/*}"
21 |
22 |
23 | FROM build AS api
24 | ENV NODE_ENV="production"
25 | WORKDIR /snailycad/apps/api
26 | RUN pnpm run build
27 | CMD ["pnpm", "start"]
28 |
29 | FROM build AS client
30 | ENV NODE_ENV="production"
31 | WORKDIR /snailycad/apps/client
32 | RUN rm -rf /snailycad/apps/client/.next
33 | RUN pnpm create-images-domain
34 | RUN pnpm run build
35 | CMD ["pnpm", "start"]
--------------------------------------------------------------------------------