mirror of
https://github.com/fergalmoran/kidarr-server.git
synced 2025-12-22 09:17:51 +00:00
Safe to say these relations are fixed!!
This commit is contained in:
@@ -14,25 +14,29 @@ CREATE TABLE IF NOT EXISTS "account" (
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "child" (
|
||||
"id" uuid PRIMARY KEY NOT NULL,
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"name" varchar(256),
|
||||
"email" varchar(256),
|
||||
"phone" varchar(256),
|
||||
"key" varchar(256),
|
||||
"parent_id" uuid
|
||||
"parent_id" uuid NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "device" (
|
||||
"id" uuid PRIMARY KEY NOT NULL,
|
||||
"child_id" uuid,
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"device_id" varchar NOT NULL,
|
||||
"child_id" uuid NOT NULL,
|
||||
"api_key" varchar NOT NULL,
|
||||
"pin" integer NOT NULL,
|
||||
"expires" timestamp DEFAULT now
|
||||
()
|
||||
+ interval '1 hour',
|
||||
CONSTRAINT "device_device_id_unique" UNIQUE("device_id"),
|
||||
CONSTRAINT "device_api_key_unique" UNIQUE("api_key")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "ping" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"device_id" uuid NOT NULL,
|
||||
"latitude" double precision NOT NULL,
|
||||
"longitude" double precision NOT NULL,
|
||||
@@ -46,7 +50,7 @@ CREATE TABLE IF NOT EXISTS "session" (
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "user" (
|
||||
"id" uuid PRIMARY KEY NOT NULL,
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"name" text,
|
||||
"email" text NOT NULL,
|
||||
"emailVerified" timestamp,
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"id": "876770f8-cae4-4913-b299-855783c0b37c",
|
||||
"id": "7ac914a6-b952-4605-8341-0b7f522e85a5",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"version": "5",
|
||||
"dialect": "pg",
|
||||
@@ -110,7 +110,8 @@
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
@@ -118,6 +119,12 @@
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "varchar(256)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"phone": {
|
||||
"name": "phone",
|
||||
"type": "varchar(256)",
|
||||
@@ -134,7 +141,7 @@
|
||||
"name": "parent_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
@@ -150,13 +157,20 @@
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"device_id": {
|
||||
"name": "device_id",
|
||||
"type": "varchar",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"child_id": {
|
||||
"name": "child_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
"notNull": true
|
||||
},
|
||||
"api_key": {
|
||||
"name": "api_key",
|
||||
@@ -182,6 +196,13 @@
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"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,
|
||||
@@ -195,6 +216,13 @@
|
||||
"name": "ping",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"device_id": {
|
||||
"name": "device_id",
|
||||
"type": "uuid",
|
||||
@@ -275,7 +303,8 @@
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "5",
|
||||
"when": 1703070994335,
|
||||
"tag": "0000_thankful_sasquatch",
|
||||
"when": 1703185817079,
|
||||
"tag": "0000_watery_zaladane",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
|
||||
12
package.json
12
package.json
@@ -5,13 +5,14 @@
|
||||
"scripts": {
|
||||
"dev2": "next dev",
|
||||
"dev": "NODE_ENV=development next dev -p 3002 & local-ssl-proxy --key /etc/letsencrypt/live/dev.fergl.ie/privkey.pem --cert /etc/letsencrypt/live/dev.fergl.ie/fullchain.pem --source 3000 --target 3002",
|
||||
"drizzle:migrate": "node --loader esbuild-register/loader -r esbuild-register ./src/db/migrate.ts",
|
||||
"drizzle:seed": "node --loader esbuild-register/loader -r esbuild-register ./src/db/seed.ts",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@auth/drizzle-adapter": "^0.3.11",
|
||||
"@auth/drizzle-adapter": "^0.3.12",
|
||||
"@hookform/resolvers": "^3.3.2",
|
||||
"@microsoft/signalr": "^8.0.0",
|
||||
"@radix-ui/react-accordion": "^1.1.2",
|
||||
@@ -42,20 +43,21 @@
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@t3-oss/env-core": "^0.7.1",
|
||||
"@t3-oss/env-nextjs": "^0.7.1",
|
||||
"@tanstack/react-query": "^5.14.1",
|
||||
"@tanstack/react-query-devtools": "^5.14.1",
|
||||
"@tanstack/react-query": "^5.14.2",
|
||||
"@tanstack/react-query-devtools": "^5.14.5",
|
||||
"axios": "^1.6.2",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"cmdk": "^0.2.0",
|
||||
"date-fns": "^3.0.0",
|
||||
"date-fns": "^3.0.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"drizzle-orm": "^0.29.1",
|
||||
"esbuild-register": "^3.5.0",
|
||||
"generate-api-key": "^1.0.2",
|
||||
"http-status-codes": "^2.3.0",
|
||||
"leaflet": "^1.9.4",
|
||||
"local-ssl-proxy": "^2.0.5",
|
||||
"lucide-react": "^0.298.0",
|
||||
"lucide-react": "^0.299.0",
|
||||
"next": "14.0.4",
|
||||
"next-auth": "^4.24.5",
|
||||
"next-themes": "^0.2.1",
|
||||
|
||||
@@ -3,7 +3,6 @@ export PGUSER=postgres
|
||||
export PGPASSWORD=hackme
|
||||
export PGHOST=localhost
|
||||
|
||||
|
||||
echo Removing migrations
|
||||
rm -rf drizzle
|
||||
echo "Dropping db"
|
||||
@@ -11,9 +10,9 @@ echo "Dropping db"
|
||||
dropdb -f --if-exists parentgrine
|
||||
echo "Creating db"
|
||||
createdb parentgrine
|
||||
echo "Exiting"
|
||||
exit
|
||||
|
||||
bunx drizzle-kit generate:pg --config=./drizzle.config.ts
|
||||
bunx drizzle-kit push:pg --config=./drizzle.config.ts
|
||||
bunx tsx src/db/migrate.ts
|
||||
|
||||
# bun run src/db/migrate.ts
|
||||
bun run ./src/db/scripts/seed.ts
|
||||
|
||||
@@ -15,7 +15,9 @@ export async function GET(request: Request) {
|
||||
const activeChildren = await db.query.child.findMany({
|
||||
where: eq(child.parentId, session.user.id),
|
||||
with: {
|
||||
devices: true,
|
||||
devices: {
|
||||
with: { pings: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ const POST = async (req: Request, res: Response) => {
|
||||
.execute();
|
||||
return Response.json(
|
||||
{ childId, deviceId, pin, apiKey },
|
||||
{ status: StatusCodes.CREATED },
|
||||
{ status: StatusCodes.CREATED }
|
||||
);
|
||||
}
|
||||
return badRequest('Invalid registration request');
|
||||
|
||||
@@ -15,6 +15,7 @@ import { Icons } from '../icons';
|
||||
|
||||
const AuthHeader = () => {
|
||||
const { data: session, status } = useSession();
|
||||
if (status === 'loading') return null;
|
||||
return !session ? (
|
||||
<Link
|
||||
className={cn(buttonVariants({ variant: 'default', size: 'sm' }))}
|
||||
|
||||
@@ -12,13 +12,16 @@ interface MainNavProps {
|
||||
|
||||
export function MainNav({ items }: MainNavProps) {
|
||||
return (
|
||||
<div className='flex gap-6 md:gap-10'>
|
||||
<Link href='/' className='flex items-center space-x-2'>
|
||||
<Icons.logo className='h-6 w-6' />
|
||||
<span className='inline-block font-bold'>{siteConfig.name}</span>
|
||||
<div className="flex gap-6 md:gap-10">
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-center space-x-2"
|
||||
>
|
||||
<Icons.logo className="h-6 w-6" />
|
||||
<span className="inline-block font-bold">{siteConfig.name}</span>
|
||||
</Link>
|
||||
{items?.length ? (
|
||||
<nav className='flex gap-6'>
|
||||
<nav className="flex gap-6">
|
||||
{items?.map(
|
||||
(item, index) =>
|
||||
item.href && (
|
||||
@@ -27,12 +30,12 @@ export function MainNav({ items }: MainNavProps) {
|
||||
href={item.href}
|
||||
className={cn(
|
||||
'flex items-center text-sm font-medium text-muted-foreground',
|
||||
item.disabled && 'cursor-not-allowed opacity-80',
|
||||
item.disabled && 'cursor-not-allowed opacity-80'
|
||||
)}
|
||||
>
|
||||
{item.title}
|
||||
</Link>
|
||||
),
|
||||
)
|
||||
)}
|
||||
</nav>
|
||||
) : null}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { drizzle } from 'drizzle-orm/postgres-js';
|
||||
import postgres from 'postgres';
|
||||
import * as schema from './schema';
|
||||
import { env } from '@/env.mjs';
|
||||
import { migrate } from 'drizzle-orm/postgres-js/migrator';
|
||||
|
||||
const client = postgres(env.DATABASE_URL as string);
|
||||
const db = drizzle(client, { schema });
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import 'dotenv/config';
|
||||
import db from '.';
|
||||
import { migrate } from 'drizzle-orm/postgres-js/migrator';
|
||||
|
||||
// This will run migrations on the database, skipping the ones already applied
|
||||
// migrate(db, { migrationsFolder: './drizzle' }).then(() =>
|
||||
// console.log('DRIZZLE', 'migrated')
|
||||
// );
|
||||
@@ -16,7 +16,10 @@ import { relations, sql } from 'drizzle-orm';
|
||||
//TODO: use this schema once https://github.com/drizzle-team/drizzle-orm/issues/636 is fixed
|
||||
const authSchema = pgSchema('auth');
|
||||
export const users = pgTable('user', {
|
||||
id: uuid('id').notNull().primaryKey(),
|
||||
id: uuid('id')
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.default(sql`gen_random_uuid()`),
|
||||
name: text('name'),
|
||||
email: text('email').notNull(),
|
||||
emailVerified: timestamp('emailVerified', { mode: 'date' }),
|
||||
@@ -74,11 +77,14 @@ export const userRelations = relations(users, ({ many }) => ({
|
||||
|
||||
//#region child
|
||||
export const child = pgTable('child', {
|
||||
id: uuid('id').primaryKey(),
|
||||
id: uuid('id')
|
||||
.primaryKey()
|
||||
.default(sql`gen_random_uuid()`),
|
||||
name: varchar('name', { length: 256 }),
|
||||
email: varchar('email', { length: 256 }),
|
||||
phone: varchar('phone', { length: 256 }),
|
||||
apiKey: varchar('key', { length: 256 }),
|
||||
parentId: uuid('parent_id'),
|
||||
parentId: uuid('parent_id').notNull(),
|
||||
});
|
||||
export const childRelations = relations(child, ({ one, many }) => ({
|
||||
parent: one(users, {
|
||||
@@ -88,19 +94,19 @@ export const childRelations = relations(child, ({ one, many }) => ({
|
||||
devices: many(device),
|
||||
}));
|
||||
|
||||
export const device = pgTable(
|
||||
'device',
|
||||
{
|
||||
id: uuid('id').primaryKey(),
|
||||
childId: uuid('child_id'),
|
||||
apiKey: varchar('api_key').notNull().unique(),
|
||||
//TODO: make the device request/pin a separate table and enforce the expiry
|
||||
pin: integer('pin').notNull(),
|
||||
expires: timestamp('expires').default(sql`now
|
||||
export const device = pgTable('device', {
|
||||
id: uuid('id')
|
||||
.primaryKey()
|
||||
.default(sql`gen_random_uuid()`),
|
||||
deviceId: varchar('device_id').notNull().unique(),
|
||||
childId: uuid('child_id').notNull(),
|
||||
apiKey: varchar('api_key').notNull().unique(),
|
||||
//TODO: make the device request/pin a separate table and enforce the expiry
|
||||
pin: integer('pin').notNull(),
|
||||
expires: timestamp('expires').default(sql`now
|
||||
()
|
||||
+ interval '1 hour'`),
|
||||
}
|
||||
);
|
||||
});
|
||||
export const deviceRelations = relations(device, ({ one, many }) => ({
|
||||
child: one(child, {
|
||||
fields: [device.childId],
|
||||
@@ -110,13 +116,16 @@ export const deviceRelations = relations(device, ({ one, many }) => ({
|
||||
}));
|
||||
|
||||
export const ping = pgTable('ping', {
|
||||
id: uuid('id')
|
||||
.primaryKey()
|
||||
.default(sql`gen_random_uuid()`),
|
||||
deviceId: uuid('device_id').notNull(),
|
||||
latitude: doublePrecision('latitude').notNull(),
|
||||
longitude: doublePrecision('longitude').notNull(),
|
||||
timestamp: timestamp('timestamp').notNull(),
|
||||
});
|
||||
export const pingRelations = relations(ping, ({ one, many }) => ({
|
||||
child: one(device, {
|
||||
device: one(device, {
|
||||
fields: [ping.deviceId],
|
||||
references: [device.id],
|
||||
}),
|
||||
|
||||
33
src/db/scripts/auth.ts
Normal file
33
src/db/scripts/auth.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import * as dotenv from 'dotenv';
|
||||
import * as schema from '@/db/schema';
|
||||
import db from '..';
|
||||
|
||||
const main = async () => {
|
||||
db.insert(schema.accounts)
|
||||
.values([
|
||||
{
|
||||
userId: '2250f34e-997a-44de-ab8d-beddeda13525',
|
||||
provider: 'google',
|
||||
type: 'oauth',
|
||||
providerAccountId: '112561477626832751929',
|
||||
access_token: 'FARTS',
|
||||
expires_at: 9,
|
||||
token_type: 'Bearer',
|
||||
scope:
|
||||
'openid https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile',
|
||||
id_token: 'FARTS',
|
||||
},
|
||||
])
|
||||
.execute();
|
||||
return;
|
||||
};
|
||||
|
||||
console.log('auth', 'Seeding auth');
|
||||
main().then(
|
||||
() => {
|
||||
console.log('auth', 'Seeded auth');
|
||||
},
|
||||
(err) => {
|
||||
console.error('auth', 'Error seeding auth', err);
|
||||
}
|
||||
);
|
||||
51
src/db/scripts/seed.ts
Normal file
51
src/db/scripts/seed.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import * as dotenv from 'dotenv';
|
||||
import * as schema from '@/db/schema';
|
||||
import db from '..';
|
||||
// dotenv.config({ path: './.env' });
|
||||
|
||||
// if (!('DATABASE_URL' in process.env))
|
||||
// throw new Error('DATABASE_URL not found on .env.development');
|
||||
const main = async () => {
|
||||
const seedUsers = [
|
||||
{
|
||||
id: '2250f34e-997a-44de-ab8d-beddeda13525',
|
||||
name: 'Fergal Moran',
|
||||
email: 'fergal.moran@gmail.com',
|
||||
emailVerified: new Date(),
|
||||
},
|
||||
];
|
||||
db.insert(schema.users).values(seedUsers).execute();
|
||||
|
||||
const seedChildren = [
|
||||
{
|
||||
id: '2250f34e-997a-44de-ab8d-beddeda13525',
|
||||
name: 'Lil Debuggles',
|
||||
phone: '123 456 789',
|
||||
email: 'lildebuggles@parentgrine.com',
|
||||
parentId: seedUsers[0].id,
|
||||
},
|
||||
];
|
||||
db.insert(schema.child).values(seedChildren).execute();
|
||||
|
||||
const seedDevices = [
|
||||
{
|
||||
deviceId: '373791e3-afe3-49de-b0a2-842a44071585',
|
||||
childId: seedChildren[0].id,
|
||||
apiKey: 'nQhXtqemsWjzBpbDxlIV2qtDx9xxO4oZVBJADdhJLfA=',
|
||||
pin: 1234,
|
||||
expires: new Date(2065),
|
||||
},
|
||||
];
|
||||
db.insert(schema.device).values(seedDevices).execute();
|
||||
return;
|
||||
};
|
||||
|
||||
console.log('seed', 'Seeding');
|
||||
main().then(
|
||||
() => {
|
||||
console.log('seed', 'Seeded');
|
||||
},
|
||||
(err) => {
|
||||
console.log('seed', 'Error', err);
|
||||
}
|
||||
);
|
||||
@@ -1,5 +0,0 @@
|
||||
import * as dotenv from 'dotenv';
|
||||
dotenv.config({ path: './.env' });
|
||||
|
||||
if (!('DATABASE_URL' in process.env))
|
||||
throw new Error('DATABASE_URL not found on .env.development');
|
||||
@@ -1,16 +1,19 @@
|
||||
import db from '@/db';
|
||||
import { StatusCodes } from 'http-status-codes';
|
||||
import { ping } from '@/db/schema';
|
||||
import {
|
||||
NextApiResponseServerIo,
|
||||
} from '@/lib/models/types/next-api-response-socket';
|
||||
import { NextApiResponseServerIo } from '@/lib/models/types/next-api-response-socket';
|
||||
import { badRequest, notAuthorised } from '@/app/api/responses';
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import LocationUpdate from '@/lib/models/location-update';
|
||||
|
||||
export default async function POST(req: NextApiRequest, res: NextApiResponseServerIo) {
|
||||
export default async function POST(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponseServerIo
|
||||
) {
|
||||
if (req.method !== 'POST') {
|
||||
return res.status(StatusCodes.METHOD_NOT_ALLOWED).send({ message: 'Method not allowed' });
|
||||
return res
|
||||
.status(StatusCodes.METHOD_NOT_ALLOWED)
|
||||
.send({ message: 'Method not allowed' });
|
||||
}
|
||||
const apiKey = req?.headers['x-api-key'] as string;
|
||||
const deviceId = req?.headers['x-device-id'] as string;
|
||||
@@ -19,10 +22,8 @@ export default async function POST(req: NextApiRequest, res: NextApiResponseServ
|
||||
}
|
||||
|
||||
const device = await db.query.device.findFirst({
|
||||
where: (device, {
|
||||
and,
|
||||
eq,
|
||||
}) => and(eq(device.deviceId, deviceId), eq(device.apiKey, apiKey)),
|
||||
where: (device, { and, eq }) =>
|
||||
and(eq(device.deviceId, deviceId), eq(device.apiKey, apiKey)),
|
||||
});
|
||||
|
||||
if (!device) {
|
||||
@@ -48,7 +49,7 @@ export default async function POST(req: NextApiRequest, res: NextApiResponseServ
|
||||
}
|
||||
|
||||
const location = await db.insert(ping).values({
|
||||
deviceId: device.deviceId,
|
||||
deviceId: device.id,
|
||||
latitude: coordinates.latitude,
|
||||
longitude: coordinates.longitude,
|
||||
timestamp: new Date(),
|
||||
@@ -66,5 +67,4 @@ export default async function POST(req: NextApiRequest, res: NextApiResponseServ
|
||||
res?.socket?.server?.io?.emit(`ping:${child.id}`, update);
|
||||
// Send a response
|
||||
return res.json({});
|
||||
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
"drizzle.config.js"
|
||||
],
|
||||
, ".krud/seed.ts", "src/db/___migrate.ts___" ],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
|
||||
3
types/next-auth.d.ts
vendored
3
types/next-auth.d.ts
vendored
@@ -7,6 +7,9 @@ declare module 'next-auth' {
|
||||
interface Session {
|
||||
user: {
|
||||
id: string;
|
||||
name: string;
|
||||
image: string;
|
||||
email: string;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user