mirror of
https://github.com/fergalmoran/kidarr-server.git
synced 2025-12-22 09:17:51 +00:00
Fix routing modes
This commit is contained in:
3
.idea/inspectionProfiles/Project_Default.xml
generated
3
.idea/inspectionProfiles/Project_Default.xml
generated
@@ -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>
|
||||||
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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]);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
57
src/env.js
57
src/env.js
@@ -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,
|
|
||||||
});
|
});
|
||||||
|
|||||||
36
src/env.mjs
36
src/env.mjs
@@ -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,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,21 +1,24 @@
|
|||||||
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 };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = (err as Error).message ?? "Error, please try again";
|
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) => {
|
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(
|
||||||
.returning();
|
and(eq(children.id, childId), eq(children.userId, session?.user.id!)),
|
||||||
|
)
|
||||||
|
.returning();
|
||||||
return { child: c };
|
return { child: c };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = (err as Error).message ?? "Error, please try again";
|
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 { 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
|
||||||
.returning();
|
.delete(children)
|
||||||
|
.where(
|
||||||
|
and(eq(children.id, childId), eq(children.userId, session?.user.id!)),
|
||||||
|
)
|
||||||
|
.returning();
|
||||||
return { child: c };
|
return { child: c };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = (err as Error).message ?? "Error, please try again";
|
const message = (err as Error).message ?? "Error, please try again";
|
||||||
@@ -55,4 +67,3 @@ export const deleteChild = async (id: ChildId) => {
|
|||||||
throw { error: message };
|
throw { error: message };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,24 @@
|
|||||||
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 };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = (err as Error).message ?? "Error, please try again";
|
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 { 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(
|
||||||
.returning();
|
and(eq(devices.id, deviceId), eq(devices.userId, session?.user.id!)),
|
||||||
|
)
|
||||||
|
.returning();
|
||||||
return { device: d };
|
return { device: d };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = (err as Error).message ?? "Error, please try again";
|
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 { 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
|
||||||
.returning();
|
.delete(devices)
|
||||||
|
.where(
|
||||||
|
and(eq(devices.id, deviceId), eq(devices.userId, session?.user.id!)),
|
||||||
|
)
|
||||||
|
.returning();
|
||||||
return { device: d };
|
return { device: d };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = (err as Error).message ?? "Error, please try again";
|
const message = (err as Error).message ?? "Error, please try again";
|
||||||
@@ -55,4 +70,3 @@ export const deleteDevice = async (id: DeviceId) => {
|
|||||||
throw { error: message };
|
throw { error: message };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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 };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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
72
src/server/auth.ts
Normal 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);
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
Reference in New Issue
Block a user