- {serverStats.numBookmarks}
+ {serverStats.numEntries}
@@ -105,28 +105,7 @@ export default function ServerStats() {
- Crawling Jobs
- {serverStats.crawlStats.queued}
- {serverStats.crawlStats.pending}
- {serverStats.crawlStats.failed}
-
-
- Indexing Jobs
- {serverStats.indexingStats.queued}
- -
- -
-
-
- Inference Jobs
- {serverStats.inferenceStats.queued}
- {serverStats.inferenceStats.pending}
- {serverStats.inferenceStats.failed}
-
-
- Tidy Assets Jobs
- {serverStats.tidyAssetsStats.queued}
- -
- -
+ No workers yet...
diff --git a/apps/web/components/dashboard/admin/UserList.tsx b/apps/web/components/dashboard/admin/UserList.tsx
index 2937df2..cadb20e 100644
--- a/apps/web/components/dashboard/admin/UserList.tsx
+++ b/apps/web/components/dashboard/admin/UserList.tsx
@@ -67,8 +67,8 @@ export default function UsersSection() {
Name
Email
- Num Bookmarks
- Asset Sizes
+ Days on Record
+ Entries
Role
Local User
Actions
@@ -79,10 +79,10 @@ export default function UsersSection() {
{u.name}
{u.email}
- {userStats[u.id].numBookmarks}
+ {userStats[u.id].numDays}
- {toHumanReadableSize(userStats[u.id].assetSizes)}
+ {userStats[u.id].numEntries}
{u.role}
diff --git a/apps/web/components/dashboard/cleanups/TagDuplicationDetention.tsx b/apps/web/components/dashboard/cleanups/TagDuplicationDetention.tsx
index 61132a6..f5139fb 100644
--- a/apps/web/components/dashboard/cleanups/TagDuplicationDetention.tsx
+++ b/apps/web/components/dashboard/cleanups/TagDuplicationDetention.tsx
@@ -26,7 +26,7 @@ import { cn } from "@/lib/utils";
import { distance } from "fastest-levenshtein";
import { Check, Combine, X } from "lucide-react";
-import { useMergeTag } from "@hoarder/shared-react/hooks/tags";
+import { useMergeTag } from "@lifetracker/shared-react/hooks/tags";
interface Suggestion {
mergeIntoId: string;
diff --git a/apps/web/server/api/client.ts b/apps/web/server/api/client.ts
index 0532e64..a999ae5 100644
--- a/apps/web/server/api/client.ts
+++ b/apps/web/server/api/client.ts
@@ -17,11 +17,6 @@ export async function createContextFromRequest(req: Request) {
if (authorizationHeader && authorizationHeader.startsWith("Bearer ")) {
const token = authorizationHeader.split(" ")[1];
try {
-
- console.log("\n\nEeeeeeentering hell\n\n");
- console.log(await authenticateApiKey(token));
- console.log("\n\n\n\n");
-
const user = await authenticateApiKey(token);
return {
user,
@@ -44,7 +39,7 @@ export const createContext = async (
): Promise => {
const session = await getServerAuthSession();
if (ip === undefined) {
- const hdrs = headers();
+ const hdrs = await headers();
ip = requestIp.getClientIp({
headers: Object.fromEntries(hdrs.entries()),
});
diff --git a/packages/db/drizzle.config.ts b/packages/db/drizzle.config.ts
index f02c071..e2f24d3 100644
--- a/packages/db/drizzle.config.ts
+++ b/packages/db/drizzle.config.ts
@@ -2,8 +2,6 @@ import dotenv from "dotenv";
import type { Config } from "drizzle-kit";
import serverConfig from "@lifetracker/shared/config";
-
-
const databaseURL = serverConfig.dataDir
? `${serverConfig.dataDir}/lifetracker.db`
: "./lifetracker.db";
diff --git a/packages/db/drizzle.ts b/packages/db/drizzle.ts
index 8c9fc28..af86231 100644
--- a/packages/db/drizzle.ts
+++ b/packages/db/drizzle.ts
@@ -8,7 +8,10 @@ import path from "path";
import dbConfig from "./drizzle.config";
const sqlite = new Database(dbConfig.dbCredentials.url);
-export const db = drizzle(sqlite, { schema, logger: true });
+export const db = drizzle(sqlite, {
+ schema,
+ // logger: true
+});
export function getInMemoryDB(runMigrations: boolean) {
const mem = new Database(":memory:");
diff --git a/packages/shared/config.ts b/packages/shared/config.ts
index 92c5d2f..e3c8117 100644
--- a/packages/shared/config.ts
+++ b/packages/shared/config.ts
@@ -1,9 +1,8 @@
import { z } from "zod";
import dotenv from "dotenv";
-console.log(dotenv.config({
+dotenv.config({
path: ".env.local",
- debug: true
-}));
+});
const stringBool = (defaultValue: string) =>
z
.string()
diff --git a/packages/shared/types/admin.ts b/packages/shared/types/admin.ts
new file mode 100644
index 0000000..0f29ed4
--- /dev/null
+++ b/packages/shared/types/admin.ts
@@ -0,0 +1,25 @@
+import { z } from "zod";
+
+import { PASSWORD_MAX_LENGTH, zSignUpSchema } from "./users";
+
+export const zRoleSchema = z.object({
+ role: z.enum(["user", "admin"]),
+});
+
+export const zAdminCreateUserSchema = zSignUpSchema.and(zRoleSchema);
+
+export const changeRoleSchema = z.object({
+ userId: z.string(),
+ role: z.enum(["user", "admin"]),
+});
+
+export const resetPasswordSchema = z
+ .object({
+ userId: z.string(),
+ newPassword: z.string().min(8).max(PASSWORD_MAX_LENGTH),
+ newPasswordConfirm: z.string(),
+ })
+ .refine((data) => data.newPassword === data.newPasswordConfirm, {
+ message: "Passwords don't match",
+ path: ["newPasswordConfirm"],
+ });
\ No newline at end of file
diff --git a/packages/trpc/routers/_app.ts b/packages/trpc/routers/_app.ts
index f1397dd..bafafa7 100644
--- a/packages/trpc/routers/_app.ts
+++ b/packages/trpc/routers/_app.ts
@@ -2,10 +2,12 @@ import { apiKeys } from "@lifetracker/db/schema";
import { router } from "../index";
import { usersAppRouter } from "./users";
import { apiKeysAppRouter } from "./apiKeys";
+import { adminAppRouter } from "./admin";
export const appRouter = router({
users: usersAppRouter,
apiKeys: apiKeysAppRouter,
+ admin: adminAppRouter,
});
// export type definition of API
export type AppRouter = typeof appRouter;
\ No newline at end of file
diff --git a/packages/trpc/routers/admin.ts b/packages/trpc/routers/admin.ts
new file mode 100644
index 0000000..e2f3edb
--- /dev/null
+++ b/packages/trpc/routers/admin.ts
@@ -0,0 +1,118 @@
+import { TRPCError } from "@trpc/server";
+import { count, eq, sum } from "drizzle-orm";
+import { z } from "zod";
+
+import { users } from "@lifetracker/db/schema";
+import {
+ changeRoleSchema,
+ resetPasswordSchema,
+ zAdminCreateUserSchema,
+} from "@lifetracker/shared/types/admin";
+
+import { hashPassword } from "../auth";
+import { adminProcedure, router } from "../index";
+import { createUser } from "./users";
+
+export const adminAppRouter = router({
+ stats: adminProcedure
+ .output(
+ z.object({
+ numUsers: z.number(),
+ numEntries: z.number(),
+ }),
+ )
+ .query(async ({ ctx }) => {
+ const [
+ [{ value: numUsers }],
+ ] = await Promise.all([
+ ctx.db.select({ value: count() }).from(users)]);
+
+ return {
+ numUsers,
+ numEntries: 420.69,
+ };
+ }),
+ userStats: adminProcedure
+ .output(
+ z.record(
+ z.string(),
+ z.object({
+ numDays: z.number(),
+ numEntries: z.number(),
+ }),
+ ),
+ )
+ .query(async ({ ctx }) => {
+ const [userIds] = await Promise.all([
+ ctx.db.select({ id: users.id }).from(users)
+ ]);
+
+ const results: Record<
+ string,
+ { numDays: number; numEntries: number }
+ > = {};
+ for (const user of userIds) {
+ results[user.id] = {
+ numDays: 0,
+ numEntries: 0,
+ };
+ }
+ return results;
+ }),
+ createUser: adminProcedure
+ .input(zAdminCreateUserSchema)
+ .output(
+ z.object({
+ id: z.string(),
+ name: z.string(),
+ email: z.string(),
+ role: z.enum(["user", "admin"]).nullable(),
+ }),
+ )
+ .mutation(async ({ input, ctx }) => {
+ return createUser(input, ctx, input.role);
+ }),
+ changeRole: adminProcedure
+ .input(changeRoleSchema)
+ .mutation(async ({ input, ctx }) => {
+ if (ctx.user.id == input.userId) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Cannot change own role",
+ });
+ }
+ const result = await ctx.db
+ .update(users)
+ .set({ role: input.role })
+ .where(eq(users.id, input.userId));
+
+ if (!result.changes) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "User not found",
+ });
+ }
+ }),
+ resetPassword: adminProcedure
+ .input(resetPasswordSchema)
+ .mutation(async ({ input, ctx }) => {
+ if (ctx.user.id == input.userId) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Cannot reset own password",
+ });
+ }
+ const hashedPassword = await hashPassword(input.newPassword);
+ const result = await ctx.db
+ .update(users)
+ .set({ password: hashedPassword })
+ .where(eq(users.id, input.userId));
+
+ if (result.changes == 0) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "User not found",
+ });
+ }
+ }),
+});
\ No newline at end of file