Fix routing modes

This commit is contained in:
Fergal Moran
2024-01-31 19:46:44 +00:00
parent 235661fbb3
commit 5f6222ed6b
22 changed files with 258 additions and 167 deletions

View File

@@ -2,5 +2,8 @@
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UpdateDependencyToLatestVersion" enabled="true" level="WARNING" enabled_by_default="true" editorAttributes="WARNING_ATTRIBUTES">
<scope name="Project Files" level="WARNING" enabled="true" editorAttributes="WARNING_ATTRIBUTES" />
</inspection_tool>
</profile>
</component>

View File

@@ -11,6 +11,7 @@
"node_modules": true
},
"workbench.colorTheme": "poimandres-noitalics",
"editor.fontFamily": "DejaVu Sans Mono",
"sqltools.connections": [
{
"previewLimit": 50,

BIN
bun.lockb

Binary file not shown.

View File

@@ -1,5 +1,5 @@
import type { Config } from "drizzle-kit";
import { env } from "@/env.mjs";
import { env } from "@/env";
export default {
schema: "./src/server/db/schema",
@@ -7,5 +7,5 @@ export default {
driver: "pg",
dbCredentials: {
connectionString: env.DATABASE_URL,
}
},
} satisfies Config;

View File

@@ -68,7 +68,7 @@
"leaflet": "^1.9.4",
"local-ssl-proxy": "^2.0.5",
"lucide-react": "^0.314.0",
"next": "^14.0.4",
"next": "^14.1.0",
"next-auth": "^4.24.5",
"next-themes": "^0.2.1",
"postgres": "^3.4.3",

View File

@@ -1,10 +1,12 @@
import DashboardPage from "@/components/pages/dashboard-page";
import { checkAuth } from "@/lib/auth/utils";
import React from "react";
import { api } from "@/trpc/server";
const Dashboard = async () => {
await checkAuth();
return <DashboardPage />;
const { children } = await api.children.getChildren.query();
return <DashboardPage children={children} />;
};
export default Dashboard;

View File

@@ -1,24 +1,33 @@
/* eslint-disable @typescript-eslint/no-unsafe-return */
"use client";
import "leaflet/dist/leaflet.css";
import React, { useEffect, useState } from "react";
import {
MapContainer,
Marker,
Popup,
TileLayer,
Circle,
Polyline,
} from "react-leaflet";
import { usePingSocket } from "@/lib/hooks/use-ping-socket";
import React from "react";
import { MapContainer, TileLayer } from "react-leaflet";
import MapMarker from "@/components/maps/map-marker";
import { Child, type CompleteChild } from "@/server/db/schema/children";
import { Ping } from "@/server/db/schema/pings";
import { Device } from "@/server/db/schema/devices";
import { getLatestPing } from "@/lib/helpers/location/ping";
import { type CompleteChild } from "@/server/db/schema/children";
type MainMapProps = {
kids: CompleteChild[];
mode: "latest" | "route";
};
const MainMap: React.FC<MainMapProps> = ({ kids }) => {
const _renderMarker = (ping: Ping, device: Device, child: Child) => {
return (
<MapMarker
key={ping.id}
deviceId={device.id}
childName={child.name}
avatar={child.avatar}
deviceName={device.name}
latitude={ping.latitude}
longitude={ping.longitude}
timestamp={ping.timestamp}
/>
);
};
const MainMap: React.FC<MainMapProps> = ({ kids, mode }) => {
return (
<div>
<MapContainer
@@ -30,18 +39,12 @@ const MainMap: React.FC<MainMapProps> = ({ kids }) => {
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
{kids?.map((kid) =>
kid.devices?.map((device) =>
device.pings.map((ping) => (
<MapMarker
key={ping.id}
deviceId={device.id}
childName={kid.name}
avatar={kid.avatar}
deviceName={device.name}
latitude={ping.latitude}
longitude={ping.longitude}
timestamp={ping.timestamp}
/>
)),
mode === "route"
? device.pings.map(
(ping) =>
device.pings.length > 0 && _renderMarker(ping, device, kid),
)
: _renderMarker(getLatestPing(device.pings)!, device, kid),
),
)}
</MapContainer>

View File

@@ -47,7 +47,6 @@ const MapMarker: React.FC<MapMarkerProps> = ({
usePingSocket({
deviceId: deviceId,
locationUpdate: (ping) => {
console.log("MainMap", "locationUpdate", ping);
setPosition([ping.location.latitude, ping.location.longitude]);
},
});

View File

@@ -3,15 +3,23 @@ import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import { Icons } from "@/components/icons";
import React from "react";
export const MapViewTypeSelector = () => {
const [currentView, setCurrentView] = React.useState("location");
type MapViewTypeSelectorProps = {
currentView: "latest" | "route";
onChange: (mode: "latest" | "route") => void;
};
export const MapViewTypeSelector: React.FC<MapViewTypeSelectorProps> = ({
currentView,
onChange,
}) => {
return (
<ToggleGroup
type="single"
value={currentView}
onValueChange={(value) => setCurrentView(value)}
onValueChange={(value) =>
onChange(value === "latest" ? "latest" : "route")
}
>
<ToggleGroupItem value="location">
<ToggleGroupItem value="latest">
<Icons.location className="mr-1 h-4 w-4" />
Location
</ToggleGroupItem>

View File

@@ -1,10 +1,15 @@
"use client";
import React from "react";
import dynamic from "next/dynamic";
import { api } from "@/trpc/server";
import ChildrenFilter from "../children/children-filter";
import { MapViewTypeSelector } from "../maps/map-viewtype-selector";
import { CompleteChild } from "@/server/db/schema/children";
const DashboardPage = async () => {
const { children } = await api.children.getChildren.query();
type DashboardPageProps = {
children: CompleteChild[];
};
const DashboardPage: React.FC<DashboardPageProps> = ({ children }) => {
const [mode, setMode] = React.useState<"latest" | "route">("latest");
const Map = dynamic(() => import("@/components/maps/main-map"), {
ssr: false,
});
@@ -12,10 +17,13 @@ const DashboardPage = async () => {
<div>
<div className="flex flex-row justify-between">
<ChildrenFilter children={children} />
<MapViewTypeSelector />
<MapViewTypeSelector
currentView={mode}
onChange={(mode) => setMode(mode)}
/>
</div>
<div className="mt-4">
<Map kids={children} />
<Map kids={children} mode={mode} />
</div>
</div>
);

View File

@@ -2,39 +2,36 @@ import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
export const env = createEnv({
/**
* Specify your server-side environment variables schema here. This way you can ensure the app
* isn't built with invalid env vars.
*/
server: {
NODE_ENV: z.enum(["development", "test", "production"]),
},
NODE_ENV: z
.enum(["development", "test", "production"])
.default("development"),
DATABASE_URL: z.string().min(1),
/**
* Specify your client-side environment variables schema here. This way you can ensure the app
* isn't built with invalid env vars. To expose them to the client, prefix them with
* `NEXT_PUBLIC_`.
*/
NEXTAUTH_SECRET:
process.env.NODE_ENV === "production"
? z.string().min(1)
: z.string().min(1).optional(),
NEXTAUTH_URL: z.preprocess(
// This makes Vercel deployments not fail if you don't set NEXTAUTH_URL
// Since NextAuth.js automatically uses the VERCEL_URL if present.
(str) => process.env.VERCEL_URL ?? str,
// VERCEL_URL doesn't include `https` so it cant be validated as a URL
process.env.VERCEL_URL ? z.string().min(1) : z.string().url(),
),
GOOGLE_CLIENT_ID: z.string().min(1),
GOOGLE_CLIENT_SECRET: z.string().min(1),
},
client: {
// NEXT_PUBLIC_CLIENTVAR: z.string(),
// NEXT_PUBLIC_PUBLISHABLE_KEY: z.string().min(1),
},
/**
* You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g.
* middlewares) or client-side so we need to destruct manually.
*/
runtimeEnv: {
NODE_ENV: process.env.NODE_ENV,
// NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,
// If you're using Next.js < 13.4.4, you'll need to specify the runtimeEnv manually
// runtimeEnv: {
// DATABASE_URL: process.env.DATABASE_URL,
// NEXT_PUBLIC_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_PUBLISHABLE_KEY,
// },
// For Next.js >= 13.4.4, you only need to destructure client variables:
experimental__runtimeEnv: {
// NEXT_PUBLIC_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_PUBLISHABLE_KEY,
},
/**
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially
* useful for Docker builds.
*/
skipValidation: !!process.env.SKIP_ENV_VALIDATION,
/**
* Makes it so that empty strings are treated as undefined. `SOME_VAR: z.string()` and
* `SOME_VAR=''` will throw an error.
*/
emptyStringAsUndefined: true,
});

View File

@@ -1,36 +0,0 @@
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
export const env = createEnv({
server: {
NODE_ENV: z
.enum(["development", "test", "production"])
.default("development"),
DATABASE_URL: z.string().min(1),
NEXTAUTH_SECRET: process.env.NODE_ENV === "production"
? z.string().min(1)
: z.string().min(1).optional(),
NEXTAUTH_URL: z.preprocess(
// This makes Vercel deployments not fail if you don't set NEXTAUTH_URL
// Since NextAuth.js automatically uses the VERCEL_URL if present.
(str) => process.env.VERCEL_URL ?? str,
// VERCEL_URL doesn't include `https` so it cant be validated as a URL
process.env.VERCEL_URL ? z.string().min(1) : z.string().url()
),
GOOGLE_CLIENT_ID: z.string().min(1),
GOOGLE_CLIENT_SECRET: z.string().min(1),
},
client: {
// NEXT_PUBLIC_PUBLISHABLE_KEY: z.string().min(1),
},
// If you're using Next.js < 13.4.4, you'll need to specify the runtimeEnv manually
// runtimeEnv: {
// DATABASE_URL: process.env.DATABASE_URL,
// NEXT_PUBLIC_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_PUBLISHABLE_KEY,
// },
// For Next.js >= 13.4.4, you only need to destructure client variables:
experimental__runtimeEnv: {
// NEXT_PUBLIC_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_PUBLISHABLE_KEY,
},
});

View File

@@ -1,21 +1,24 @@
import { db } from "@/server/db/index";
import { and, eq } from "drizzle-orm";
import {
ChildId,
NewChildParams,
UpdateChildParams,
type ChildId,
type NewChildParams,
type UpdateChildParams,
updateChildSchema,
insertChildSchema,
children,
childIdSchema
childIdSchema,
} from "@/server/db/schema/children";
import { getUserAuth } from "@/lib/auth/utils";
export const createChild = async (child: NewChildParams) => {
const { session } = await getUserAuth();
const newChild = insertChildSchema.parse({ ...child, userId: session?.user.id! });
const newChild = insertChildSchema.parse({
...child,
userId: session?.user.id!,
});
try {
const [c] = await db.insert(children).values(newChild).returning();
const [c] = await db.insert(children).values(newChild).returning();
return { child: c };
} catch (err) {
const message = (err as Error).message ?? "Error, please try again";
@@ -27,13 +30,18 @@ export const createChild = async (child: NewChildParams) => {
export const updateChild = async (id: ChildId, child: UpdateChildParams) => {
const { session } = await getUserAuth();
const { id: childId } = childIdSchema.parse({ id });
const newChild = updateChildSchema.parse({ ...child, userId: session?.user.id! });
const newChild = updateChildSchema.parse({
...child,
userId: session?.user.id!,
});
try {
const [c] = await db
.update(children)
.set(newChild)
.where(and(eq(children.id, childId!), eq(children.userId, session?.user.id!)))
.returning();
const [c] = await db
.update(children)
.set(newChild)
.where(
and(eq(children.id, childId), eq(children.userId, session?.user.id!)),
)
.returning();
return { child: c };
} catch (err) {
const message = (err as Error).message ?? "Error, please try again";
@@ -46,8 +54,12 @@ export const deleteChild = async (id: ChildId) => {
const { session } = await getUserAuth();
const { id: childId } = childIdSchema.parse({ id });
try {
const [c] = await db.delete(children).where(and(eq(children.id, childId!), eq(children.userId, session?.user.id!)))
.returning();
const [c] = await db
.delete(children)
.where(
and(eq(children.id, childId), eq(children.userId, session?.user.id!)),
)
.returning();
return { child: c };
} catch (err) {
const message = (err as Error).message ?? "Error, please try again";
@@ -55,4 +67,3 @@ export const deleteChild = async (id: ChildId) => {
throw { error: message };
}
};

View File

@@ -1,21 +1,24 @@
import { db } from "@/server/db/index";
import { and, eq } from "drizzle-orm";
import {
DeviceId,
NewDeviceParams,
UpdateDeviceParams,
type DeviceId,
type NewDeviceParams,
type UpdateDeviceParams,
updateDeviceSchema,
insertDeviceSchema,
devices,
deviceIdSchema
deviceIdSchema,
} from "@/server/db/schema/devices";
import { getUserAuth } from "@/lib/auth/utils";
export const createDevice = async (device: NewDeviceParams) => {
const { session } = await getUserAuth();
const newDevice = insertDeviceSchema.parse({ ...device, userId: session?.user.id! });
const newDevice = insertDeviceSchema.parse({
...device,
userId: session?.user.id!,
});
try {
const [d] = await db.insert(devices).values(newDevice).returning();
const [d] = await db.insert(devices).values(newDevice).returning();
return { device: d };
} catch (err) {
const message = (err as Error).message ?? "Error, please try again";
@@ -24,16 +27,24 @@ export const createDevice = async (device: NewDeviceParams) => {
}
};
export const updateDevice = async (id: DeviceId, device: UpdateDeviceParams) => {
export const updateDevice = async (
id: DeviceId,
device: UpdateDeviceParams,
) => {
const { session } = await getUserAuth();
const { id: deviceId } = deviceIdSchema.parse({ id });
const newDevice = updateDeviceSchema.parse({ ...device, userId: session?.user.id! });
const newDevice = updateDeviceSchema.parse({
...device,
userId: session?.user.id!,
});
try {
const [d] = await db
.update(devices)
.set(newDevice)
.where(and(eq(devices.id, deviceId!), eq(devices.userId, session?.user.id!)))
.returning();
const [d] = await db
.update(devices)
.set(newDevice)
.where(
and(eq(devices.id, deviceId), eq(devices.userId, session?.user.id!)),
)
.returning();
return { device: d };
} catch (err) {
const message = (err as Error).message ?? "Error, please try again";
@@ -46,8 +57,12 @@ export const deleteDevice = async (id: DeviceId) => {
const { session } = await getUserAuth();
const { id: deviceId } = deviceIdSchema.parse({ id });
try {
const [d] = await db.delete(devices).where(and(eq(devices.id, deviceId!), eq(devices.userId, session?.user.id!)))
.returning();
const [d] = await db
.delete(devices)
.where(
and(eq(devices.id, deviceId), eq(devices.userId, session?.user.id!)),
)
.returning();
return { device: d };
} catch (err) {
const message = (err as Error).message ?? "Error, please try again";
@@ -55,4 +70,3 @@ export const deleteDevice = async (id: DeviceId) => {
throw { error: message };
}
};

View File

@@ -2,18 +2,22 @@ import { db } from "@/server/db/index";
import { eq, and } from "drizzle-orm";
import { getUserAuth } from "@/lib/auth/utils";
import { type PingId, pingIdSchema, pings } from "@/server/db/schema/pings";
import { devices } from "@/server/db/schema/devices";
export const getPings = async () => {
const { session } = await getUserAuth();
const p = await db.select({ ping: pings, device: devices }).from(pings).leftJoin(devices, eq(pings.deviceId, devices.id)).where(eq(pings.userId, session?.user.id!));
const p = await db.query.pings.findMany({
where: (pings, { eq }) => eq(pings.userId, session?.user.id!),
});
return { pings: p };
};
export const getPingById = async (id: PingId) => {
const { session } = await getUserAuth();
const { id: pingId } = pingIdSchema.parse({ id });
const [p] = await db.select().from(pings).where(and(eq(pings.id, pingId), eq(pings.userId, session?.user.id!))).leftJoin(devices, eq(pings.deviceId, devices.id));
const p = await db.query.pings.findFirst({
where: (pings, { eq, and }) =>
and(eq(pings.id, pingId), eq(pings.userId, session?.user.id!)),
});
return { ping: p };
};

View File

@@ -2,8 +2,9 @@ import { db } from "@/server/db/index";
import { DrizzleAdapter } from "@auth/drizzle-adapter";
import { DefaultSession, getServerSession, NextAuthOptions } from "next-auth";
import { redirect } from "next/navigation";
import { env } from "@/env.mjs";
import GoogleProvider from "next-auth/providers/google";
import { Adapter } from "next-auth/adapters";
import { env } from "@/env";
declare module "next-auth" {
interface Session {
@@ -24,7 +25,7 @@ export type AuthSession = {
};
export const authOptions: NextAuthOptions = {
adapter: DrizzleAdapter(db),
adapter: DrizzleAdapter(db) as Adapter,
callbacks: {
session: ({ session, user }) => {
session.user.id = user.id;

View File

@@ -1,6 +1,8 @@
import type PingModel from "@/lib/models/ping";
import { type CompletePing } from "@/server/db/schema/pings";
export const getLatestPing = (pings: PingModel[]): PingModel | undefined => {
export const getLatestPing = (
pings: CompletePing[],
): CompletePing | undefined => {
if (pings && pings.length !== 0) {
return pings.reduce((ping, current) =>
ping.timestamp > current.timestamp ? ping : current,

View File

@@ -1,11 +1,8 @@
import { db } from "@/server/db";
import { StatusCodes } from "http-status-codes";
import { type NextApiResponseServerIo } from "@/lib/models/types/next-api-response-socket";
import { type NextApiRequest } from "next";
import type LocationUpdate from "@/lib/models/location-update";
import { badRequest, notAuthorised } from "@/lib/api/responses";
import { getDeviceById } from "@/lib/api/devices/queries";
import { getChildById } from "@/lib/api/children/queries";
import { createPing } from "@/lib/api/pings/mutations";
type PingRequest = {

View File

@@ -6,10 +6,14 @@
* TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will
* need to use are documented accordingly near the end.
*/
import { initTRPC } from "@trpc/server";
import { initTRPC, TRPCError } from "@trpc/server";
import superjson from "superjson";
import { ZodError } from "zod";
import { db } from "@/server/db";
import { getServerAuthSession } from "../auth";
/**
* 1. CONTEXT
*
@@ -23,7 +27,11 @@ import { ZodError } from "zod";
* @see https://trpc.io/docs/server/context
*/
export const createTRPCContext = async (opts: { headers: Headers }) => {
const session = await getServerAuthSession();
return {
db,
session,
...opts,
};
};
@@ -72,8 +80,15 @@ export const createTRPCRouter = t.router;
*/
export const publicProcedure = t.procedure;
/** Reusable middleware that enforces users are logged in before running the procedure. */
const enforceUserIsAuthed = t.middleware(({ ctx, next }) => {
/**
* Protected (authenticated) procedure
*
* If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies
* the session is valid and guarantees `ctx.session.user` is not null.
*
* @see https://trpc.io/docs/procedures
*/
export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
if (!ctx.session || !ctx.session.user) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
@@ -84,13 +99,3 @@ const enforceUserIsAuthed = t.middleware(({ ctx, next }) => {
},
});
});
/**
* Protected (authenticated) procedure
*
* If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies
* the session is valid and guarantees `ctx.session.user` is not null.
*
* @see https://trpc.io/docs/procedures
*/
export const protectedProcedure = t.procedure.use(enforceUserIsAuthed);

72
src/server/auth.ts Normal file
View File

@@ -0,0 +1,72 @@
import { DrizzleAdapter } from "@auth/drizzle-adapter";
import {
getServerSession,
type DefaultSession,
type NextAuthOptions,
} from "next-auth";
import { type Adapter } from "next-auth/adapters";
import DiscordProvider from "next-auth/providers/discord";
import { env } from "@/env";
import { db } from "@/server/db";
/**
* Module augmentation for `next-auth` types. Allows us to add custom properties to the `session`
* object and keep type safety.
*
* @see https://next-auth.js.org/getting-started/typescript#module-augmentation
*/
declare module "next-auth" {
interface Session extends DefaultSession {
user: {
id: string;
// ...other properties
// role: UserRole;
} & DefaultSession["user"];
}
// interface User {
// // ...other properties
// // role: UserRole;
// }
}
/**
* Options for NextAuth.js used to configure adapters, providers, callbacks, etc.
*
* @see https://next-auth.js.org/configuration/options
*/
export const authOptions: NextAuthOptions = {
callbacks: {
session: ({ session, user }) => ({
...session,
user: {
...session.user,
id: user.id,
},
}),
},
adapter: DrizzleAdapter(db) as Adapter,
providers: [
DiscordProvider({
clientId: env.GOOGLE_CLIENT_ID,
clientSecret: env.GOOGLE_CLIENT_SECRET,
}),
/**
* ...add more providers here.
*
* Most other providers require a bit more work than the Discord provider. For example, the
* GitHub provider requires you to add the `refresh_token_expires_in` field to the Account
* model. Refer to the NextAuth.js docs for the provider you want to use. Example:
*
* @see https://next-auth.js.org/providers/github
*/
],
};
/**
* Wrapper for `getServerSession` so that you don't need to import the `authOptions` in every file.
*
* @see https://next-auth.js.org/configuration/nextjs
*/
export const getServerAuthSession = () => getServerSession(authOptions);

View File

@@ -1,6 +1,6 @@
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import { env } from "@/env.mjs";
import { env } from "@/env";
import {
users,
accounts,

View File

@@ -1,4 +1,4 @@
import { env } from "@/env.mjs";
import { env } from "@/env";
import { drizzle } from "drizzle-orm/postgres-js";
import { migrate } from "drizzle-orm/postgres-js/migrator";