* Wip

* Add drizzle support

* Fix build

* fix migrations

* update dockerfiles

* update stuff

* simplify

* fix migrations

* remove migration code

* fix migrations

* update script

* wip

* bump script

* wip

* fix

* bump

* copy public

* wip

* wip
This commit is contained in:
Bill Yang 2025-03-05 12:29:30 -08:00 committed by GitHub
parent 8d6762d37d
commit 2b5c677933
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 3583 additions and 206 deletions

View file

@ -13,7 +13,7 @@ export function Header() {
const pathname = usePathname(); const pathname = usePathname();
const site = sites?.data?.find( const site = sites?.data?.find(
(site) => site.site_id === Number(pathname.split("/")[1]) (site) => site.siteId === Number(pathname.split("/")[1])
); );
// Check which tab is active based on the current path // Check which tab is active based on the current path

View file

@ -27,7 +27,7 @@ export function NoData({
</div> </div>
<CodeSnippet <CodeSnippet
language="HTML" language="HTML"
code={`<script\n src="${BACKEND_URL}/script.js"\n site-id="${siteMetadata?.site_id}"\n defer\n/>`} code={`<script\n src="${BACKEND_URL}/script.js"\n site-id="${siteMetadata?.siteId}"\n defer\n/>`}
/> />
</div> </div>
</CardContent> </CardContent>

View file

@ -41,7 +41,7 @@ export function SubHeader() {
const pathname = usePathname(); const pathname = usePathname();
const site = sites?.data?.find( const site = sites?.data?.find(
(site) => site.site_id === Number(pathname.slice(1)) (site) => site.siteId === Number(pathname.slice(1))
); );
return ( return (

View file

@ -25,7 +25,7 @@ export function DeleteSite({
isOpen={isOpen} isOpen={isOpen}
setIsOpen={setIsOpen} setIsOpen={setIsOpen}
onConfirm={async () => { onConfirm={async () => {
await deleteSite(site.site_id); await deleteSite(site.siteId);
refetch(); refetch();
router.push("/"); router.push("/");
}} }}

View file

@ -13,7 +13,7 @@ export default function SettingsPage() {
const pathname = usePathname(); const pathname = usePathname();
const site = sites?.data?.find( const site = sites?.data?.find(
(site) => site.site_id === Number(pathname.split("/")[1]) (site) => site.siteId === Number(pathname.split("/")[1])
); );
return ( return (

View file

@ -15,7 +15,7 @@ export default function Home() {
</div> </div>
<div className="grid grid-cols-3 gap-4 mt-4"> <div className="grid grid-cols-3 gap-4 mt-4">
{sites?.data?.map((site) => ( {sites?.data?.map((site) => (
<Link href={`/${site.site_id}`} key={site.site_id}> <Link href={`/${site.siteId}`} key={site.siteId}>
<div className="flex p-4 rounded-lg bg-neutral-900 text-lg font-semibold gap-2"> <div className="flex p-4 rounded-lg bg-neutral-900 text-lg font-semibold gap-2">
<img <img
className="w-6 mr-1" className="w-6 mr-1"

View file

@ -178,12 +178,12 @@ export function useGetOverview(periodTime?: PeriodTime) {
} }
export type GetSitesResponse = { export type GetSitesResponse = {
site_id: number; siteId: number;
site_name: string; name: string;
domain: string; domain: string;
created_at: string; createdAt: string;
updated_at: string; updatedAt: string;
created_by: string; createdBy: string;
}[]; }[];
export function useGetSites() { export function useGetSites() {

View file

@ -3,7 +3,7 @@ import { useGetSites } from "./api";
export function useGetSiteMetadata(siteId: string) { export function useGetSiteMetadata(siteId: string) {
const { data, isLoading } = useGetSites(); const { data, isLoading } = useGetSites();
return { return {
siteMetadata: data?.data?.find((site) => site.site_id === Number(siteId)), siteMetadata: data?.data?.find((site) => site.siteId === Number(siteId)),
isLoading, isLoading,
}; };
} }

View file

@ -1,14 +1,46 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
FROM node:22-alpine FROM node:20-alpine AS builder
WORKDIR /app WORKDIR /app
COPY ["package.json", "package-lock.json*", "./"] # Install dependencies
COPY package*.json ./
RUN npm install RUN npm ci
# Copy source code
COPY . . COPY . .
# Build the application
RUN npm run build RUN npm run build
CMD [ "npm", "start" ] # Generate migrations (but don't run them)
RUN mkdir -p /app/drizzle
RUN npx drizzle-kit generate || echo "Skipping migration generation during build"
# Runtime image
FROM node:20-alpine
WORKDIR /app
# Install PostgreSQL client for migrations
RUN apk add --no-cache postgresql-client
# Copy built application and dependencies
COPY --from=builder /app/package*.json ./
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/drizzle ./drizzle
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/docker-entrypoint.sh /docker-entrypoint.sh
COPY --from=builder /app/drizzle.config.ts ./drizzle.config.ts
COPY --from=builder /app/public ./public
# Make the entrypoint executable
RUN chmod +x /docker-entrypoint.sh
# Expose the API port
EXPOSE 3001
# Use our custom entrypoint script
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["node", "dist/index.js"]

View file

@ -1,7 +0,0 @@
create table "user" ("id" text not null primary key, "name" text not null, "email" text not null unique, "emailVerified" boolean not null, "image" text, "createdAt" timestamp not null, "updatedAt" timestamp not null);
create table "session" ("id" text not null primary key, "expiresAt" timestamp not null, "token" text not null unique, "createdAt" timestamp not null, "updatedAt" timestamp not null, "ipAddress" text, "userAgent" text, "userId" text not null references "user" ("id"));
create table "account" ("id" text not null primary key, "accountId" text not null, "providerId" text not null, "userId" text not null references "user" ("id"), "accessToken" text, "refreshToken" text, "idToken" text, "accessTokenExpiresAt" timestamp, "refreshTokenExpiresAt" timestamp, "scope" text, "password" text, "createdAt" timestamp not null, "updatedAt" timestamp not null);
create table "verification" ("id" text not null primary key, "identifier" text not null, "value" text not null, "expiresAt" timestamp not null, "createdAt" timestamp, "updatedAt" timestamp)

View file

@ -0,0 +1,13 @@
#!/bin/sh
set -e
# Docker Compose already ensures services are ready using healthchecks
# and dependency conditions in the docker-compose.yml file
# Run migrations explicitly using the npm script
echo "Running database migrations...."
# npm run db:migrate
# Start the application
echo "Starting application..."
exec "$@"

18
server/drizzle.config.ts Normal file
View file

@ -0,0 +1,18 @@
import "dotenv/config";
import { defineConfig } from "drizzle-kit";
export default defineConfig({
schema: "./src/db/postgres/schema.ts",
out: "./drizzle",
dialect: "postgresql",
dbCredentials: {
host: "postgres",
port: 5432,
database: "analytics",
user: "frog",
password: "frog",
ssl: false,
},
verbose: true,
strict: true,
});

View file

@ -0,0 +1,109 @@
CREATE TABLE "account" (
"id" text PRIMARY KEY NOT NULL,
"accountId" text NOT NULL,
"providerId" text NOT NULL,
"userId" text NOT NULL,
"accessToken" text,
"refreshToken" text,
"idToken" text,
"accessTokenExpiresAt" timestamp,
"refreshTokenExpiresAt" timestamp,
"scope" text,
"password" text,
"createdAt" timestamp NOT NULL,
"updatedAt" timestamp NOT NULL
);
--> statement-breakpoint
CREATE TABLE "active_sessions" (
"session_id" text PRIMARY KEY NOT NULL,
"site_id" integer,
"user_id" text,
"hostname" text,
"start_time" timestamp DEFAULT now(),
"last_activity" timestamp DEFAULT now(),
"pageviews" integer DEFAULT 0,
"entry_page" text,
"exit_page" text,
"device_type" text,
"screen_width" integer,
"screen_height" integer,
"browser" text,
"operating_system" text,
"language" text,
"referrer" text
);
--> statement-breakpoint
CREATE TABLE "member" (
"id" text PRIMARY KEY NOT NULL,
"organizationId" text NOT NULL,
"userId" text NOT NULL,
"role" text NOT NULL,
"createdAt" timestamp NOT NULL
);
--> statement-breakpoint
CREATE TABLE "organization" (
"id" text PRIMARY KEY NOT NULL,
"name" text NOT NULL,
"slug" text NOT NULL,
"logo" text,
"createdAt" timestamp NOT NULL,
"metadata" text,
CONSTRAINT "organization_slug_unique" UNIQUE("slug")
);
--> statement-breakpoint
CREATE TABLE "session" (
"id" text PRIMARY KEY NOT NULL,
"expiresAt" timestamp NOT NULL,
"token" text NOT NULL,
"createdAt" timestamp NOT NULL,
"updatedAt" timestamp NOT NULL,
"ipAddress" text,
"userAgent" text,
"userId" text NOT NULL,
"impersonatedBy" text,
"activeOrganizationId" text,
CONSTRAINT "session_token_unique" UNIQUE("token")
);
--> statement-breakpoint
CREATE TABLE "sites" (
"site_id" serial PRIMARY KEY NOT NULL,
"name" text NOT NULL,
"domain" text NOT NULL,
"created_at" timestamp DEFAULT now(),
"updated_at" timestamp DEFAULT now(),
"created_by" text NOT NULL,
CONSTRAINT "sites_domain_unique" UNIQUE("domain")
);
--> statement-breakpoint
CREATE TABLE "user" (
"id" text PRIMARY KEY NOT NULL,
"name" text NOT NULL,
"username" text NOT NULL,
"email" text NOT NULL,
"emailVerified" boolean NOT NULL,
"image" text,
"createdAt" timestamp NOT NULL,
"updatedAt" timestamp NOT NULL,
"role" text DEFAULT 'user' NOT NULL,
"displayUsername" text,
"banned" boolean,
"banReason" text,
"banExpires" timestamp,
CONSTRAINT "user_username_unique" UNIQUE("username"),
CONSTRAINT "user_email_unique" UNIQUE("email")
);
--> statement-breakpoint
CREATE TABLE "verification" (
"id" text PRIMARY KEY NOT NULL,
"identifier" text NOT NULL,
"value" text NOT NULL,
"expiresAt" timestamp NOT NULL,
"createdAt" timestamp,
"updatedAt" timestamp
);
--> statement-breakpoint
ALTER TABLE "account" ADD CONSTRAINT "account_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "member" ADD CONSTRAINT "member_organizationId_organization_id_fk" FOREIGN KEY ("organizationId") REFERENCES "public"."organization"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "member" ADD CONSTRAINT "member_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "session" ADD CONSTRAINT "session_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "sites" ADD CONSTRAINT "sites_created_by_user_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."user"("id") ON DELETE no action ON UPDATE no action;

View file

@ -0,0 +1,12 @@
CREATE TABLE "reports" (
"report_id" serial PRIMARY KEY NOT NULL,
"site_id" integer,
"user_id" text,
"report_type" text,
"data" jsonb,
"created_at" timestamp DEFAULT now(),
"updated_at" timestamp DEFAULT now()
);
--> statement-breakpoint
ALTER TABLE "reports" ADD CONSTRAINT "reports_site_id_sites_site_id_fk" FOREIGN KEY ("site_id") REFERENCES "public"."sites"("site_id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "reports" ADD CONSTRAINT "reports_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE no action ON UPDATE no action;

View file

@ -0,0 +1,687 @@
{
"id": "a1455965-aa57-4090-83fa-c3863fbdca3d",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.account": {
"name": "account",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"accountId": {
"name": "accountId",
"type": "text",
"primaryKey": false,
"notNull": true
},
"providerId": {
"name": "providerId",
"type": "text",
"primaryKey": false,
"notNull": true
},
"userId": {
"name": "userId",
"type": "text",
"primaryKey": false,
"notNull": true
},
"accessToken": {
"name": "accessToken",
"type": "text",
"primaryKey": false,
"notNull": false
},
"refreshToken": {
"name": "refreshToken",
"type": "text",
"primaryKey": false,
"notNull": false
},
"idToken": {
"name": "idToken",
"type": "text",
"primaryKey": false,
"notNull": false
},
"accessTokenExpiresAt": {
"name": "accessTokenExpiresAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"refreshTokenExpiresAt": {
"name": "refreshTokenExpiresAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"scope": {
"name": "scope",
"type": "text",
"primaryKey": false,
"notNull": false
},
"password": {
"name": "password",
"type": "text",
"primaryKey": false,
"notNull": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"account_userId_user_id_fk": {
"name": "account_userId_user_id_fk",
"tableFrom": "account",
"tableTo": "user",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.active_sessions": {
"name": "active_sessions",
"schema": "",
"columns": {
"session_id": {
"name": "session_id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"site_id": {
"name": "site_id",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"hostname": {
"name": "hostname",
"type": "text",
"primaryKey": false,
"notNull": false
},
"start_time": {
"name": "start_time",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"last_activity": {
"name": "last_activity",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"pageviews": {
"name": "pageviews",
"type": "integer",
"primaryKey": false,
"notNull": false,
"default": 0
},
"entry_page": {
"name": "entry_page",
"type": "text",
"primaryKey": false,
"notNull": false
},
"exit_page": {
"name": "exit_page",
"type": "text",
"primaryKey": false,
"notNull": false
},
"device_type": {
"name": "device_type",
"type": "text",
"primaryKey": false,
"notNull": false
},
"screen_width": {
"name": "screen_width",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"screen_height": {
"name": "screen_height",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"browser": {
"name": "browser",
"type": "text",
"primaryKey": false,
"notNull": false
},
"operating_system": {
"name": "operating_system",
"type": "text",
"primaryKey": false,
"notNull": false
},
"language": {
"name": "language",
"type": "text",
"primaryKey": false,
"notNull": false
},
"referrer": {
"name": "referrer",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.member": {
"name": "member",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"organizationId": {
"name": "organizationId",
"type": "text",
"primaryKey": false,
"notNull": true
},
"userId": {
"name": "userId",
"type": "text",
"primaryKey": false,
"notNull": true
},
"role": {
"name": "role",
"type": "text",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"member_organizationId_organization_id_fk": {
"name": "member_organizationId_organization_id_fk",
"tableFrom": "member",
"tableTo": "organization",
"columnsFrom": [
"organizationId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"member_userId_user_id_fk": {
"name": "member_userId_user_id_fk",
"tableFrom": "member",
"tableTo": "user",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.organization": {
"name": "organization",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"slug": {
"name": "slug",
"type": "text",
"primaryKey": false,
"notNull": true
},
"logo": {
"name": "logo",
"type": "text",
"primaryKey": false,
"notNull": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"metadata": {
"name": "metadata",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"organization_slug_unique": {
"name": "organization_slug_unique",
"nullsNotDistinct": false,
"columns": [
"slug"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.session": {
"name": "session",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"expiresAt": {
"name": "expiresAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"token": {
"name": "token",
"type": "text",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"ipAddress": {
"name": "ipAddress",
"type": "text",
"primaryKey": false,
"notNull": false
},
"userAgent": {
"name": "userAgent",
"type": "text",
"primaryKey": false,
"notNull": false
},
"userId": {
"name": "userId",
"type": "text",
"primaryKey": false,
"notNull": true
},
"impersonatedBy": {
"name": "impersonatedBy",
"type": "text",
"primaryKey": false,
"notNull": false
},
"activeOrganizationId": {
"name": "activeOrganizationId",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"session_userId_user_id_fk": {
"name": "session_userId_user_id_fk",
"tableFrom": "session",
"tableTo": "user",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"session_token_unique": {
"name": "session_token_unique",
"nullsNotDistinct": false,
"columns": [
"token"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.sites": {
"name": "sites",
"schema": "",
"columns": {
"site_id": {
"name": "site_id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"domain": {
"name": "domain",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"created_by": {
"name": "created_by",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"sites_created_by_user_id_fk": {
"name": "sites_created_by_user_id_fk",
"tableFrom": "sites",
"tableTo": "user",
"columnsFrom": [
"created_by"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"sites_domain_unique": {
"name": "sites_domain_unique",
"nullsNotDistinct": false,
"columns": [
"domain"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"emailVerified": {
"name": "emailVerified",
"type": "boolean",
"primaryKey": false,
"notNull": true
},
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"role": {
"name": "role",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'user'"
},
"displayUsername": {
"name": "displayUsername",
"type": "text",
"primaryKey": false,
"notNull": false
},
"banned": {
"name": "banned",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"banReason": {
"name": "banReason",
"type": "text",
"primaryKey": false,
"notNull": false
},
"banExpires": {
"name": "banExpires",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"user_username_unique": {
"name": "user_username_unique",
"nullsNotDistinct": false,
"columns": [
"username"
]
},
"user_email_unique": {
"name": "user_email_unique",
"nullsNotDistinct": false,
"columns": [
"email"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.verification": {
"name": "verification",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"identifier": {
"name": "identifier",
"type": "text",
"primaryKey": false,
"notNull": true
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true
},
"expiresAt": {
"name": "expiresAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View file

@ -0,0 +1,771 @@
{
"id": "b13c246a-71be-4b88-9672-eb0943b3fe19",
"prevId": "a1455965-aa57-4090-83fa-c3863fbdca3d",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.account": {
"name": "account",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"accountId": {
"name": "accountId",
"type": "text",
"primaryKey": false,
"notNull": true
},
"providerId": {
"name": "providerId",
"type": "text",
"primaryKey": false,
"notNull": true
},
"userId": {
"name": "userId",
"type": "text",
"primaryKey": false,
"notNull": true
},
"accessToken": {
"name": "accessToken",
"type": "text",
"primaryKey": false,
"notNull": false
},
"refreshToken": {
"name": "refreshToken",
"type": "text",
"primaryKey": false,
"notNull": false
},
"idToken": {
"name": "idToken",
"type": "text",
"primaryKey": false,
"notNull": false
},
"accessTokenExpiresAt": {
"name": "accessTokenExpiresAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"refreshTokenExpiresAt": {
"name": "refreshTokenExpiresAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"scope": {
"name": "scope",
"type": "text",
"primaryKey": false,
"notNull": false
},
"password": {
"name": "password",
"type": "text",
"primaryKey": false,
"notNull": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"account_userId_user_id_fk": {
"name": "account_userId_user_id_fk",
"tableFrom": "account",
"tableTo": "user",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.active_sessions": {
"name": "active_sessions",
"schema": "",
"columns": {
"session_id": {
"name": "session_id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"site_id": {
"name": "site_id",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"hostname": {
"name": "hostname",
"type": "text",
"primaryKey": false,
"notNull": false
},
"start_time": {
"name": "start_time",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"last_activity": {
"name": "last_activity",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"pageviews": {
"name": "pageviews",
"type": "integer",
"primaryKey": false,
"notNull": false,
"default": 0
},
"entry_page": {
"name": "entry_page",
"type": "text",
"primaryKey": false,
"notNull": false
},
"exit_page": {
"name": "exit_page",
"type": "text",
"primaryKey": false,
"notNull": false
},
"device_type": {
"name": "device_type",
"type": "text",
"primaryKey": false,
"notNull": false
},
"screen_width": {
"name": "screen_width",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"screen_height": {
"name": "screen_height",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"browser": {
"name": "browser",
"type": "text",
"primaryKey": false,
"notNull": false
},
"operating_system": {
"name": "operating_system",
"type": "text",
"primaryKey": false,
"notNull": false
},
"language": {
"name": "language",
"type": "text",
"primaryKey": false,
"notNull": false
},
"referrer": {
"name": "referrer",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.member": {
"name": "member",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"organizationId": {
"name": "organizationId",
"type": "text",
"primaryKey": false,
"notNull": true
},
"userId": {
"name": "userId",
"type": "text",
"primaryKey": false,
"notNull": true
},
"role": {
"name": "role",
"type": "text",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"member_organizationId_organization_id_fk": {
"name": "member_organizationId_organization_id_fk",
"tableFrom": "member",
"tableTo": "organization",
"columnsFrom": [
"organizationId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"member_userId_user_id_fk": {
"name": "member_userId_user_id_fk",
"tableFrom": "member",
"tableTo": "user",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.organization": {
"name": "organization",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"slug": {
"name": "slug",
"type": "text",
"primaryKey": false,
"notNull": true
},
"logo": {
"name": "logo",
"type": "text",
"primaryKey": false,
"notNull": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"metadata": {
"name": "metadata",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"organization_slug_unique": {
"name": "organization_slug_unique",
"nullsNotDistinct": false,
"columns": [
"slug"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.reports": {
"name": "reports",
"schema": "",
"columns": {
"report_id": {
"name": "report_id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"site_id": {
"name": "site_id",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"report_type": {
"name": "report_type",
"type": "text",
"primaryKey": false,
"notNull": false
},
"data": {
"name": "data",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"reports_site_id_sites_site_id_fk": {
"name": "reports_site_id_sites_site_id_fk",
"tableFrom": "reports",
"tableTo": "sites",
"columnsFrom": [
"site_id"
],
"columnsTo": [
"site_id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"reports_user_id_user_id_fk": {
"name": "reports_user_id_user_id_fk",
"tableFrom": "reports",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.session": {
"name": "session",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"expiresAt": {
"name": "expiresAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"token": {
"name": "token",
"type": "text",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"ipAddress": {
"name": "ipAddress",
"type": "text",
"primaryKey": false,
"notNull": false
},
"userAgent": {
"name": "userAgent",
"type": "text",
"primaryKey": false,
"notNull": false
},
"userId": {
"name": "userId",
"type": "text",
"primaryKey": false,
"notNull": true
},
"impersonatedBy": {
"name": "impersonatedBy",
"type": "text",
"primaryKey": false,
"notNull": false
},
"activeOrganizationId": {
"name": "activeOrganizationId",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"session_userId_user_id_fk": {
"name": "session_userId_user_id_fk",
"tableFrom": "session",
"tableTo": "user",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"session_token_unique": {
"name": "session_token_unique",
"nullsNotDistinct": false,
"columns": [
"token"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.sites": {
"name": "sites",
"schema": "",
"columns": {
"site_id": {
"name": "site_id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"domain": {
"name": "domain",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"created_by": {
"name": "created_by",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"sites_created_by_user_id_fk": {
"name": "sites_created_by_user_id_fk",
"tableFrom": "sites",
"tableTo": "user",
"columnsFrom": [
"created_by"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"sites_domain_unique": {
"name": "sites_domain_unique",
"nullsNotDistinct": false,
"columns": [
"domain"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"emailVerified": {
"name": "emailVerified",
"type": "boolean",
"primaryKey": false,
"notNull": true
},
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"role": {
"name": "role",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'user'"
},
"displayUsername": {
"name": "displayUsername",
"type": "text",
"primaryKey": false,
"notNull": false
},
"banned": {
"name": "banned",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"banReason": {
"name": "banReason",
"type": "text",
"primaryKey": false,
"notNull": false
},
"banExpires": {
"name": "banExpires",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"user_username_unique": {
"name": "user_username_unique",
"nullsNotDistinct": false,
"columns": [
"username"
]
},
"user_email_unique": {
"name": "user_email_unique",
"nullsNotDistinct": false,
"columns": [
"email"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.verification": {
"name": "verification",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"identifier": {
"name": "identifier",
"type": "text",
"primaryKey": false,
"notNull": true
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true
},
"expiresAt": {
"name": "expiresAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View file

@ -0,0 +1,20 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1741156226094,
"tag": "0000_gorgeous_shriek",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1741197517430,
"tag": "0001_sweet_retro_girl",
"breakpoints": true
}
]
}

1542
server/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -7,7 +7,12 @@
"scripts": { "scripts": {
"dev": "tsc && node dist/index.js", "dev": "tsc && node dist/index.js",
"build": "tsc", "build": "tsc",
"start": "node dist/index.js" "start": "node dist/index.js",
"db:generate": "drizzle-kit generate --config=drizzle.config.ts",
"db:migrate": "drizzle-kit migrate --config=drizzle.config.ts",
"db:push": "drizzle-kit push --config=drizzle.config.ts",
"db:drop": "drizzle-kit drop --config=drizzle.config.ts",
"db:check": "drizzle-kit check --config=drizzle.config.ts"
}, },
"dependencies": { "dependencies": {
"@clickhouse/client": "^1.10.1", "@clickhouse/client": "^1.10.1",
@ -16,21 +21,24 @@
"@fastify/static": "^8.0.4", "@fastify/static": "^8.0.4",
"better-auth": "^1.2.2", "better-auth": "^1.2.2",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"drizzle-orm": "^0.40.0",
"fastify": "^5.1.0", "fastify": "^5.1.0",
"fastify-better-auth": "^1.0.1", "fastify-better-auth": "^1.0.1",
"luxon": "^3.5.0", "luxon": "^3.5.0",
"node-cron": "^3.0.3", "node-cron": "^3.0.3",
"pg": "^8.13.1", "pg": "^8.13.3",
"postgres": "^3.4.5", "postgres": "^3.4.5",
"ua-parser-js": "^2.0.0", "ua-parser-js": "^2.0.0",
"undici": "^7.3.0" "undici": "^7.3.0"
}, },
"devDependencies": { "devDependencies": {
"@types/pg": "^8.11.11",
"@types/luxon": "^3.4.2", "@types/luxon": "^3.4.2",
"@types/node": "^20.10.0", "@types/node": "^20.10.0",
"@types/node-cron": "^3.0.11", "@types/node-cron": "^3.0.11",
"@types/pg": "^8.11.11",
"drizzle-kit": "^0.30.5",
"ts-node-dev": "^2.0.0", "ts-node-dev": "^2.0.0",
"tsx": "^4.19.3",
"typescript": "^5.7.3" "typescript": "^5.7.3"
} }
} }

View file

@ -1,12 +1,16 @@
import { FastifyReply, FastifyRequest } from "fastify"; import { FastifyReply, FastifyRequest } from "fastify";
import { sql } from "../db/postgres/postgres.js"; import { db } from "../db/postgres/postgres.js";
import { activeSessions } from "../db/postgres/schema.js";
import { eq, count } from "drizzle-orm";
export const getLiveUsercount = async ( export const getLiveUsercount = async (
{ params: { site } }: FastifyRequest<{ Params: { site: string } }>, { params: { site } }: FastifyRequest<{ Params: { site: string } }>,
res: FastifyReply res: FastifyReply
) => { ) => {
const result = const result = await db
await sql`SELECT COUNT(*) FROM active_sessions WHERE site_id = ${site}`; .select({ count: count() })
.from(activeSessions)
.where(eq(activeSessions.siteId, Number(site)));
return res.send({ count: result[0].count }); return res.send({ count: result[0].count });
}; };

View file

@ -1,10 +1,9 @@
import { FastifyReply } from "fastify";
import { FastifyRequest } from "fastify";
import { auth } from "../../lib/auth.js";
import { fromNodeHeaders } from "better-auth/node"; import { fromNodeHeaders } from "better-auth/node";
import { sql } from "../../db/postgres/postgres.js"; import { FastifyReply, FastifyRequest } from "fastify";
import { db } from "../../db/postgres/postgres.js";
import { sites } from "../../db/postgres/schema.js";
import { loadAllowedDomains } from "../../lib/allowedDomains.js"; import { loadAllowedDomains } from "../../lib/allowedDomains.js";
import { auth } from "../../lib/auth.js";
export async function addSite( export async function addSite(
request: FastifyRequest<{ Body: { domain: string; name: string } }>, request: FastifyRequest<{ Body: { domain: string; name: string } }>,
@ -29,8 +28,14 @@ export async function addSite(
if (!session?.user.id) { if (!session?.user.id) {
return reply.status(500).send({ error: "Could not find user id" }); return reply.status(500).send({ error: "Could not find user id" });
} }
try { try {
await sql`INSERT INTO sites (domain, name, created_by) VALUES (${domain}, ${name}, ${session?.user.id})`; await db.insert(sites).values({
domain,
name,
createdBy: session.user.id,
});
await loadAllowedDomains(); await loadAllowedDomains();
return reply.status(200).send(); return reply.status(200).send();
} catch (err) { } catch (err) {

View file

@ -1,9 +1,8 @@
import { FastifyReply } from "fastify"; import { eq } from "drizzle-orm";
import { FastifyReply, FastifyRequest } from "fastify";
import { FastifyRequest } from "fastify"; import { db } from "../../db/postgres/postgres.js";
import { sql } from "../../db/postgres/postgres.js"; import { sites } from "../../db/postgres/schema.js";
import { loadAllowedDomains } from "../../lib/allowedDomains.js"; import { loadAllowedDomains } from "../../lib/allowedDomains.js";
import clickhouse from "../../db/clickhouse/clickhouse.js";
export async function deleteSite( export async function deleteSite(
request: FastifyRequest<{ Params: { id: string } }>, request: FastifyRequest<{ Params: { id: string } }>,
@ -11,7 +10,7 @@ export async function deleteSite(
) { ) {
const { id } = request.params; const { id } = request.params;
await sql`DELETE FROM sites WHERE site_id = ${id}`; await db.delete(sites).where(eq(sites.siteId, Number(id)));
// await clickhouse.query({ // await clickhouse.query({
// query: `DELETE FROM pageviews WHERE site_id = ${id}`, // query: `DELETE FROM pageviews WHERE site_id = ${id}`,
// }); // });

View file

@ -1,12 +1,12 @@
import { FastifyReply } from "fastify"; import { FastifyReply } from "fastify";
import { FastifyRequest } from "fastify"; import { FastifyRequest } from "fastify";
import { sql } from "../../db/postgres/postgres.js"; import { db } from "../../db/postgres/postgres.js";
import { sites } from "../../db/postgres/schema.js";
export async function getSites(_: FastifyRequest, reply: FastifyReply) { export async function getSites(_: FastifyRequest, reply: FastifyReply) {
try { try {
const sites = await sql`SELECT * FROM sites`; const sitesData = await db.select().from(sites);
return reply.status(200).send({ data: sites }); return reply.status(200).send({ data: sitesData });
} catch (err) { } catch (err) {
return reply.status(500).send({ error: String(err) }); return reply.status(500).send({ error: String(err) });
} }

View file

@ -0,0 +1,89 @@
import { drizzle } from "drizzle-orm/postgres-js";
import { migrate } from "drizzle-orm/postgres-js/migrator";
import postgres from "postgres";
import dotenv from "dotenv";
import * as schema from "./schema.js";
dotenv.config();
/**
* Run database migrations
* @param migrationsPath Path to migrations folder
* @returns Promise that resolves when migrations are complete
*/
export async function runMigrations(migrationsPath: string = "./drizzle") {
console.log("Running database migrations...");
const migrationClient = postgres({
host: process.env.POSTGRES_HOST || "postgres",
port: parseInt(process.env.POSTGRES_PORT || "5432", 10),
database: process.env.POSTGRES_DB,
username: process.env.POSTGRES_USER,
password: process.env.POSTGRES_PASSWORD,
max: 1,
onnotice: () => {}, // Silence notices
});
const db = drizzle(migrationClient, { schema });
try {
// Create drizzle schema if it doesn't exist
await migrationClient`CREATE SCHEMA IF NOT EXISTS drizzle;`;
// Create migration table if it doesn't exist
await migrationClient`
CREATE TABLE IF NOT EXISTS drizzle."__drizzle_migrations" (
id SERIAL PRIMARY KEY,
hash text NOT NULL,
created_at timestamp with time zone DEFAULT now()
);
`;
// Always run migrations, Drizzle will skip already applied ones
console.log(
"Running all migrations - Drizzle will skip already applied ones"
);
// This will run migrations on the database, skipping the ones already applied
try {
await migrate(db, { migrationsFolder: migrationsPath });
console.log("Migrations completed!");
return true; // Return success
} catch (err: any) {
// If error contains relation already exists, tables likely exist but not in drizzle metadata
if (err.message && err.message.includes("already exists")) {
console.log(
"Some tables already exist but not tracked by drizzle. This is expected for existing databases."
);
console.log(
"You can safely ignore these errors if your database is already set up."
);
return true; // Consider this a success case
} else {
// Other errors should be reported
throw err;
}
}
} catch (e) {
console.error("Migration failed!");
console.error(e);
return false; // Return failure
} finally {
// Don't forget to close the connection
await migrationClient.end();
}
}
// Only run migrations directly if this module is executed directly
if (import.meta.url === `file://${process.argv[1]}`) {
runMigrations()
.then((success) => {
if (!success) {
process.exit(1);
}
})
.catch((err) => {
console.error("Unhandled error in migrations:", err);
process.exit(1);
});
}

View file

@ -1,10 +1,13 @@
import dotenv from "dotenv"; import dotenv from "dotenv";
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres"; import postgres from "postgres";
import { auth } from "../../lib/auth.js"; import { auth } from "../../lib/auth.js";
import * as schema from "./schema.js";
dotenv.config(); dotenv.config();
export const sql = postgres({ // Create postgres connection
const client = postgres({
host: process.env.POSTGRES_HOST || "postgres", host: process.env.POSTGRES_HOST || "postgres",
port: parseInt(process.env.POSTGRES_PORT || "5432", 10), port: parseInt(process.env.POSTGRES_PORT || "5432", 10),
database: process.env.POSTGRES_DB, database: process.env.POSTGRES_DB,
@ -13,115 +16,41 @@ export const sql = postgres({
onnotice: () => {}, onnotice: () => {},
}); });
// Create drizzle ORM instance
export const db = drizzle(client, { schema });
// For compatibility with raw SQL if needed
export const sql = client;
export async function initializePostgres() { export async function initializePostgres() {
try { try {
// Phase 1: Create tables with no dependencies console.log("Initializing PostgreSQL database...");
await sql`
CREATE TABLE IF NOT EXISTS "user" (
"id" text not null primary key,
"name" text not null,
"username" text not null unique,
"email" text not null unique,
"emailVerified" boolean not null,
"image" text,
"createdAt" timestamp not null,
"updatedAt" timestamp not null,
"role" text not null default 'user'
);
`;
await sql` // Assume migrations have been run manually with 'npm run db:migrate'
CREATE TABLE IF NOT EXISTS "verification" ( // No automatic migrations during application startup
"id" text not null primary key,
"identifier" text not null,
"value" text not null,
"expiresAt" timestamp not null,
"createdAt" timestamp,
"updatedAt" timestamp
);
`;
await sql`
CREATE TABLE IF NOT EXISTS active_sessions (
session_id TEXT PRIMARY KEY,
site_id INT,
user_id TEXT,
hostname TEXT,
start_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_activity TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
pageviews INT DEFAULT 0,
entry_page TEXT,
exit_page TEXT,
device_type TEXT,
screen_width INT,
screen_height INT,
browser TEXT,
operating_system TEXT,
language TEXT,
referrer TEXT
);
`;
await sql`
CREATE TABLE IF NOT EXISTS sites (
site_id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
domain TEXT NOT NULL UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by TEXT NOT NULL REFERENCES "user" ("id")
);
`;
await sql`
CREATE TABLE IF NOT EXISTS "session" (
"id" text not null primary key,
"expiresAt" timestamp not null,
"token" text not null unique,
"createdAt" timestamp not null,
"updatedAt" timestamp not null,
"ipAddress" text,
"userAgent" text,
"userId" text not null references "user" ("id")
);
`;
await sql`
CREATE TABLE IF NOT EXISTS "account" (
"id" text not null primary key,
"accountId" text not null,
"providerId" text not null,
"userId" text not null references "user" ("id"),
"accessToken" text,
"refreshToken" text,
"idToken" text,
"accessTokenExpiresAt" timestamp,
"refreshTokenExpiresAt" timestamp,
"scope" text,
"password" text,
"createdAt" timestamp not null,
"updatedAt" timestamp not null
);
`;
// Check if admin user exists, if not create one
const [{ count }]: { count: number }[] = const [{ count }]: { count: number }[] =
await sql`SELECT count(*) FROM "user" WHERE username = 'admin'`; await client`SELECT count(*) FROM "user" WHERE username = 'admin'`;
if (Number(count) === 0) { if (Number(count) === 0) {
// Create admin user
console.log("Creating admin user");
await auth!.api.signUpEmail({ await auth!.api.signUpEmail({
body: { body: {
email: "test@test.com", email: "admin@example.com",
username: "admin", username: "admin",
name: "admin",
password: "admin123", password: "admin123",
name: "Admin User",
}, },
}); });
} }
await sql`UPDATE "user" SET "role" = 'admin' WHERE username = 'admin'`; await client`UPDATE "user" SET "role" = 'admin' WHERE username = 'admin'`;
console.log("Tables created successfully."); console.log("PostgreSQL initialization completed successfully.");
} catch (err) { } catch (error) {
console.error("Error creating tables:", err); console.error("Error initializing PostgreSQL:", error);
throw error;
} }
} }

View file

@ -0,0 +1,155 @@
import {
pgTable,
text,
timestamp,
integer,
boolean,
primaryKey,
foreignKey,
serial,
unique,
jsonb,
} from "drizzle-orm/pg-core";
// User table
export const users = pgTable("user", {
id: text("id").primaryKey().notNull(),
name: text("name").notNull(),
username: text("username").notNull().unique(),
email: text("email").notNull().unique(),
emailVerified: boolean("emailVerified").notNull(),
image: text("image"),
createdAt: timestamp("createdAt").notNull(),
updatedAt: timestamp("updatedAt").notNull(),
role: text("role").notNull().default("user"),
displayUsername: text("displayUsername"),
banned: boolean("banned"),
banReason: text("banReason"),
banExpires: timestamp("banExpires"),
});
// Verification table
export const verification = pgTable("verification", {
id: text("id").primaryKey().notNull(),
identifier: text("identifier").notNull(),
value: text("value").notNull(),
expiresAt: timestamp("expiresAt").notNull(),
createdAt: timestamp("createdAt"),
updatedAt: timestamp("updatedAt"),
});
// Sites table
export const sites = pgTable("sites", {
siteId: serial("site_id").primaryKey().notNull(),
name: text("name").notNull(),
domain: text("domain").notNull().unique(),
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").defaultNow(),
createdBy: text("created_by")
.notNull()
.references(() => users.id),
});
// Active sessions table
export const activeSessions = pgTable("active_sessions", {
sessionId: text("session_id").primaryKey().notNull(),
siteId: integer("site_id"),
userId: text("user_id"),
hostname: text("hostname"),
startTime: timestamp("start_time").defaultNow(),
lastActivity: timestamp("last_activity").defaultNow(),
pageviews: integer("pageviews").default(0),
entryPage: text("entry_page"),
exitPage: text("exit_page"),
deviceType: text("device_type"),
screenWidth: integer("screen_width"),
screenHeight: integer("screen_height"),
browser: text("browser"),
operatingSystem: text("operating_system"),
language: text("language"),
referrer: text("referrer"),
});
export const reports = pgTable("reports", {
reportId: serial("report_id").primaryKey().notNull(),
siteId: integer("site_id").references(() => sites.siteId),
userId: text("user_id").references(() => users.id),
reportType: text("report_type"),
data: jsonb("data"),
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").defaultNow(),
});
// Account table
export const account = pgTable("account", {
id: text("id").primaryKey().notNull(),
accountId: text("accountId").notNull(),
providerId: text("providerId").notNull(),
userId: text("userId")
.notNull()
.references(() => users.id),
accessToken: text("accessToken"),
refreshToken: text("refreshToken"),
idToken: text("idToken"),
accessTokenExpiresAt: timestamp("accessTokenExpiresAt"),
refreshTokenExpiresAt: timestamp("refreshTokenExpiresAt"),
scope: text("scope"),
password: text("password"),
createdAt: timestamp("createdAt").notNull(),
updatedAt: timestamp("updatedAt").notNull(),
});
// Organization table
export const organization = pgTable("organization", {
id: text("id").primaryKey().notNull(),
name: text("name").notNull(),
slug: text("slug").notNull().unique(),
logo: text("logo"),
createdAt: timestamp("createdAt").notNull(),
metadata: text("metadata"),
});
// Member table
export const member = pgTable("member", {
id: text("id").primaryKey().notNull(),
organizationId: text("organizationId")
.notNull()
.references(() => organization.id),
userId: text("userId")
.notNull()
.references(() => users.id),
role: text("role").notNull(),
createdAt: timestamp("createdAt").notNull(),
});
// Invitation table
export const invitation = pgTable("invitation", {
id: text("id").primaryKey().notNull(),
email: text("email").notNull(),
inviterId: text("inviterId")
.notNull()
.references(() => users.id),
organizationId: text("organizationId")
.notNull()
.references(() => organization.id),
role: text("role").notNull(),
status: text("status").notNull(),
expiresAt: timestamp("expiresAt").notNull(),
createdAt: timestamp("createdAt").notNull(),
});
// Session table
export const session = pgTable("session", {
id: text("id").primaryKey().notNull(),
expiresAt: timestamp("expiresAt").notNull(),
token: text("token").notNull().unique(),
createdAt: timestamp("createdAt").notNull(),
updatedAt: timestamp("updatedAt").notNull(),
ipAddress: text("ipAddress"),
userAgent: text("userAgent"),
userId: text("userId")
.notNull()
.references(() => users.id),
impersonatedBy: text("impersonatedBy"),
activeOrganizationId: text("activeOrganizationId"),
});

View file

@ -1,4 +1,5 @@
import { sql } from "../db/postgres/postgres.js"; import { db, sql } from "../db/postgres/postgres.js";
import { sites } from "../db/postgres/schema.js";
import { initAuth } from "./auth.js"; import { initAuth } from "./auth.js";
import dotenv from "dotenv"; import dotenv from "dotenv";
@ -20,7 +21,9 @@ export const loadAllowedDomains = async () => {
// Only query the sites table if it exists // Only query the sites table if it exists
let domains: { domain: string }[] = []; let domains: { domain: string }[] = [];
if (tableExists[0].exists) { if (tableExists[0].exists) {
domains = await sql`SELECT domain FROM sites`; // Use Drizzle to get domains
const sitesData = await db.select({ domain: sites.domain }).from(sites);
domains = sitesData;
} }
allowList = [ allowList = [

View file

@ -2,6 +2,9 @@ import { betterAuth } from "better-auth";
import { username, admin, organization } from "better-auth/plugins"; import { username, admin, organization } from "better-auth/plugins";
import dotenv from "dotenv"; import dotenv from "dotenv";
import pg from "pg"; import pg from "pg";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db } from "../db/postgres/postgres.js";
import * as schema from "../db/postgres/schema.js";
dotenv.config(); dotenv.config();
@ -33,16 +36,26 @@ export let auth: AuthType | null = betterAuth({
}, },
}); });
export const initAuth = (allowList: string[]) => { export function initAuth(allowedOrigins: string[]) {
auth = betterAuth({ auth = betterAuth({
basePath: "/auth", basePath: "/auth",
database: new pg.Pool({ database: drizzleAdapter(db, {
host: process.env.POSTGRES_HOST || "postgres", provider: "pg",
port: parseInt(process.env.POSTGRES_PORT || "5432", 10), schema: {
database: process.env.POSTGRES_DB, // Map our schema tables to what better-auth expects
user: process.env.POSTGRES_USER, user: schema.users,
password: process.env.POSTGRES_PASSWORD, account: schema.account,
session: schema.session,
verification: schema.verification,
organization: schema.organization,
member: schema.member,
},
}), }),
experimental: {
sessionCookie: {
domains: allowedOrigins,
},
},
emailAndPassword: { emailAndPassword: {
enabled: true, enabled: true,
}, },
@ -50,7 +63,7 @@ export const initAuth = (allowList: string[]) => {
enabled: true, enabled: true,
}, },
plugins: [username(), admin(), organization()], plugins: [username(), admin(), organization()],
trustedOrigins: allowList, trustedOrigins: allowedOrigins,
advanced: { advanced: {
useSecureCookies: process.env.NODE_ENV === "production", // don't mark Secure in dev useSecureCookies: process.env.NODE_ENV === "production", // don't mark Secure in dev
defaultCookieAttributes: { defaultCookieAttributes: {
@ -59,4 +72,4 @@ export const initAuth = (allowList: string[]) => {
}, },
}, },
}); });
}; }

View file

@ -2,8 +2,10 @@ import { FastifyReply, FastifyRequest } from "fastify";
import { TrackingPayload } from "../types.js"; import { TrackingPayload } from "../types.js";
import { getUserId, getDeviceType, getIpAddress } from "../utils.js"; import { getUserId, getDeviceType, getIpAddress } from "../utils.js";
import crypto from "crypto"; import crypto from "crypto";
import { sql } from "../db/postgres/postgres.js"; import { db, sql } from "../db/postgres/postgres.js";
import { activeSessions } from "../db/postgres/schema.js";
import UAParser, { UAParser as userAgentParser } from "ua-parser-js"; import UAParser, { UAParser as userAgentParser } from "ua-parser-js";
import { eq } from "drizzle-orm";
import { Pageview } from "../db/clickhouse/types.js"; import { Pageview } from "../db/clickhouse/types.js";
import { pageviewQueue } from "./pageviewQueue.js"; import { pageviewQueue } from "./pageviewQueue.js";
@ -17,8 +19,31 @@ type TotalPayload = TrackingPayload & {
ipAddress: string; ipAddress: string;
}; };
const getExistingSession = async (userId: string): Promise<Pageview | null> => { // Extended type for database active sessions
const [existingSession] = await sql<Pageview[]>` type ActiveSession = {
session_id: string;
site_id: number | null;
user_id: string;
pageviews: number;
hostname: string | null;
start_time: Date | null;
last_activity: Date | null;
entry_page: string | null;
exit_page: string | null;
device_type: string | null;
screen_width: number | null;
screen_height: number | null;
browser: string | null;
operating_system: string | null;
language: string | null;
referrer: string | null;
};
const getExistingSession = async (
userId: string
): Promise<ActiveSession | null> => {
// We need to use the raw SQL query here since we're selecting into a specific type
const [existingSession] = await sql<ActiveSession[]>`
SELECT * FROM active_sessions WHERE user_id = ${userId} SELECT * FROM active_sessions WHERE user_id = ${userId}
`; `;
return existingSession; return existingSession;
@ -26,41 +51,47 @@ const getExistingSession = async (userId: string): Promise<Pageview | null> => {
const updateSession = async ( const updateSession = async (
pageview: TotalPayload, pageview: TotalPayload,
existingSession: Pageview | null existingSession: ActiveSession | null
) => { ) => {
if (existingSession) { if (existingSession) {
await sql` // Update session with Drizzle
UPDATE active_sessions SET last_activity = ${pageview.timestamp}, pageviews = pageviews + 1 WHERE user_id = ${pageview.userId} await db
`; .update(activeSessions)
.set({
lastActivity: new Date(pageview.timestamp),
pageviews: (existingSession.pageviews || 0) + 1,
})
.where(eq(activeSessions.userId, pageview.userId));
return; return;
} }
const inserts = { // Insert new session with Drizzle
site_id: pageview.site_id || 0, const insertData = {
session_id: pageview.sessionId, sessionId: pageview.sessionId,
user_id: pageview.userId, siteId:
hostname: pageview.hostname || "", typeof pageview.site_id === "string"
start_time: pageview.timestamp || "", ? parseInt(pageview.site_id, 10)
last_activity: pageview.timestamp || "", : pageview.site_id,
userId: pageview.userId,
hostname: pageview.hostname || null,
startTime: new Date(pageview.timestamp || Date.now()),
lastActivity: new Date(pageview.timestamp || Date.now()),
pageviews: 1, pageviews: 1,
entry_page: pageview.pathname || "", entryPage: pageview.pathname || null,
// exit_page: pageview.pathname, deviceType: getDeviceType(
device_type: getDeviceType(
pageview.screenWidth, pageview.screenWidth,
pageview.screenHeight, pageview.screenHeight,
pageview.ua pageview.ua
), ),
screen_width: pageview.screenWidth || 0, screenWidth: pageview.screenWidth || null,
screen_height: pageview.screenHeight || 0, screenHeight: pageview.screenHeight || null,
browser: pageview.ua.browser.name || "", browser: pageview.ua.browser.name || null,
operating_system: pageview.ua.os.name || "", operatingSystem: pageview.ua.os.name || null,
language: pageview.language || "", language: pageview.language || null,
referrer: pageview.referrer || "", referrer: pageview.referrer || null,
}; };
await sql` await db.insert(activeSessions).values(insertData);
INSERT INTO active_sessions ${sql(inserts)}
`;
}; };
export async function trackPageView( export async function trackPageView(