diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000..2727a18
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,28 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Next.js: debug server-side",
+ "type": "node-terminal",
+ "request": "launch",
+ "command": "bun dev"
+ },
+ {
+ "name": "Next.js: debug client-side",
+ "type": "chrome",
+ "request": "launch",
+ "url": "http://localhost:3000"
+ },
+ {
+ "name": "Next.js: debug full stack",
+ "type": "node-terminal",
+ "request": "launch",
+ "command": "bun dev",
+ "serverReadyAction": {
+ "pattern": "- Local:.+(https?://.+)",
+ "uriFormat": "%s",
+ "action": "debugWithChrome"
+ }
+ }
+ ]
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 77478c2..f2a0327 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -12,7 +12,7 @@
"**/.DS_Store": true,
"**/Thumbs.db": true
},
- "workbench.colorTheme": "Pink Cat Boo",
+ "workbench.colorTheme": "Monokai Classic",
"workbench.iconTheme": "vscode-icontheme-nomo-dark-macos",
"workbench.colorCustomizations": {
"activityBar.activeBackground": "#ab307e",
@@ -33,5 +33,17 @@
"titleBar.inactiveBackground": "#83256199",
"titleBar.inactiveForeground": "#e7e7e799"
},
- "peacock.color": "#832561"
+ "peacock.color": "#832561",
+ "sqltools.connections": [
+ {
+ "previewLimit": 50,
+ "server": "localhost",
+ "port": 5432,
+ "driver": "PostgreSQL",
+ "name": "localhost",
+ "username": "postgres",
+ "password": "hackme",
+ "database": "parentgrine"
+ }
+ ]
}
diff --git a/bun.lockb b/bun.lockb
index ba78f91..b30d231 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/drizzle.config.ts b/drizzle.config.ts
index 43d26a9..36b5421 100644
--- a/drizzle.config.ts
+++ b/drizzle.config.ts
@@ -1,13 +1,13 @@
-import type { Config } from "drizzle-kit";
-import 'dotenv/config'
+import type { Config } from 'drizzle-kit';
+import 'dotenv/config';
if (!process.env.DATABASE_URL) {
- throw new Error("DATABASE_URL is missing");
+ throw new Error('DATABASE_URL is missing');
}
export default {
- schema: "./src/db/schema/*",
- out: "./drizzle",
- driver: "pg",
+ schema: './src/db',
+ out: './drizzle',
+ driver: 'pg',
dbCredentials: {
connectionString: process.env.DATABASE_URL as string,
},
diff --git a/drizzle/0000_bizarre_firebrand.sql b/drizzle/0000_medical_jetstream.sql
similarity index 80%
rename from drizzle/0000_bizarre_firebrand.sql
rename to drizzle/0000_medical_jetstream.sql
index d753b09..bfcd2a8 100644
--- a/drizzle/0000_bizarre_firebrand.sql
+++ b/drizzle/0000_medical_jetstream.sql
@@ -13,6 +13,25 @@ CREATE TABLE IF NOT EXISTS "account" (
CONSTRAINT account_provider_providerAccountId PRIMARY KEY("provider","providerAccountId")
);
--> statement-breakpoint
+CREATE TABLE IF NOT EXISTS "child" (
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
+ "parent_id" uuid NOT NULL,
+ "name" varchar(256),
+ "phone" varchar(256),
+ "key" varchar(256)
+);
+--> statement-breakpoint
+CREATE TABLE IF NOT EXISTS "device" (
+ "child_id" uuid NOT NULL,
+ "pin" integer NOT NULL,
+ "device_id" varchar NOT NULL,
+ "api_key" varchar NOT NULL,
+ "expires" timestamp DEFAULT now() + interval '1 hour',
+ CONSTRAINT device_child_id_device_id PRIMARY KEY("child_id","device_id"),
+ CONSTRAINT "device_device_id_unique" UNIQUE("device_id"),
+ CONSTRAINT "device_api_key_unique" UNIQUE("api_key")
+);
+--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "session" (
"sessionToken" text PRIMARY KEY NOT NULL,
"userId" uuid NOT NULL,
@@ -34,41 +53,12 @@ CREATE TABLE IF NOT EXISTS "verificationToken" (
CONSTRAINT verificationToken_identifier_token PRIMARY KEY("identifier","token")
);
--> statement-breakpoint
-CREATE TABLE IF NOT EXISTS "child" (
- "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
- "parent_id" uuid NOT NULL,
- "name" varchar(256),
- "phone" varchar(256),
- "key" varchar(256)
-);
---> statement-breakpoint
-CREATE TABLE IF NOT EXISTS "child_devices" (
- "child_id" uuid NOT NULL,
- "pin" integer,
- "api_key" varchar,
- "expires" timestamp DEFAULT now() + interval '1 hour',
- CONSTRAINT child_devices_child_id_pin PRIMARY KEY("child_id","pin")
-);
---> statement-breakpoint
-CREATE TABLE IF NOT EXISTS "locations" (
- "uuid1" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
- "longitude" numeric,
- "latitude" numeric,
- "user_id" uuid
-);
---> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "account" ADD CONSTRAINT "account_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
-DO $$ BEGIN
- ALTER TABLE "session" ADD CONSTRAINT "session_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE cascade ON UPDATE no action;
-EXCEPTION
- WHEN duplicate_object THEN null;
-END $$;
---> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "child" ADD CONSTRAINT "child_parent_id_user_id_fk" FOREIGN KEY ("parent_id") REFERENCES "user"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
@@ -76,7 +66,13 @@ EXCEPTION
END $$;
--> statement-breakpoint
DO $$ BEGIN
- ALTER TABLE "child_devices" ADD CONSTRAINT "child_devices_child_id_child_id_fk" FOREIGN KEY ("child_id") REFERENCES "child"("id") ON DELETE cascade ON UPDATE no action;
+ ALTER TABLE "device" ADD CONSTRAINT "device_child_id_child_id_fk" FOREIGN KEY ("child_id") REFERENCES "child"("id") ON DELETE cascade ON UPDATE no action;
+EXCEPTION
+ WHEN duplicate_object THEN null;
+END $$;
+--> statement-breakpoint
+DO $$ BEGIN
+ ALTER TABLE "session" ADD CONSTRAINT "session_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json
index c2680dd..08cebeb 100644
--- a/drizzle/meta/0000_snapshot.json
+++ b/drizzle/meta/0000_snapshot.json
@@ -1,8 +1,8 @@
{
+ "id": "31902de1-3b11-4911-8297-18ddfbb762fa",
+ "prevId": "00000000-0000-0000-0000-000000000000",
"version": "5",
"dialect": "pg",
- "id": "95fbc031-2db5-4391-aa2c-f48c8829eda6",
- "prevId": "00000000-0000-0000-0000-000000000000",
"tables": {
"account": {
"name": "account",
@@ -102,6 +102,139 @@
},
"uniqueConstraints": {}
},
+ "child": {
+ "name": "child",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "parent_id": {
+ "name": "parent_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(256)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "phone": {
+ "name": "phone",
+ "type": "varchar(256)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "key": {
+ "name": "key",
+ "type": "varchar(256)",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "child_parent_id_user_id_fk": {
+ "name": "child_parent_id_user_id_fk",
+ "tableFrom": "child",
+ "tableTo": "user",
+ "columnsFrom": [
+ "parent_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "device": {
+ "name": "device",
+ "schema": "",
+ "columns": {
+ "child_id": {
+ "name": "child_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "pin": {
+ "name": "pin",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "device_id": {
+ "name": "device_id",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "api_key": {
+ "name": "api_key",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires": {
+ "name": "expires",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now() + interval '1 hour'"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "device_child_id_child_id_fk": {
+ "name": "device_child_id_child_id_fk",
+ "tableFrom": "device",
+ "tableTo": "child",
+ "columnsFrom": [
+ "child_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "device_child_id_device_id": {
+ "name": "device_child_id_device_id",
+ "columns": [
+ "child_id",
+ "device_id"
+ ]
+ }
+ },
+ "uniqueConstraints": {
+ "device_device_id_unique": {
+ "name": "device_device_id_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "device_id"
+ ]
+ },
+ "device_api_key_unique": {
+ "name": "device_api_key_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "api_key"
+ ]
+ }
+ }
+ },
"session": {
"name": "session",
"schema": "",
@@ -219,153 +352,6 @@
}
},
"uniqueConstraints": {}
- },
- "child": {
- "name": "child",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "uuid",
- "primaryKey": true,
- "notNull": true,
- "default": "gen_random_uuid()"
- },
- "parent_id": {
- "name": "parent_id",
- "type": "uuid",
- "primaryKey": false,
- "notNull": true
- },
- "name": {
- "name": "name",
- "type": "varchar(256)",
- "primaryKey": false,
- "notNull": false
- },
- "phone": {
- "name": "phone",
- "type": "varchar(256)",
- "primaryKey": false,
- "notNull": false
- },
- "key": {
- "name": "key",
- "type": "varchar(256)",
- "primaryKey": false,
- "notNull": false
- }
- },
- "indexes": {},
- "foreignKeys": {
- "child_parent_id_user_id_fk": {
- "name": "child_parent_id_user_id_fk",
- "tableFrom": "child",
- "tableTo": "user",
- "columnsFrom": [
- "parent_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
- },
- "child_devices": {
- "name": "child_devices",
- "schema": "",
- "columns": {
- "child_id": {
- "name": "child_id",
- "type": "uuid",
- "primaryKey": false,
- "notNull": true
- },
- "pin": {
- "name": "pin",
- "type": "integer",
- "primaryKey": false,
- "notNull": false
- },
- "api_key": {
- "name": "api_key",
- "type": "varchar",
- "primaryKey": false,
- "notNull": false
- },
- "expires": {
- "name": "expires",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": false,
- "default": "now() + interval '1 hour'"
- }
- },
- "indexes": {},
- "foreignKeys": {
- "child_devices_child_id_child_id_fk": {
- "name": "child_devices_child_id_child_id_fk",
- "tableFrom": "child_devices",
- "tableTo": "child",
- "columnsFrom": [
- "child_id"
- ],
- "columnsTo": [
- "id"
- ],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {
- "child_devices_child_id_pin": {
- "name": "child_devices_child_id_pin",
- "columns": [
- "child_id",
- "pin"
- ]
- }
- },
- "uniqueConstraints": {}
- },
- "locations": {
- "name": "locations",
- "schema": "",
- "columns": {
- "uuid1": {
- "name": "uuid1",
- "type": "uuid",
- "primaryKey": true,
- "notNull": true,
- "default": "gen_random_uuid()"
- },
- "longitude": {
- "name": "longitude",
- "type": "numeric",
- "primaryKey": false,
- "notNull": false
- },
- "latitude": {
- "name": "latitude",
- "type": "numeric",
- "primaryKey": false,
- "notNull": false
- },
- "user_id": {
- "name": "user_id",
- "type": "uuid",
- "primaryKey": false,
- "notNull": false
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {}
}
},
"enums": {},
diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json
index 83ed5cf..e1a71c9 100644
--- a/drizzle/meta/_journal.json
+++ b/drizzle/meta/_journal.json
@@ -5,8 +5,8 @@
{
"idx": 0,
"version": "5",
- "when": 1699562330825,
- "tag": "0000_bizarre_firebrand",
+ "when": 1700413726993,
+ "tag": "0000_medical_jetstream",
"breakpoints": true
}
]
diff --git a/package.json b/package.json
index e1d4712..9f2f72f 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,7 @@
"lint": "next lint"
},
"dependencies": {
- "@auth/drizzle-adapter": "^0.3.5",
+ "@auth/drizzle-adapter": "^0.3.6",
"@hookform/resolvers": "^3.3.2",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-alert-dialog": "^1.0.5",
@@ -38,9 +38,9 @@
"@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-toggle": "^1.0.3",
"@radix-ui/react-tooltip": "^1.0.7",
- "@tanstack/react-query": "^5.8.1",
- "@tanstack/react-query-devtools": "^5.8.1",
- "axios": "^1.6.1",
+ "@tanstack/react-query": "^5.8.4",
+ "@tanstack/react-query-devtools": "^5.8.4",
+ "axios": "^1.6.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"cmdk": "^0.2.0",
@@ -52,9 +52,10 @@
"leaflet": "^1.9.4",
"local-ssl-proxy": "^2.0.5",
"lucide-react": "^0.292.0",
- "next": "14.0.2",
- "next-auth": "^4.24.4",
+ "next": "14.0.3",
+ "next-auth": "^4.24.5",
"next-themes": "^0.2.1",
+ "pg": "^8.11.3",
"postgres": "^3.4.3",
"react": "^18",
"react-day-picker": "^8.9.1",
@@ -72,11 +73,11 @@
"@types/react": "^18",
"@types/react-dom": "^18",
"autoprefixer": "^10",
- "drizzle-kit": "^0.20.1",
+ "drizzle-kit": "^0.20.4",
"eslint": "^8",
- "eslint-config-next": "14.0.2",
+ "eslint-config-next": "14.0.3",
"postcss": "^8",
- "prettier": "3.0.3",
+ "prettier": "3.1.0",
"tailwindcss": "^3",
"typescript": "^5"
}
diff --git a/scripts/reset.sh b/scripts/reset.sh
index 63518cf..5c717d7 100755
--- a/scripts/reset.sh
+++ b/scripts/reset.sh
@@ -3,6 +3,7 @@ export PGUSER=postgres
export PGPASSWORD=hackme
export PGHOST=localhost
+
echo Removing migrations
rm -rf drizzle
echo "Dropping db"
diff --git a/src/app/api/child/create/route.ts b/src/app/api/child/create/route.ts
index 759dc97..36ca131 100644
--- a/src/app/api/child/create/route.ts
+++ b/src/app/api/child/create/route.ts
@@ -4,19 +4,20 @@ import { createApiKey } from '@/lib/services/auth/api';
import { NextResponse } from 'next/server';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
-import db from '@/db/schema';
+import db from '@/db';
-import { child, childDevices } from '@/db/schema/child';
-import { users } from '@/db/schema/auth';
+import { child } from '@/db/schema';
+import { users } from '@/db/schema';
import { eq } from 'drizzle-orm';
+import { device } from '@/db/schema';
export async function POST(req: Request) {
const session = await getServerAuthSession();
if (!session || !session.user?.email)
- return NextResponse.json(
- { error: getReasonPhrase(StatusCodes.UNAUTHORIZED) },
- { status: StatusCodes.UNAUTHORIZED }
- );
+ return NextResponse.next({
+ statusText: getReasonPhrase(StatusCodes.UNAUTHORIZED),
+ status: StatusCodes.UNAUTHORIZED,
+ });
const body = await req.json();
const user = await db
@@ -25,10 +26,10 @@ export async function POST(req: Request) {
.where(eq(users.email, session.user.email));
if (!user) {
- return NextResponse.json(
- { error: `Unable to find user id for ${session.user.email}` },
- { status: StatusCodes.INTERNAL_SERVER_ERROR }
- );
+ return NextResponse.next({
+ statusText: `Unable to find user id for ${session.user.email}`,
+ status: StatusCodes.INTERNAL_SERVER_ERROR,
+ });
}
const { name } = newChildSchema.parse(body);
@@ -40,26 +41,11 @@ export async function POST(req: Request) {
})
.returning();
if (!newChild) {
- return NextResponse.json(
- { error: `Error inserting child` },
- { status: StatusCodes.INTERNAL_SERVER_ERROR }
- );
+ return NextResponse.next({
+ statusText: 'Error inserting child',
+ status: StatusCodes.INTERNAL_SERVER_ERROR,
+ });
}
- let done = false;
- let pin = 0;
- while (!done) {
- pin = Math.floor(1000 + Math.random() * 9000);
- const exists = await db
- .selectDistinct()
- .from(childDevices)
- .where(eq(childDevices.childId, newChild[0].id));
- done = exists.length === 0;
- }
- const apiKey = createApiKey();
- await db
- .insert(childDevices)
- .values({ childId: newChild[0].id, pin, apiKey: apiKey })
- .execute();
- return NextResponse.json({ status: 'success', pin: pin });
+ return NextResponse.json({ status: 'success' });
}
diff --git a/src/app/api/child/ping/route.ts b/src/app/api/child/ping/route.ts
new file mode 100644
index 0000000..804a94e
--- /dev/null
+++ b/src/app/api/child/ping/route.ts
@@ -0,0 +1,41 @@
+import db from '@/db';
+import { StatusCodes } from 'http-status-codes';
+import { headers } from 'next/headers';
+import { eq } from 'drizzle-orm';
+import { device, child } from '@/db/schema';
+export async function POST(req: Request) {
+ const headersList = headers();
+ const apiKey = headersList.get('X-Auth-ApiKey');
+
+ if (!apiKey) {
+ return new Response('Unauthorized', {
+ status: StatusCodes.UNAUTHORIZED,
+ statusText: 'Unauthorized',
+ });
+ }
+
+ const childId = await db
+ .selectDistinct()
+ .from(device)
+ .where(eq(device.apiKey, apiKey));
+ if (!childId) {
+ return new Response('Unauthorized', {
+ status: StatusCodes.UNAUTHORIZED,
+ statusText: 'Unauthorized',
+ });
+ }
+ const pinger = await db
+ .selectDistinct()
+ .from(child)
+ .where(eq(child.id, childId[0].childId));
+ if (!pinger) {
+ return new Response('Unauthorized', {
+ status: StatusCodes.UNAUTHORIZED,
+ statusText: 'Unauthorized',
+ });
+ }
+ return new Response('pong', {
+ status: StatusCodes.OK,
+ statusText: 'OK',
+ });
+}
diff --git a/src/app/api/child/route.ts b/src/app/api/child/route.ts
index 74f5c5c..cc85b52 100644
--- a/src/app/api/child/route.ts
+++ b/src/app/api/child/route.ts
@@ -1,5 +1,5 @@
-import db from '@/db/schema';
-import { child } from '@/db/schema/child';
+import db from '@/db';
+import { child } from '@/db/schema';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { NextResponse } from 'next/server';
import { getServerAuthSession } from '@/lib/services/auth/config';
diff --git a/src/app/api/device/connect/route.ts b/src/app/api/device/connect/route.ts
index e69de29..e0d70b8 100644
--- a/src/app/api/device/connect/route.ts
+++ b/src/app/api/device/connect/route.ts
@@ -0,0 +1,58 @@
+import db from '@/db'
+import { eq } from 'drizzle-orm'
+import { StatusCodes } from 'http-status-codes'
+import { child, device } from '@/db/schema'
+import { createApiKey } from '@/lib/services/auth/api'
+import { badRequest } from '@/app/api/responses'
+
+const POST = async (req: Request, res: Response) => {
+ if (req.method === 'POST') {
+ const url = new URL(req.url)
+ const { deviceId, childId } = await req.json()
+ console.log('route', 'childId', childId)
+ console.log('route', 'deviceId', deviceId)
+
+ if (!childId || !deviceId) {
+ return badRequest('Invalid registration request')
+ }
+
+ const childToRegister = (
+ await db.selectDistinct().from(child).where(eq(child.id, childId))
+ )[0]
+
+ if (!childToRegister) {
+ return badRequest('Invalid registration request')
+ }
+
+ let done = false
+ let pin = 2021
+ while (!done) {
+ pin = Math.floor(1000 + Math.random() * 9000)
+ console.log('route', 'device/connect', 'checking for PIN', pin)
+ const exists = await db
+ .selectDistinct()
+ .from(device)
+ .where(eq(device.pin, pin))
+ console.log('route', 'exists', exists)
+ done = exists.length === 0
+ }
+
+ const apiKey = createApiKey()
+ await db
+ .insert(device)
+ .values({
+ childId: childToRegister.id,
+ deviceId: deviceId,
+ pin,
+ apiKey: apiKey,
+ })
+ .execute()
+ return Response.json(
+ { childId, pin, apiKey },
+ { status: StatusCodes.CREATED },
+ )
+ }
+ return badRequest('Invalid registration request')
+}
+
+export { POST }
diff --git a/src/app/api/device/ping/route.ts b/src/app/api/device/ping/route.ts
new file mode 100644
index 0000000..b477acb
--- /dev/null
+++ b/src/app/api/device/ping/route.ts
@@ -0,0 +1,34 @@
+import { StatusCodes } from 'http-status-codes';
+import { notAuthorised, badRequest } from '../../responses';
+import db from '@/db';
+import { device } from '@/db/schema';
+import { eq } from 'drizzle-orm';
+
+const POST = async (req: Request, res: Response) => {
+ const apiKey = req.headers.get('x-api-key');
+ if (!apiKey) {
+ return notAuthorised();
+ }
+
+ const user = await db.query.device.findFirst({
+ where: (device, { eq }) => eq(device.apiKey, apiKey),
+ with: {
+ child: true,
+ },
+ });
+
+ const { coordinates } = await req.json();
+
+ // Check if coordinates exist in the headers
+ if (!coordinates) {
+ return badRequest('Invalid coordinates');
+ }
+
+ // Process the coordinates
+ // ...
+
+ // Send a response
+ return Response.json({}, { status: StatusCodes.CREATED });
+};
+
+export { POST };
diff --git a/src/app/api/responses/index.ts b/src/app/api/responses/index.ts
new file mode 100644
index 0000000..5cd6bad
--- /dev/null
+++ b/src/app/api/responses/index.ts
@@ -0,0 +1,10 @@
+import { StatusCodes } from 'http-status-codes';
+
+export const notAuthorised = () =>
+ new Response('Not authorised', {
+ status: StatusCodes.BAD_REQUEST,
+ });
+export const badRequest = (message: string) =>
+ new Response(message, {
+ status: StatusCodes.BAD_REQUEST,
+ });
diff --git a/src/components/children/child-select-list.tsx b/src/components/children/child-select-list.tsx
index c373046..e8d1342 100644
--- a/src/components/children/child-select-list.tsx
+++ b/src/components/children/child-select-list.tsx
@@ -17,7 +17,7 @@ const ChildSelectList = () => {
queryKey: ['user-children'],
queryFn: async () => {
const { data } = await axios.get(
- `${process.env.NEXT_PUBLIC_BASE_URL}/api/child`,
+ `${process.env.NEXT_PUBLIC_BASE_URL}/child`,
{
withCredentials: true,
}
diff --git a/src/components/children/children-list.tsx b/src/components/children/children-list.tsx
index b991ad1..83f7ccb 100644
--- a/src/components/children/children-list.tsx
+++ b/src/components/children/children-list.tsx
@@ -19,7 +19,7 @@ const ChildrenList = () => {
queryKey: ['user-children'],
queryFn: async () => {
const { data } = await axios.get(
- `${process.env.NEXT_PUBLIC_BASE_URL}/api/child`,
+ `${process.env.NEXT_PUBLIC_BASE_URL}/child`,
{
withCredentials: true,
}
@@ -34,9 +34,9 @@ const ChildrenList = () => {
Here are your children.
- Name
+ Name
Last seen at
- Actions
+ Actions
@@ -45,7 +45,12 @@ const ChildrenList = () => {
{child.name}
Douglas
-
+
+
+
+
))}
diff --git a/src/components/children/connect-device-dialog.tsx b/src/components/children/connect-device-dialog.tsx
index e0e60db..6f1fa60 100644
--- a/src/components/children/connect-device-dialog.tsx
+++ b/src/components/children/connect-device-dialog.tsx
@@ -37,7 +37,7 @@ const ConnectDeviceDialog: React.FC = ({ child }) => {
diff --git a/src/components/icons.tsx b/src/components/icons.tsx
index faf81bd..3d9b5fb 100644
--- a/src/components/icons.tsx
+++ b/src/components/icons.tsx
@@ -17,6 +17,8 @@ import {
Save,
Copy,
Cable,
+ Edit,
+ Edit2,
} from 'lucide-react';
export type Icon = LucideIcon;
@@ -27,6 +29,7 @@ export const Icons = {
connect: Cable,
chevronRight: ChevronRight,
copy: Copy,
+ edit: Edit,
sun: SunMedium,
login: LogIn,
mobile: TabletSmartphone,
diff --git a/src/db/index.ts b/src/db/index.ts
new file mode 100644
index 0000000..936304f
--- /dev/null
+++ b/src/db/index.ts
@@ -0,0 +1,8 @@
+import { drizzle, PostgresJsDatabase } from 'drizzle-orm/postgres-js';
+import postgres from 'postgres';
+import * as schema from './schema';
+
+const client = postgres(process.env.DATABASE_URL as string);
+const db = drizzle(client, { schema });
+
+export default db;
diff --git a/src/db/schema/auth.ts b/src/db/schema.ts
similarity index 54%
rename from src/db/schema/auth.ts
rename to src/db/schema.ts
index 0603d9b..d604992 100644
--- a/src/db/schema/auth.ts
+++ b/src/db/schema.ts
@@ -5,8 +5,12 @@ import {
integer,
pgTable,
uuid,
+ varchar,
} from 'drizzle-orm/pg-core';
import type { AdapterAccount } from '@auth/core/adapters';
+import { relations, sql } from 'drizzle-orm';
+
+//#region auth
export const users = pgTable('user', {
id: uuid('id').notNull().primaryKey(),
@@ -34,7 +38,9 @@ export const accounts = pgTable(
session_state: text('session_state'),
},
(account) => ({
- compoundKey: primaryKey(account.provider, account.providerAccountId),
+ compoundKey: primaryKey({
+ columns: [account.provider, account.providerAccountId],
+ }),
})
);
@@ -54,6 +60,42 @@ export const verificationTokens = pgTable(
expires: timestamp('expires', { mode: 'date' }).notNull(),
},
(vt) => ({
- compoundKey: primaryKey(vt.identifier, vt.token),
+ compoundKey: primaryKey({ columns: [vt.identifier, vt.token] }),
})
);
+//#endregion auth
+
+//#region child
+export const child = pgTable('child', {
+ id: uuid('id')
+ .default(sql`gen_random_uuid()`)
+ .primaryKey(),
+ parentId: uuid('parent_id')
+ .notNull()
+ .references(() => users.id, { onDelete: 'cascade' }),
+ name: varchar('name', { length: 256 }),
+ phone: varchar('phone', { length: 256 }),
+ apiKey: varchar('key', { length: 256 }),
+});
+
+export const childDevices = relations(child, ({ many }) => ({
+ devices: many(device),
+}));
+//#endregion child
+//#region device
+export const device = pgTable(
+ 'device',
+ {
+ childId: uuid('child_id')
+ .notNull()
+ .references(() => child.id, { onDelete: 'cascade' }),
+ pin: integer('pin').notNull(),
+ deviceId: varchar('device_id').notNull().unique(),
+ apiKey: varchar('api_key').notNull().unique(),
+ expires: timestamp('expires').default(sql`now() + interval '1 hour'`),
+ },
+ (device) => ({
+ composePk: primaryKey({ columns: [device.childId, device.deviceId] }),
+ })
+);
+//#endregion device
diff --git a/src/db/schema/child.ts b/src/db/schema/child.ts
deleted file mode 100644
index f34067e..0000000
--- a/src/db/schema/child.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-import {
- integer,
- numeric,
- pgEnum,
- pgTable,
- primaryKey,
- serial,
- text,
- timestamp,
- uniqueIndex,
- uuid,
- varchar,
-} from 'drizzle-orm/pg-core';
-import { relations, sql } from 'drizzle-orm';
-import { users } from './auth';
-
-export const child = pgTable('child', {
- id: uuid('id')
- .default(sql`gen_random_uuid()`)
- .primaryKey(),
- parentId: uuid('parent_id')
- .notNull()
- .references(() => users.id, { onDelete: 'cascade' }),
- name: varchar('name', { length: 256 }),
- phone: varchar('phone', { length: 256 }),
- apiKey: varchar('key', { length: 256 }),
-});
-export const locations = pgTable('locations', {
- id: uuid('uuid1').defaultRandom().primaryKey(),
- longitude: numeric('longitude'),
- latitude: numeric('latitude'),
- userId: uuid('user_id'),
-});
-
-export const childDevices = pgTable(
- 'child_devices',
- {
- childId: uuid('child_id')
- .notNull()
- .references(() => child.id, { onDelete: 'cascade' }),
- pin: integer('pin'),
- apiKey: varchar('api_key'),
- expires: timestamp('expires').default(sql`now() + interval '1 hour'`),
- },
- (childPIN) => ({
- composePk: primaryKey(childPIN.childId, childPIN.pin),
- })
-);
-export const childLocations = relations(child, ({ many }) => ({
- locations: many(locations),
-}));
-export const locationsRelations = relations(locations, ({ one }) => ({
- author: one(child, {
- fields: [locations.userId],
- references: [child.id],
- }),
-}));
diff --git a/src/db/schema/index.ts b/src/db/schema/index.ts
deleted file mode 100644
index 48628b1..0000000
--- a/src/db/schema/index.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { drizzle, PostgresJsDatabase } from "drizzle-orm/postgres-js";
-import postgres from "postgres";
-
-// for query purposes
-const queryClient = postgres(process.env.DATABASE_URL as string);
-const db: PostgresJsDatabase = drizzle(queryClient);
-export default db;
diff --git a/src/lib/services/auth/config.ts b/src/lib/services/auth/config.ts
index be90497..38f565f 100644
--- a/src/lib/services/auth/config.ts
+++ b/src/lib/services/auth/config.ts
@@ -1,7 +1,7 @@
import { getServerSession, type AuthOptions } from 'next-auth';
import GoogleProvider from 'next-auth/providers/google';
import { DrizzleAdapter } from '@auth/drizzle-adapter';
-import db from '@/db/schema';
+import db from '@/db';
const authOptions: AuthOptions = {
adapter: DrizzleAdapter(db),
diff --git a/src/lib/services/auth/provider.tsx b/src/lib/services/auth/provider.tsx
index dffd619..bfda253 100644
--- a/src/lib/services/auth/provider.tsx
+++ b/src/lib/services/auth/provider.tsx
@@ -1,12 +1,12 @@
-'use client'
+'use client';
-import { SessionProvider } from 'next-auth/react'
-import { ReactNode } from 'react'
+import { SessionProvider } from 'next-auth/react';
+import { ReactNode } from 'react';
export default function NextAuthProvider({
children,
}: {
children: ReactNode;
}) {
- return {children}
+ return {children};
}
diff --git a/src/lib/validations/connect-device.ts b/src/lib/validations/connect-device.ts
new file mode 100644
index 0000000..cae18e4
--- /dev/null
+++ b/src/lib/validations/connect-device.ts
@@ -0,0 +1,5 @@
+import * as z from 'zod';
+
+export const connectDeviceSchema = z.object({
+ childId: z.string().max(50),
+});