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), +});