+>(({ className, ...props }, ref) => (
+ [role=checkbox]]:translate-y-[2px]",
+ className
+ )}
+ {...props}
+ />
+));
+TableCell.displayName = "TableCell";
+
+const TableCaption = React.forwardRef<
+ HTMLTableCaptionElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+TableCaption.displayName = "TableCaption";
+
+export {
+ Table,
+ TableHeader,
+ TableBody,
+ TableFooter,
+ TableHead,
+ TableRow,
+ TableCell,
+ TableCaption,
+};
diff --git a/client/src/hooks/api.ts b/client/src/hooks/api.ts
index 410acbe..19bbb63 100644
--- a/client/src/hooks/api.ts
+++ b/client/src/hooks/api.ts
@@ -173,3 +173,18 @@ export function deleteSite(siteId: string) {
method: "POST",
});
}
+
+export type ListUsersResponse = {
+ id: string;
+ name: string;
+ username: string;
+ email: string;
+ emailVerified: boolean;
+ role: string;
+ createdAt: string;
+ updatedAt: string;
+}[];
+
+export function useListUsers() {
+ return useGenericQuery("list-users");
+}
diff --git a/client/src/lib/auth.ts b/client/src/lib/auth.ts
index 275f050..dc5c01f 100644
--- a/client/src/lib/auth.ts
+++ b/client/src/lib/auth.ts
@@ -1,9 +1,9 @@
-import { usernameClient } from "better-auth/client/plugins";
+import { usernameClient, adminClient } from "better-auth/client/plugins";
import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_BACKEND_URL,
- plugins: [usernameClient()],
+ plugins: [usernameClient(), adminClient()],
fetchOptions: {
credentials: "include",
},
diff --git a/client/tailwind.config.ts b/client/tailwind.config.ts
index e2144f7..a611f48 100644
--- a/client/tailwind.config.ts
+++ b/client/tailwind.config.ts
@@ -17,7 +17,7 @@ module.exports = {
},
extend: {
colors: {
- border: "hsl(var(--border))",
+ border: "red",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
diff --git a/server/src/actions/sites/addSite.ts b/server/src/actions/sites/addSite.ts
index 4f61094..1cfdb7a 100644
--- a/server/src/actions/sites/addSite.ts
+++ b/server/src/actions/sites/addSite.ts
@@ -31,7 +31,6 @@ export async function addSite(
}
try {
await sql`INSERT INTO sites (domain, name, created_by) VALUES (${domain}, ${name}, ${session?.user.id})`;
-
await loadAllowedDomains();
return reply.status(200).send();
} catch (err) {
diff --git a/server/src/api/listUsers.ts b/server/src/api/listUsers.ts
new file mode 100644
index 0000000..b3b6ee9
--- /dev/null
+++ b/server/src/api/listUsers.ts
@@ -0,0 +1,23 @@
+import { FastifyReply, FastifyRequest } from "fastify";
+import { sql } from "../db/postgres/postgres.js";
+
+type ListUsersResponse = {
+ id: string;
+ name: string;
+ username: string;
+ email: string;
+ emailVerified: boolean;
+ role: string;
+ createdAt: string;
+ updatedAt: string;
+}[];
+
+export async function listUsers(_: FastifyRequest, res: FastifyReply) {
+ try {
+ const users = await sql`SELECT * FROM "user"`;
+ return res.send({ data: users });
+ } catch (error) {
+ console.error("Error fetching users:", error);
+ return res.status(500).send({ error: "Failed to fetch users" });
+ }
+}
diff --git a/server/src/db/postgres/postgres.ts b/server/src/db/postgres/postgres.ts
index 6c1e8b2..f64a8d3 100644
--- a/server/src/db/postgres/postgres.ts
+++ b/server/src/db/postgres/postgres.ts
@@ -21,11 +21,13 @@ export async function initializePostgres() {
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
+ "updatedAt" timestamp not null,
+ "role" text not null default 'user'
);
`,
@@ -120,6 +122,8 @@ export async function initializePostgres() {
});
}
+ await sql`UPDATE "user" SET "role" = 'admin' WHERE username = 'admin'`;
+
console.log("Tables created successfully.");
} catch (err) {
console.error("Error creating tables:", err);
diff --git a/server/src/index.ts b/server/src/index.ts
index 712d524..d633fd3 100644
--- a/server/src/index.ts
+++ b/server/src/index.ts
@@ -20,12 +20,12 @@ import { getPages } from "./api/getPages.js";
import { getPageViews } from "./api/getPageViews.js";
import { getReferrers } from "./api/getReferrers.js";
import { initializeClickhouse } from "./db/clickhouse/clickhouse.js";
-import { initializePostgres, sql } from "./db/postgres/postgres.js";
+import { initializePostgres } from "./db/postgres/postgres.js";
import { cleanupOldSessions } from "./db/postgres/session-cleanup.js";
-import { auth, initAuth } from "./lib/auth.js";
+import { allowList, loadAllowedDomains } from "./lib/allowedDomains.js";
+import { auth } from "./lib/auth.js";
import { mapHeaders } from "./lib/betterAuth.js";
-import { allowList } from "./lib/allowedDomains.js";
-import { loadAllowedDomains } from "./lib/allowedDomains.js";
+import { listUsers } from "./api/listUsers.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
@@ -128,9 +128,11 @@ server.get("/pages", getPages);
server.get("/referrers", getReferrers);
server.get("/pageviews", getPageViews);
+// Administrative
server.post("/add-site", addSite);
server.post("/delete-site/:id", deleteSite);
server.get("/get-sites", getSites);
+server.get("/list-users", listUsers);
// Track pageview endpoint
server.post("/track/pageview", trackPageView);
diff --git a/server/src/lib/auth.ts b/server/src/lib/auth.ts
index d16eb4c..a17f588 100644
--- a/server/src/lib/auth.ts
+++ b/server/src/lib/auth.ts
@@ -1,5 +1,5 @@
import { betterAuth } from "better-auth";
-import { username } from "better-auth/plugins";
+import { username, admin } from "better-auth/plugins";
import dotenv from "dotenv";
import pg from "pg";
@@ -7,7 +7,31 @@ dotenv.config();
type AuthType = ReturnType | null;
-export let auth: AuthType | null = null;
+export let auth: AuthType | null = 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,
+ }),
+ emailAndPassword: {
+ enabled: true,
+ },
+ deleteUser: {
+ enabled: true,
+ },
+ plugins: [username(), admin()],
+ trustedOrigins: [],
+ advanced: {
+ useSecureCookies: process.env.NODE_ENV === "production", // don't mark Secure in dev
+ defaultCookieAttributes: {
+ sameSite: process.env.NODE_ENV === "production" ? "none" : "lax",
+ path: "/",
+ },
+ },
+});
export const initAuth = (allowList: string[]) => {
auth = betterAuth({
@@ -25,7 +49,7 @@ export const initAuth = (allowList: string[]) => {
deleteUser: {
enabled: true,
},
- plugins: [username()],
+ plugins: [username(), admin()],
trustedOrigins: allowList,
advanced: {
useSecureCookies: process.env.NODE_ENV === "production", // don't mark Secure in dev
|