* 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 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

View file

@ -27,7 +27,7 @@ export function NoData({
</div>
<CodeSnippet
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>
</CardContent>

View file

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

View file

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

View file

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

View file

@ -15,7 +15,7 @@ export default function Home() {
</div>
<div className="grid grid-cols-3 gap-4 mt-4">
{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">
<img
className="w-6 mr-1"

View file

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

View file

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

View file

@ -1,14 +1,46 @@
# syntax=docker/dockerfile:1
FROM node:22-alpine
FROM node:20-alpine AS builder
WORKDIR /app
COPY ["package.json", "package-lock.json*", "./"]
RUN npm install
# Install dependencies
COPY package*.json ./
RUN npm ci
# Copy source code
COPY . .
# Build the application
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": {
"dev": "tsc && node dist/index.js",
"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": {
"@clickhouse/client": "^1.10.1",
@ -16,21 +21,24 @@
"@fastify/static": "^8.0.4",
"better-auth": "^1.2.2",
"dotenv": "^16.4.7",
"drizzle-orm": "^0.40.0",
"fastify": "^5.1.0",
"fastify-better-auth": "^1.0.1",
"luxon": "^3.5.0",
"node-cron": "^3.0.3",
"pg": "^8.13.1",
"pg": "^8.13.3",
"postgres": "^3.4.5",
"ua-parser-js": "^2.0.0",
"undici": "^7.3.0"
},
"devDependencies": {
"@types/pg": "^8.11.11",
"@types/luxon": "^3.4.2",
"@types/node": "^20.10.0",
"@types/node-cron": "^3.0.11",
"@types/pg": "^8.11.11",
"drizzle-kit": "^0.30.5",
"ts-node-dev": "^2.0.0",
"tsx": "^4.19.3",
"typescript": "^5.7.3"
}
}

View file

@ -1,12 +1,16 @@
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 (
{ params: { site } }: FastifyRequest<{ Params: { site: string } }>,
res: FastifyReply
) => {
const result =
await sql`SELECT COUNT(*) FROM active_sessions WHERE site_id = ${site}`;
const result = await db
.select({ count: count() })
.from(activeSessions)
.where(eq(activeSessions.siteId, Number(site)));
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 { 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 { auth } from "../../lib/auth.js";
export async function addSite(
request: FastifyRequest<{ Body: { domain: string; name: string } }>,
@ -29,8 +28,14 @@ export async function addSite(
if (!session?.user.id) {
return reply.status(500).send({ error: "Could not find user id" });
}
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();
return reply.status(200).send();
} catch (err) {

View file

@ -1,9 +1,8 @@
import { FastifyReply } from "fastify";
import { FastifyRequest } from "fastify";
import { sql } from "../../db/postgres/postgres.js";
import { eq } from "drizzle-orm";
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 clickhouse from "../../db/clickhouse/clickhouse.js";
export async function deleteSite(
request: FastifyRequest<{ Params: { id: string } }>,
@ -11,7 +10,7 @@ export async function deleteSite(
) {
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({
// query: `DELETE FROM pageviews WHERE site_id = ${id}`,
// });

View file

@ -1,12 +1,12 @@
import { FastifyReply } 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) {
try {
const sites = await sql`SELECT * FROM sites`;
return reply.status(200).send({ data: sites });
const sitesData = await db.select().from(sites);
return reply.status(200).send({ data: sitesData });
} catch (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 { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import { auth } from "../../lib/auth.js";
import * as schema from "./schema.js";
dotenv.config();
export const sql = postgres({
// Create postgres connection
const client = postgres({
host: process.env.POSTGRES_HOST || "postgres",
port: parseInt(process.env.POSTGRES_PORT || "5432", 10),
database: process.env.POSTGRES_DB,
@ -13,115 +16,41 @@ export const sql = postgres({
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() {
try {
// Phase 1: Create tables with no dependencies
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'
);
`;
console.log("Initializing PostgreSQL database...");
await sql`
CREATE TABLE IF NOT EXISTS "verification" (
"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
);
`;
// Assume migrations have been run manually with 'npm run db:migrate'
// No automatic migrations during application startup
// Check if admin user exists, if not create one
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) {
// Create admin user
console.log("Creating admin user");
await auth!.api.signUpEmail({
body: {
email: "test@test.com",
email: "admin@example.com",
username: "admin",
name: "admin",
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.");
} catch (err) {
console.error("Error creating tables:", err);
console.log("PostgreSQL initialization completed successfully.");
} catch (error) {
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 dotenv from "dotenv";
@ -20,7 +21,9 @@ export const loadAllowedDomains = async () => {
// Only query the sites table if it exists
let domains: { domain: string }[] = [];
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 = [

View file

@ -2,6 +2,9 @@ import { betterAuth } from "better-auth";
import { username, admin, organization } from "better-auth/plugins";
import dotenv from "dotenv";
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();
@ -33,16 +36,26 @@ export let auth: AuthType | null = betterAuth({
},
});
export const initAuth = (allowList: string[]) => {
export function initAuth(allowedOrigins: string[]) {
auth = betterAuth({
basePath: "/auth",
database: new pg.Pool({
host: process.env.POSTGRES_HOST || "postgres",
port: parseInt(process.env.POSTGRES_PORT || "5432", 10),
database: process.env.POSTGRES_DB,
user: process.env.POSTGRES_USER,
password: process.env.POSTGRES_PASSWORD,
database: drizzleAdapter(db, {
provider: "pg",
schema: {
// Map our schema tables to what better-auth expects
user: schema.users,
account: schema.account,
session: schema.session,
verification: schema.verification,
organization: schema.organization,
member: schema.member,
},
}),
experimental: {
sessionCookie: {
domains: allowedOrigins,
},
},
emailAndPassword: {
enabled: true,
},
@ -50,7 +63,7 @@ export const initAuth = (allowList: string[]) => {
enabled: true,
},
plugins: [username(), admin(), organization()],
trustedOrigins: allowList,
trustedOrigins: allowedOrigins,
advanced: {
useSecureCookies: process.env.NODE_ENV === "production", // don't mark Secure in dev
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 { getUserId, getDeviceType, getIpAddress } from "../utils.js";
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 { eq } from "drizzle-orm";
import { Pageview } from "../db/clickhouse/types.js";
import { pageviewQueue } from "./pageviewQueue.js";
@ -17,8 +19,31 @@ type TotalPayload = TrackingPayload & {
ipAddress: string;
};
const getExistingSession = async (userId: string): Promise<Pageview | null> => {
const [existingSession] = await sql<Pageview[]>`
// Extended type for database active sessions
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}
`;
return existingSession;
@ -26,41 +51,47 @@ const getExistingSession = async (userId: string): Promise<Pageview | null> => {
const updateSession = async (
pageview: TotalPayload,
existingSession: Pageview | null
existingSession: ActiveSession | null
) => {
if (existingSession) {
await sql`
UPDATE active_sessions SET last_activity = ${pageview.timestamp}, pageviews = pageviews + 1 WHERE user_id = ${pageview.userId}
`;
// Update session with Drizzle
await db
.update(activeSessions)
.set({
lastActivity: new Date(pageview.timestamp),
pageviews: (existingSession.pageviews || 0) + 1,
})
.where(eq(activeSessions.userId, pageview.userId));
return;
}
const inserts = {
site_id: pageview.site_id || 0,
session_id: pageview.sessionId,
user_id: pageview.userId,
hostname: pageview.hostname || "",
start_time: pageview.timestamp || "",
last_activity: pageview.timestamp || "",
// Insert new session with Drizzle
const insertData = {
sessionId: pageview.sessionId,
siteId:
typeof pageview.site_id === "string"
? parseInt(pageview.site_id, 10)
: 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,
entry_page: pageview.pathname || "",
// exit_page: pageview.pathname,
device_type: getDeviceType(
entryPage: pageview.pathname || null,
deviceType: getDeviceType(
pageview.screenWidth,
pageview.screenHeight,
pageview.ua
),
screen_width: pageview.screenWidth || 0,
screen_height: pageview.screenHeight || 0,
browser: pageview.ua.browser.name || "",
operating_system: pageview.ua.os.name || "",
language: pageview.language || "",
referrer: pageview.referrer || "",
screenWidth: pageview.screenWidth || null,
screenHeight: pageview.screenHeight || null,
browser: pageview.ua.browser.name || null,
operatingSystem: pageview.ua.os.name || null,
language: pageview.language || null,
referrer: pageview.referrer || null,
};
await sql`
INSERT INTO active_sessions ${sql(inserts)}
`;
await db.insert(activeSessions).values(insertData);
};
export async function trackPageView(