mirror of
https://github.com/fergalmoran/kidarr-server.git
synced 2026-02-18 22:04:19 +00:00
Child and device onboarding working
This commit is contained in:
28
.vscode/launch.json
vendored
Normal file
28
.vscode/launch.json
vendored
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
16
.vscode/settings.json
vendored
16
.vscode/settings.json
vendored
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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 $$;
|
||||
@@ -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": {},
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "5",
|
||||
"when": 1699562330825,
|
||||
"tag": "0000_bizarre_firebrand",
|
||||
"when": 1700413726993,
|
||||
"tag": "0000_medical_jetstream",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
|
||||
19
package.json
19
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"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ export PGUSER=postgres
|
||||
export PGPASSWORD=hackme
|
||||
export PGHOST=localhost
|
||||
|
||||
|
||||
echo Removing migrations
|
||||
rm -rf drizzle
|
||||
echo "Dropping db"
|
||||
|
||||
@@ -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' });
|
||||
}
|
||||
|
||||
41
src/app/api/child/ping/route.ts
Normal file
41
src/app/api/child/ping/route.ts
Normal file
@@ -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',
|
||||
});
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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 }
|
||||
|
||||
34
src/app/api/device/ping/route.ts
Normal file
34
src/app/api/device/ping/route.ts
Normal file
@@ -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 };
|
||||
10
src/app/api/responses/index.ts
Normal file
10
src/app/api/responses/index.ts
Normal file
@@ -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,
|
||||
});
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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 = () => {
|
||||
<TableCaption>Here are your children.</TableCaption>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[100px]">Name</TableHead>
|
||||
<TableHead className="">Name</TableHead>
|
||||
<TableHead>Last seen at</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
<TableHead className="">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -45,7 +45,12 @@ const ChildrenList = () => {
|
||||
<TableCell className="font-medium">{child.name}</TableCell>
|
||||
<TableCell>Douglas</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<ConnectDeviceDialog child={child} />
|
||||
<div className="space-x-1">
|
||||
<ConnectDeviceDialog child={child} />
|
||||
<Button>
|
||||
<Icons.edit className="mr-2 h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
|
||||
@@ -37,7 +37,7 @@ const ConnectDeviceDialog: React.FC<ConnectDeviceDialogProps> = ({ child }) => {
|
||||
<QRCode
|
||||
size={190}
|
||||
style={{ height: 'auto', maxWidth: '100%', width: '100%' }}
|
||||
value={`${process.env.NEXT_PUBLIC_BASE_URL}/device/connect?childId=${child.id}`}
|
||||
value={child.id}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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,
|
||||
|
||||
8
src/db/index.ts
Normal file
8
src/db/index.ts
Normal file
@@ -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;
|
||||
@@ -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
|
||||
@@ -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],
|
||||
}),
|
||||
}));
|
||||
@@ -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;
|
||||
@@ -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),
|
||||
|
||||
@@ -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 <SessionProvider>{children}</SessionProvider>
|
||||
return <SessionProvider>{children}</SessionProvider>;
|
||||
}
|
||||
|
||||
5
src/lib/validations/connect-device.ts
Normal file
5
src/lib/validations/connect-device.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import * as z from 'zod';
|
||||
|
||||
export const connectDeviceSchema = z.object({
|
||||
childId: z.string().max(50),
|
||||
});
|
||||
Reference in New Issue
Block a user