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"> <profile version="1.0">
<option name="myName" value="Project Default" /> <option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" /> <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> </profile>
</component> </component>

View File

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

BIN
bun.lockb

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

View File

@@ -47,7 +47,6 @@ const MapMarker: React.FC<MapMarkerProps> = ({
usePingSocket({ usePingSocket({
deviceId: deviceId, deviceId: deviceId,
locationUpdate: (ping) => { locationUpdate: (ping) => {
console.log("MainMap", "locationUpdate", ping);
setPosition([ping.location.latitude, ping.location.longitude]); 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 { Icons } from "@/components/icons";
import React from "react"; import React from "react";
export const MapViewTypeSelector = () => { type MapViewTypeSelectorProps = {
const [currentView, setCurrentView] = React.useState("location"); currentView: "latest" | "route";
onChange: (mode: "latest" | "route") => void;
};
export const MapViewTypeSelector: React.FC<MapViewTypeSelectorProps> = ({
currentView,
onChange,
}) => {
return ( return (
<ToggleGroup <ToggleGroup
type="single" type="single"
value={currentView} 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" /> <Icons.location className="mr-1 h-4 w-4" />
Location Location
</ToggleGroupItem> </ToggleGroupItem>

View File

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

View File

@@ -2,39 +2,36 @@ import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod"; import { z } from "zod";
export const env = createEnv({ 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: { server: {
NODE_ENV: z.enum(["development", "test", "production"]), NODE_ENV: z
}, .enum(["development", "test", "production"])
.default("development"),
DATABASE_URL: z.string().min(1),
/** NEXTAUTH_SECRET:
* Specify your client-side environment variables schema here. This way you can ensure the app process.env.NODE_ENV === "production"
* isn't built with invalid env vars. To expose them to the client, prefix them with ? z.string().min(1)
* `NEXT_PUBLIC_`. : 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: { client: {
// NEXT_PUBLIC_CLIENTVAR: z.string(), // 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: {
* You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g. // DATABASE_URL: process.env.DATABASE_URL,
* middlewares) or client-side so we need to destruct manually. // NEXT_PUBLIC_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_PUBLISHABLE_KEY,
*/ // },
runtimeEnv: { // For Next.js >= 13.4.4, you only need to destructure client variables:
NODE_ENV: process.env.NODE_ENV, experimental__runtimeEnv: {
// NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR, // 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,19 +1,22 @@
import { db } from "@/server/db/index"; import { db } from "@/server/db/index";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import { import {
ChildId, type ChildId,
NewChildParams, type NewChildParams,
UpdateChildParams, type UpdateChildParams,
updateChildSchema, updateChildSchema,
insertChildSchema, insertChildSchema,
children, children,
childIdSchema childIdSchema,
} from "@/server/db/schema/children"; } from "@/server/db/schema/children";
import { getUserAuth } from "@/lib/auth/utils"; import { getUserAuth } from "@/lib/auth/utils";
export const createChild = async (child: NewChildParams) => { export const createChild = async (child: NewChildParams) => {
const { session } = await getUserAuth(); const { session } = await getUserAuth();
const newChild = insertChildSchema.parse({ ...child, userId: session?.user.id! }); const newChild = insertChildSchema.parse({
...child,
userId: session?.user.id!,
});
try { try {
const [c] = await db.insert(children).values(newChild).returning(); const [c] = await db.insert(children).values(newChild).returning();
return { child: c }; return { child: c };
@@ -27,12 +30,17 @@ export const createChild = async (child: NewChildParams) => {
export const updateChild = async (id: ChildId, child: UpdateChildParams) => { export const updateChild = async (id: ChildId, child: UpdateChildParams) => {
const { session } = await getUserAuth(); const { session } = await getUserAuth();
const { id: childId } = childIdSchema.parse({ id }); 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 { try {
const [c] = await db const [c] = await db
.update(children) .update(children)
.set(newChild) .set(newChild)
.where(and(eq(children.id, childId!), eq(children.userId, session?.user.id!))) .where(
and(eq(children.id, childId), eq(children.userId, session?.user.id!)),
)
.returning(); .returning();
return { child: c }; return { child: c };
} catch (err) { } catch (err) {
@@ -46,7 +54,11 @@ export const deleteChild = async (id: ChildId) => {
const { session } = await getUserAuth(); const { session } = await getUserAuth();
const { id: childId } = childIdSchema.parse({ id }); const { id: childId } = childIdSchema.parse({ id });
try { try {
const [c] = await db.delete(children).where(and(eq(children.id, childId!), eq(children.userId, session?.user.id!))) const [c] = await db
.delete(children)
.where(
and(eq(children.id, childId), eq(children.userId, session?.user.id!)),
)
.returning(); .returning();
return { child: c }; return { child: c };
} catch (err) { } catch (err) {
@@ -55,4 +67,3 @@ export const deleteChild = async (id: ChildId) => {
throw { error: message }; throw { error: message };
} }
}; };

View File

@@ -1,19 +1,22 @@
import { db } from "@/server/db/index"; import { db } from "@/server/db/index";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import { import {
DeviceId, type DeviceId,
NewDeviceParams, type NewDeviceParams,
UpdateDeviceParams, type UpdateDeviceParams,
updateDeviceSchema, updateDeviceSchema,
insertDeviceSchema, insertDeviceSchema,
devices, devices,
deviceIdSchema deviceIdSchema,
} from "@/server/db/schema/devices"; } from "@/server/db/schema/devices";
import { getUserAuth } from "@/lib/auth/utils"; import { getUserAuth } from "@/lib/auth/utils";
export const createDevice = async (device: NewDeviceParams) => { export const createDevice = async (device: NewDeviceParams) => {
const { session } = await getUserAuth(); const { session } = await getUserAuth();
const newDevice = insertDeviceSchema.parse({ ...device, userId: session?.user.id! }); const newDevice = insertDeviceSchema.parse({
...device,
userId: session?.user.id!,
});
try { try {
const [d] = await db.insert(devices).values(newDevice).returning(); const [d] = await db.insert(devices).values(newDevice).returning();
return { device: d }; return { device: d };
@@ -24,15 +27,23 @@ 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 { session } = await getUserAuth();
const { id: deviceId } = deviceIdSchema.parse({ id }); 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 { try {
const [d] = await db const [d] = await db
.update(devices) .update(devices)
.set(newDevice) .set(newDevice)
.where(and(eq(devices.id, deviceId!), eq(devices.userId, session?.user.id!))) .where(
and(eq(devices.id, deviceId), eq(devices.userId, session?.user.id!)),
)
.returning(); .returning();
return { device: d }; return { device: d };
} catch (err) { } catch (err) {
@@ -46,7 +57,11 @@ export const deleteDevice = async (id: DeviceId) => {
const { session } = await getUserAuth(); const { session } = await getUserAuth();
const { id: deviceId } = deviceIdSchema.parse({ id }); const { id: deviceId } = deviceIdSchema.parse({ id });
try { try {
const [d] = await db.delete(devices).where(and(eq(devices.id, deviceId!), eq(devices.userId, session?.user.id!))) const [d] = await db
.delete(devices)
.where(
and(eq(devices.id, deviceId), eq(devices.userId, session?.user.id!)),
)
.returning(); .returning();
return { device: d }; return { device: d };
} catch (err) { } catch (err) {
@@ -55,4 +70,3 @@ export const deleteDevice = async (id: DeviceId) => {
throw { error: message }; throw { error: message };
} }
}; };

View File

@@ -2,18 +2,22 @@ import { db } from "@/server/db/index";
import { eq, and } from "drizzle-orm"; import { eq, and } from "drizzle-orm";
import { getUserAuth } from "@/lib/auth/utils"; import { getUserAuth } from "@/lib/auth/utils";
import { type PingId, pingIdSchema, pings } from "@/server/db/schema/pings"; import { type PingId, pingIdSchema, pings } from "@/server/db/schema/pings";
import { devices } from "@/server/db/schema/devices";
export const getPings = async () => { export const getPings = async () => {
const { session } = await getUserAuth(); 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 }; return { pings: p };
}; };
export const getPingById = async (id: PingId) => { export const getPingById = async (id: PingId) => {
const { session } = await getUserAuth(); const { session } = await getUserAuth();
const { id: pingId } = pingIdSchema.parse({ id }); 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 }; return { ping: p };
}; };

View File

@@ -2,8 +2,9 @@ import { db } from "@/server/db/index";
import { DrizzleAdapter } from "@auth/drizzle-adapter"; import { DrizzleAdapter } from "@auth/drizzle-adapter";
import { DefaultSession, getServerSession, NextAuthOptions } from "next-auth"; import { DefaultSession, getServerSession, NextAuthOptions } from "next-auth";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { env } from "@/env.mjs";
import GoogleProvider from "next-auth/providers/google"; import GoogleProvider from "next-auth/providers/google";
import { Adapter } from "next-auth/adapters";
import { env } from "@/env";
declare module "next-auth" { declare module "next-auth" {
interface Session { interface Session {
@@ -24,7 +25,7 @@ export type AuthSession = {
}; };
export const authOptions: NextAuthOptions = { export const authOptions: NextAuthOptions = {
adapter: DrizzleAdapter(db), adapter: DrizzleAdapter(db) as Adapter,
callbacks: { callbacks: {
session: ({ session, user }) => { session: ({ session, user }) => {
session.user.id = user.id; 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) { if (pings && pings.length !== 0) {
return pings.reduce((ping, current) => return pings.reduce((ping, current) =>
ping.timestamp > current.timestamp ? 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 { StatusCodes } from "http-status-codes";
import { type NextApiResponseServerIo } from "@/lib/models/types/next-api-response-socket"; import { type NextApiResponseServerIo } from "@/lib/models/types/next-api-response-socket";
import { type NextApiRequest } from "next"; import { type NextApiRequest } from "next";
import type LocationUpdate from "@/lib/models/location-update";
import { badRequest, notAuthorised } from "@/lib/api/responses"; import { badRequest, notAuthorised } from "@/lib/api/responses";
import { getDeviceById } from "@/lib/api/devices/queries"; import { getDeviceById } from "@/lib/api/devices/queries";
import { getChildById } from "@/lib/api/children/queries";
import { createPing } from "@/lib/api/pings/mutations"; import { createPing } from "@/lib/api/pings/mutations";
type PingRequest = { 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 * 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. * 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 superjson from "superjson";
import { ZodError } from "zod"; import { ZodError } from "zod";
import { db } from "@/server/db";
import { getServerAuthSession } from "../auth";
/** /**
* 1. CONTEXT * 1. CONTEXT
* *
@@ -23,7 +27,11 @@ import { ZodError } from "zod";
* @see https://trpc.io/docs/server/context * @see https://trpc.io/docs/server/context
*/ */
export const createTRPCContext = async (opts: { headers: Headers }) => { export const createTRPCContext = async (opts: { headers: Headers }) => {
const session = await getServerAuthSession();
return { return {
db,
session,
...opts, ...opts,
}; };
}; };
@@ -72,8 +80,15 @@ export const createTRPCRouter = t.router;
*/ */
export const publicProcedure = t.procedure; 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) { if (!ctx.session || !ctx.session.user) {
throw new TRPCError({ code: "UNAUTHORIZED" }); 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 { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres"; import postgres from "postgres";
import { env } from "@/env.mjs"; import { env } from "@/env";
import { import {
users, users,
accounts, accounts,

View File

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