Added auth, refactored schema, almost have TRPC MVP working
This commit is contained in:
parent
ef1d1f04e3
commit
c023fa1730
8
apps/web/app/api/health/route.ts
Normal file
8
apps/web/app/api/health/route.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export const GET = async (_req: NextRequest) => {
|
||||||
|
return NextResponse.json({
|
||||||
|
status: "ok",
|
||||||
|
message: "Web app is working",
|
||||||
|
});
|
||||||
|
};
|
||||||
22
apps/web/app/api/trpc/route.ts
Normal file
22
apps/web/app/api/trpc/route.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { createContextFromRequest } from "@/server/api/client";
|
||||||
|
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
|
||||||
|
|
||||||
|
import { appRouter } from "@lifetracker/trpc/routers/_app";
|
||||||
|
|
||||||
|
const handler = (req: Request) =>
|
||||||
|
fetchRequestHandler({
|
||||||
|
endpoint: "/api/trpc",
|
||||||
|
req,
|
||||||
|
router: appRouter,
|
||||||
|
onError: ({ path, error }) => {
|
||||||
|
if (process.env.NODE_ENV === "development") {
|
||||||
|
console.error(`❌ tRPC failed on ${path}`);
|
||||||
|
}
|
||||||
|
console.error(error);
|
||||||
|
},
|
||||||
|
|
||||||
|
createContext: async (opts) => {
|
||||||
|
return await createContextFromRequest(opts.req);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
export { handler as GET, handler as POST };
|
||||||
60
apps/web/app/server/api/client.ts
Normal file
60
apps/web/app/server/api/client.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { headers } from "next/headers";
|
||||||
|
import { getServerAuthSession } from "@/server/auth";
|
||||||
|
import requestIp from "request-ip";
|
||||||
|
|
||||||
|
import { db } from "@lifetracker/db";
|
||||||
|
import { Context, createCallerFactory } from "@lifetracker/trpc";
|
||||||
|
import { authenticateApiKey } from "@lifetracker/trpc/auth";
|
||||||
|
import { appRouter } from "@lifetracker/trpc/routers/_app";
|
||||||
|
|
||||||
|
export async function createContextFromRequest(req: Request) {
|
||||||
|
// TODO: This is a hack until we offer a proper REST API instead of the trpc based one.
|
||||||
|
// Check if the request has an Authorization token, if it does, assume that API key authentication is requested.
|
||||||
|
const ip = requestIp.getClientIp({
|
||||||
|
headers: Object.fromEntries(req.headers.entries()),
|
||||||
|
});
|
||||||
|
const authorizationHeader = req.headers.get("Authorization");
|
||||||
|
if (authorizationHeader && authorizationHeader.startsWith("Bearer ")) {
|
||||||
|
const token = authorizationHeader.split(" ")[1];
|
||||||
|
try {
|
||||||
|
const user = await authenticateApiKey(token);
|
||||||
|
return {
|
||||||
|
user,
|
||||||
|
db,
|
||||||
|
req: {
|
||||||
|
ip,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
// Fallthrough to cookie-based auth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return createContext(db, ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createContext = async (
|
||||||
|
database?: typeof db,
|
||||||
|
ip?: string | null,
|
||||||
|
): Promise<Context> => {
|
||||||
|
const session = await getServerAuthSession();
|
||||||
|
if (ip === undefined) {
|
||||||
|
const hdrs = headers();
|
||||||
|
ip = requestIp.getClientIp({
|
||||||
|
headers: Object.fromEntries(hdrs.entries()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
user: session?.user ?? null,
|
||||||
|
db: database ?? db,
|
||||||
|
req: {
|
||||||
|
ip,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const createCaller = createCallerFactory(appRouter);
|
||||||
|
|
||||||
|
export const api = createCaller(createContext);
|
||||||
|
|
||||||
|
export const createTrcpClientFromCtx = createCaller;
|
||||||
@ -3,17 +3,29 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbo",
|
"dev": "next dev",
|
||||||
|
"clean": "git clean -xdf .next .turbo node_modules",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint"
|
"lint": "next lint",
|
||||||
|
"test": "vitest",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"format": "prettier --check . --ignore-path ../../.gitignore"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@auth/drizzle-adapter": "^1.7.3",
|
||||||
"@lifetracker/db": "workspace:^",
|
"@lifetracker/db": "workspace:^",
|
||||||
|
"@lifetracker/shared": "workspace:^",
|
||||||
|
"@lifetracker/trpc": "workspace:^",
|
||||||
"@lifetracker/ui": "workspace:*",
|
"@lifetracker/ui": "workspace:*",
|
||||||
|
"@trpc/client": "^10.45.2",
|
||||||
|
"@trpc/server": "^10.45.2",
|
||||||
|
"drizzle-orm": "^0.33.0",
|
||||||
"next": "14.2.6",
|
"next": "14.2.6",
|
||||||
|
"next-auth": "^4.24.10",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-dom": "18.3.1"
|
"react-dom": "18.3.1",
|
||||||
|
"request-ip": "^3.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@lifetracker/eslint-config": "workspace:*",
|
"@lifetracker/eslint-config": "workspace:*",
|
||||||
@ -21,8 +33,10 @@
|
|||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^18",
|
||||||
|
"@types/request-ip": "^0.0.41",
|
||||||
"eslint": "^8",
|
"eslint": "^8",
|
||||||
"eslint-config-next": "14.2.6",
|
"eslint-config-next": "14.2.6",
|
||||||
"typescript": "^5"
|
"typescript": "^5",
|
||||||
|
"vite-tsconfig-paths": "^4.3.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
60
apps/web/server/api/client.ts
Normal file
60
apps/web/server/api/client.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { headers } from "next/headers";
|
||||||
|
import { getServerAuthSession } from "@/server/auth";
|
||||||
|
import requestIp from "request-ip";
|
||||||
|
|
||||||
|
import { db } from "@lifetracker/db";
|
||||||
|
import { Context, createCallerFactory } from "@lifetracker/trpc";
|
||||||
|
import { authenticateApiKey } from "@lifetracker/trpc/auth";
|
||||||
|
import { appRouter } from "@lifetracker/trpc/routers/_app";
|
||||||
|
|
||||||
|
export async function createContextFromRequest(req: Request) {
|
||||||
|
// TODO: This is a hack until we offer a proper REST API instead of the trpc based one.
|
||||||
|
// Check if the request has an Authorization token, if it does, assume that API key authentication is requested.
|
||||||
|
const ip = requestIp.getClientIp({
|
||||||
|
headers: Object.fromEntries(req.headers.entries()),
|
||||||
|
});
|
||||||
|
const authorizationHeader = req.headers.get("Authorization");
|
||||||
|
if (authorizationHeader && authorizationHeader.startsWith("Bearer ")) {
|
||||||
|
const token = authorizationHeader.split(" ")[1];
|
||||||
|
try {
|
||||||
|
const user = await authenticateApiKey(token);
|
||||||
|
return {
|
||||||
|
user,
|
||||||
|
db,
|
||||||
|
req: {
|
||||||
|
ip,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
// Fallthrough to cookie-based auth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return createContext(db, ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createContext = async (
|
||||||
|
database?: typeof db,
|
||||||
|
ip?: string | null,
|
||||||
|
): Promise<Context> => {
|
||||||
|
const session = await getServerAuthSession();
|
||||||
|
if (ip === undefined) {
|
||||||
|
const hdrs = headers();
|
||||||
|
ip = requestIp.getClientIp({
|
||||||
|
headers: Object.fromEntries(hdrs.entries()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
user: session?.user ?? null,
|
||||||
|
db: database ?? db,
|
||||||
|
req: {
|
||||||
|
ip,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const createCaller = createCallerFactory(appRouter);
|
||||||
|
|
||||||
|
export const api = createCaller(createContext);
|
||||||
|
|
||||||
|
export const createTrcpClientFromCtx = createCaller;
|
||||||
191
apps/web/server/auth.ts
Normal file
191
apps/web/server/auth.ts
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
import type { Adapter } from "next-auth/adapters";
|
||||||
|
import { DrizzleAdapter } from "@auth/drizzle-adapter";
|
||||||
|
import { and, count, eq } from "drizzle-orm";
|
||||||
|
import NextAuth, {
|
||||||
|
DefaultSession,
|
||||||
|
getServerSession,
|
||||||
|
NextAuthOptions,
|
||||||
|
} from "next-auth";
|
||||||
|
import CredentialsProvider from "next-auth/providers/credentials";
|
||||||
|
import { Provider } from "next-auth/providers/index";
|
||||||
|
import requestIp from "request-ip";
|
||||||
|
|
||||||
|
import { db } from "@lifetracker/db";
|
||||||
|
import {
|
||||||
|
accounts,
|
||||||
|
sessions,
|
||||||
|
users,
|
||||||
|
verificationTokens,
|
||||||
|
} from "@lifetracker/db/schema";
|
||||||
|
import serverConfig from "@lifetracker/shared/config";
|
||||||
|
import { logAuthenticationError, validatePassword } from "@lifetracker/trpc/auth";
|
||||||
|
|
||||||
|
type UserRole = "admin" | "user";
|
||||||
|
|
||||||
|
declare module "next-auth/jwt" {
|
||||||
|
export interface JWT {
|
||||||
|
user: {
|
||||||
|
id: string;
|
||||||
|
role: UserRole;
|
||||||
|
} & DefaultSession["user"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "next-auth" {
|
||||||
|
/**
|
||||||
|
* Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
|
||||||
|
*/
|
||||||
|
export interface Session {
|
||||||
|
user: {
|
||||||
|
id: string;
|
||||||
|
role: UserRole;
|
||||||
|
} & DefaultSession["user"];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DefaultUser {
|
||||||
|
role: UserRole | null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the user table is empty, which indicates that this user is going to be
|
||||||
|
* the first one. This can be racy if multiple users are created at the same time, but
|
||||||
|
* that should be fine.
|
||||||
|
*/
|
||||||
|
async function isFirstUser(): Promise<boolean> {
|
||||||
|
const [{ count: userCount }] = await db
|
||||||
|
.select({ count: count() })
|
||||||
|
.from(users);
|
||||||
|
return userCount == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the user is an admin
|
||||||
|
*/
|
||||||
|
async function isAdmin(email: string): Promise<boolean> {
|
||||||
|
const res = await db.query.users.findFirst({
|
||||||
|
columns: { role: true },
|
||||||
|
where: eq(users.email, email),
|
||||||
|
});
|
||||||
|
return res?.role == "admin";
|
||||||
|
}
|
||||||
|
|
||||||
|
const providers: Provider[] = [
|
||||||
|
CredentialsProvider({
|
||||||
|
// The name to display on the sign in form (e.g. "Sign in with...")
|
||||||
|
name: "Credentials",
|
||||||
|
credentials: {
|
||||||
|
email: { label: "Email", type: "email", placeholder: "Email" },
|
||||||
|
password: { label: "Password", type: "password" },
|
||||||
|
},
|
||||||
|
async authorize(credentials, req) {
|
||||||
|
if (!credentials) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await validatePassword(
|
||||||
|
credentials?.email,
|
||||||
|
credentials?.password,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
const error = e as Error;
|
||||||
|
logAuthenticationError(
|
||||||
|
credentials?.email,
|
||||||
|
error.message,
|
||||||
|
requestIp.getClientIp({ headers: req.headers }),
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const oauth = serverConfig.auth.oauth;
|
||||||
|
if (oauth.wellKnownUrl) {
|
||||||
|
providers.push({
|
||||||
|
id: "custom",
|
||||||
|
name: oauth.name,
|
||||||
|
type: "oauth",
|
||||||
|
wellKnown: oauth.wellKnownUrl,
|
||||||
|
authorization: { params: { scope: oauth.scope } },
|
||||||
|
clientId: oauth.clientId,
|
||||||
|
clientSecret: oauth.clientSecret,
|
||||||
|
allowDangerousEmailAccountLinking: oauth.allowDangerousEmailAccountLinking,
|
||||||
|
idToken: true,
|
||||||
|
checks: ["pkce", "state"],
|
||||||
|
async profile(profile: Record<string, string>) {
|
||||||
|
const [admin, firstUser] = await Promise.all([
|
||||||
|
isAdmin(profile.email),
|
||||||
|
isFirstUser(),
|
||||||
|
]);
|
||||||
|
return {
|
||||||
|
id: profile.sub,
|
||||||
|
name: profile.name || profile.email,
|
||||||
|
email: profile.email,
|
||||||
|
image: profile.picture,
|
||||||
|
role: admin || firstUser ? "admin" : "user",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const authOptions: NextAuthOptions = {
|
||||||
|
// https://github.com/nextauthjs/next-auth/issues/9493
|
||||||
|
adapter: DrizzleAdapter(db, {
|
||||||
|
usersTable: users,
|
||||||
|
accountsTable: accounts,
|
||||||
|
sessionsTable: sessions,
|
||||||
|
verificationTokensTable: verificationTokens,
|
||||||
|
}) as Adapter,
|
||||||
|
providers: providers,
|
||||||
|
session: {
|
||||||
|
strategy: "jwt",
|
||||||
|
},
|
||||||
|
pages: {
|
||||||
|
signIn: "/signin",
|
||||||
|
signOut: "/signin",
|
||||||
|
error: "/signin",
|
||||||
|
newUser: "/signin",
|
||||||
|
},
|
||||||
|
callbacks: {
|
||||||
|
async signIn({ credentials, profile }) {
|
||||||
|
if (credentials) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!profile?.email) {
|
||||||
|
throw new Error("No profile");
|
||||||
|
}
|
||||||
|
const [{ count: userCount }] = await db
|
||||||
|
.select({ count: count() })
|
||||||
|
.from(users)
|
||||||
|
.where(and(eq(users.email, profile.email)));
|
||||||
|
|
||||||
|
// If it's a new user and signups are disabled, fail the sign in
|
||||||
|
if (userCount === 0 && serverConfig.auth.disableSignups) {
|
||||||
|
throw new Error("Signups are disabled in server config");
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
async jwt({ token, user }) {
|
||||||
|
if (user) {
|
||||||
|
token.user = {
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
email: user.email,
|
||||||
|
image: user.image,
|
||||||
|
role: user.role ?? "user",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
},
|
||||||
|
async session({ session, token }) {
|
||||||
|
session.user = { ...token.user };
|
||||||
|
return session;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const authHandler = NextAuth(authOptions);
|
||||||
|
|
||||||
|
export const getServerAuthSession = () => getServerSession(authOptions);
|
||||||
@ -1,20 +1,27 @@
|
|||||||
{
|
{
|
||||||
"extends": "@lifetracker/typescript-config/nextjs.json",
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"extends": "@lifetracker/typescript-config/base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
"name": "next"
|
"name": "next"
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"./*"
|
||||||
]
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"next-env.d.ts",
|
"next-env.d.ts",
|
||||||
"next.config.mjs",
|
|
||||||
"**/*.ts",
|
"**/*.ts",
|
||||||
"**/*.tsx",
|
"**/*.tsx",
|
||||||
".next/types/**/*.ts"
|
".next/types/**/*.ts"
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"node_modules"
|
"node_modules",
|
||||||
|
".next"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -5,7 +5,7 @@ const databaseURL = "./lifetracker.db";
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
dialect: "sqlite",
|
dialect: "sqlite",
|
||||||
schema: "./schema",
|
schema: "./schema.ts",
|
||||||
out: "./migrations",
|
out: "./migrations",
|
||||||
dbCredentials: {
|
dbCredentials: {
|
||||||
url: databaseURL,
|
url: databaseURL,
|
||||||
|
|||||||
@ -1,20 +1,21 @@
|
|||||||
import "dotenv/config";
|
import "dotenv/config";
|
||||||
import { drizzle } from "drizzle-orm/better-sqlite3";
|
import { drizzle } from "drizzle-orm/better-sqlite3";
|
||||||
import Database from "better-sqlite3";
|
import Database from "better-sqlite3";
|
||||||
import * as schema from "./schema/day";
|
import * as schema from "./schema";
|
||||||
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
import dbConfig from "./drizzle.config";
|
import dbConfig from "./drizzle.config";
|
||||||
|
|
||||||
const sqlite = new Database(dbConfig.dbCredentials.url);
|
const sqlite = new Database(dbConfig.dbCredentials.url);
|
||||||
|
|
||||||
export const db = drizzle(sqlite, { schema });
|
export const db = drizzle(sqlite, { schema });
|
||||||
|
|
||||||
export function getInMemoryDB(runMigrations: boolean) {
|
export function getInMemoryDB(runMigrations: boolean) {
|
||||||
const mem = new Database(":memory:");
|
const mem = new Database(":memory:");
|
||||||
const db = drizzle(mem, { schema, logger: true });
|
const db = drizzle(mem, { schema, logger: true });
|
||||||
if (runMigrations) {
|
if (runMigrations) {
|
||||||
migrate(db, { migrationsFolder: path.resolve(__dirname, "./drizzle") });
|
migrate(db, { migrationsFolder: path.resolve(__dirname, "./migrations") });
|
||||||
}
|
}
|
||||||
return db;
|
return db;
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
53
packages/db/migrations/0002_minor_toxin.sql
Normal file
53
packages/db/migrations/0002_minor_toxin.sql
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
CREATE TABLE `account` (
|
||||||
|
`userId` text NOT NULL,
|
||||||
|
`type` text NOT NULL,
|
||||||
|
`provider` text NOT NULL,
|
||||||
|
`providerAccountId` text NOT NULL,
|
||||||
|
`refresh_token` text,
|
||||||
|
`access_token` text,
|
||||||
|
`expires_at` integer,
|
||||||
|
`token_type` text,
|
||||||
|
`scope` text,
|
||||||
|
`id_token` text,
|
||||||
|
`session_state` text,
|
||||||
|
PRIMARY KEY(`provider`, `providerAccountId`),
|
||||||
|
FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `apiKey` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`name` text NOT NULL,
|
||||||
|
`createdAt` integer NOT NULL,
|
||||||
|
`keyId` text NOT NULL,
|
||||||
|
`keyHash` text NOT NULL,
|
||||||
|
`userId` text NOT NULL,
|
||||||
|
FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `session` (
|
||||||
|
`sessionToken` text PRIMARY KEY NOT NULL,
|
||||||
|
`userId` text NOT NULL,
|
||||||
|
`expires` integer NOT NULL,
|
||||||
|
FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `user` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`name` text NOT NULL,
|
||||||
|
`email` text NOT NULL,
|
||||||
|
`emailVerified` integer,
|
||||||
|
`image` text,
|
||||||
|
`password` text,
|
||||||
|
`role` text DEFAULT 'user'
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `verificationToken` (
|
||||||
|
`identifier` text NOT NULL,
|
||||||
|
`token` text NOT NULL,
|
||||||
|
`expires` integer NOT NULL,
|
||||||
|
PRIMARY KEY(`identifier`, `token`)
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `apiKey_keyId_unique` ON `apiKey` (`keyId`);--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `apiKey_name_userId_unique` ON `apiKey` (`name`,`userId`);--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `user_email_unique` ON `user` (`email`);
|
||||||
3
packages/db/migrations/0003_complex_ares.sql
Normal file
3
packages/db/migrations/0003_complex_ares.sql
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE `days` RENAME TO `day`;--> statement-breakpoint
|
||||||
|
DROP INDEX IF EXISTS `days_date_unique`;--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `day_date_unique` ON `day` (`date`);
|
||||||
400
packages/db/migrations/meta/0002_snapshot.json
Normal file
400
packages/db/migrations/meta/0002_snapshot.json
Normal file
@ -0,0 +1,400 @@
|
|||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "709a800b-1a51-4e31-96ef-0e8dd1c42a95",
|
||||||
|
"prevId": "6ad45ca2-1f2d-4e94-a8fd-b749a8577a61",
|
||||||
|
"tables": {
|
||||||
|
"account": {
|
||||||
|
"name": "account",
|
||||||
|
"columns": {
|
||||||
|
"userId": {
|
||||||
|
"name": "userId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "type",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"provider": {
|
||||||
|
"name": "provider",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"providerAccountId": {
|
||||||
|
"name": "providerAccountId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"refresh_token": {
|
||||||
|
"name": "refresh_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"access_token": {
|
||||||
|
"name": "access_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"token_type": {
|
||||||
|
"name": "token_type",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"name": "scope",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"id_token": {
|
||||||
|
"name": "id_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"session_state": {
|
||||||
|
"name": "session_state",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"account_userId_user_id_fk": {
|
||||||
|
"name": "account_userId_user_id_fk",
|
||||||
|
"tableFrom": "account",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"userId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"account_provider_providerAccountId_pk": {
|
||||||
|
"columns": [
|
||||||
|
"provider",
|
||||||
|
"providerAccountId"
|
||||||
|
],
|
||||||
|
"name": "account_provider_providerAccountId_pk"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"apiKey": {
|
||||||
|
"name": "apiKey",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"keyId": {
|
||||||
|
"name": "keyId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"keyHash": {
|
||||||
|
"name": "keyHash",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"userId": {
|
||||||
|
"name": "userId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"apiKey_keyId_unique": {
|
||||||
|
"name": "apiKey_keyId_unique",
|
||||||
|
"columns": [
|
||||||
|
"keyId"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
},
|
||||||
|
"apiKey_name_userId_unique": {
|
||||||
|
"name": "apiKey_name_userId_unique",
|
||||||
|
"columns": [
|
||||||
|
"name",
|
||||||
|
"userId"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"apiKey_userId_user_id_fk": {
|
||||||
|
"name": "apiKey_userId_user_id_fk",
|
||||||
|
"tableFrom": "apiKey",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"userId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"session": {
|
||||||
|
"name": "session",
|
||||||
|
"columns": {
|
||||||
|
"sessionToken": {
|
||||||
|
"name": "sessionToken",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"userId": {
|
||||||
|
"name": "userId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"expires": {
|
||||||
|
"name": "expires",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"session_userId_user_id_fk": {
|
||||||
|
"name": "session_userId_user_id_fk",
|
||||||
|
"tableFrom": "session",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"userId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"name": "user",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"emailVerified": {
|
||||||
|
"name": "emailVerified",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"image": {
|
||||||
|
"name": "image",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"name": "password",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"name": "role",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'user'"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"user_email_unique": {
|
||||||
|
"name": "user_email_unique",
|
||||||
|
"columns": [
|
||||||
|
"email"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"verificationToken": {
|
||||||
|
"name": "verificationToken",
|
||||||
|
"columns": {
|
||||||
|
"identifier": {
|
||||||
|
"name": "identifier",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"token": {
|
||||||
|
"name": "token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"expires": {
|
||||||
|
"name": "expires",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"verificationToken_identifier_token_pk": {
|
||||||
|
"columns": [
|
||||||
|
"identifier",
|
||||||
|
"token"
|
||||||
|
],
|
||||||
|
"name": "verificationToken_identifier_token_pk"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"days": {
|
||||||
|
"name": "days",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"mood": {
|
||||||
|
"name": "mood",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"date": {
|
||||||
|
"name": "date",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"comment": {
|
||||||
|
"name": "comment",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"days_date_unique": {
|
||||||
|
"name": "days_date_unique",
|
||||||
|
"columns": [
|
||||||
|
"date"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"_meta": {
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {},
|
||||||
|
"columns": {}
|
||||||
|
},
|
||||||
|
"internal": {
|
||||||
|
"indexes": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
402
packages/db/migrations/meta/0003_snapshot.json
Normal file
402
packages/db/migrations/meta/0003_snapshot.json
Normal file
@ -0,0 +1,402 @@
|
|||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "67edcacc-ad95-4e34-b368-4beeda535072",
|
||||||
|
"prevId": "709a800b-1a51-4e31-96ef-0e8dd1c42a95",
|
||||||
|
"tables": {
|
||||||
|
"account": {
|
||||||
|
"name": "account",
|
||||||
|
"columns": {
|
||||||
|
"userId": {
|
||||||
|
"name": "userId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "type",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"provider": {
|
||||||
|
"name": "provider",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"providerAccountId": {
|
||||||
|
"name": "providerAccountId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"refresh_token": {
|
||||||
|
"name": "refresh_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"access_token": {
|
||||||
|
"name": "access_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"token_type": {
|
||||||
|
"name": "token_type",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"name": "scope",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"id_token": {
|
||||||
|
"name": "id_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"session_state": {
|
||||||
|
"name": "session_state",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"account_userId_user_id_fk": {
|
||||||
|
"name": "account_userId_user_id_fk",
|
||||||
|
"tableFrom": "account",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"userId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"account_provider_providerAccountId_pk": {
|
||||||
|
"columns": [
|
||||||
|
"provider",
|
||||||
|
"providerAccountId"
|
||||||
|
],
|
||||||
|
"name": "account_provider_providerAccountId_pk"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"apiKey": {
|
||||||
|
"name": "apiKey",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"keyId": {
|
||||||
|
"name": "keyId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"keyHash": {
|
||||||
|
"name": "keyHash",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"userId": {
|
||||||
|
"name": "userId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"apiKey_keyId_unique": {
|
||||||
|
"name": "apiKey_keyId_unique",
|
||||||
|
"columns": [
|
||||||
|
"keyId"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
},
|
||||||
|
"apiKey_name_userId_unique": {
|
||||||
|
"name": "apiKey_name_userId_unique",
|
||||||
|
"columns": [
|
||||||
|
"name",
|
||||||
|
"userId"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"apiKey_userId_user_id_fk": {
|
||||||
|
"name": "apiKey_userId_user_id_fk",
|
||||||
|
"tableFrom": "apiKey",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"userId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"session": {
|
||||||
|
"name": "session",
|
||||||
|
"columns": {
|
||||||
|
"sessionToken": {
|
||||||
|
"name": "sessionToken",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"userId": {
|
||||||
|
"name": "userId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"expires": {
|
||||||
|
"name": "expires",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"session_userId_user_id_fk": {
|
||||||
|
"name": "session_userId_user_id_fk",
|
||||||
|
"tableFrom": "session",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"userId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"name": "user",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"emailVerified": {
|
||||||
|
"name": "emailVerified",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"image": {
|
||||||
|
"name": "image",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"name": "password",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"name": "role",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'user'"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"user_email_unique": {
|
||||||
|
"name": "user_email_unique",
|
||||||
|
"columns": [
|
||||||
|
"email"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"verificationToken": {
|
||||||
|
"name": "verificationToken",
|
||||||
|
"columns": {
|
||||||
|
"identifier": {
|
||||||
|
"name": "identifier",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"token": {
|
||||||
|
"name": "token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"expires": {
|
||||||
|
"name": "expires",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"verificationToken_identifier_token_pk": {
|
||||||
|
"columns": [
|
||||||
|
"identifier",
|
||||||
|
"token"
|
||||||
|
],
|
||||||
|
"name": "verificationToken_identifier_token_pk"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"day": {
|
||||||
|
"name": "day",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"mood": {
|
||||||
|
"name": "mood",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"date": {
|
||||||
|
"name": "date",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"comment": {
|
||||||
|
"name": "comment",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"day_date_unique": {
|
||||||
|
"name": "day_date_unique",
|
||||||
|
"columns": [
|
||||||
|
"date"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"_meta": {
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {
|
||||||
|
"\"days\"": "\"day\""
|
||||||
|
},
|
||||||
|
"columns": {}
|
||||||
|
},
|
||||||
|
"internal": {
|
||||||
|
"indexes": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -15,6 +15,20 @@
|
|||||||
"when": 1731100507972,
|
"when": 1731100507972,
|
||||||
"tag": "0001_hard_lionheart",
|
"tag": "0001_hard_lionheart",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 2,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1731542160896,
|
||||||
|
"tag": "0002_minor_toxin",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 3,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1731542202120,
|
||||||
|
"tag": "0003_complex_ares",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -12,6 +12,8 @@
|
|||||||
"studio": "drizzle-kit studio"
|
"studio": "drizzle-kit studio"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@auth/core": "^0.37.3",
|
||||||
|
"@paralleldrive/cuid2": "^2.2.2",
|
||||||
"better-sqlite3": "^9.4.3",
|
"better-sqlite3": "^9.4.3",
|
||||||
"dotenv": "^16.4.1",
|
"dotenv": "^16.4.1",
|
||||||
"drizzle-orm": "^0.33.0",
|
"drizzle-orm": "^0.33.0",
|
||||||
|
|||||||
106
packages/db/schema.ts
Normal file
106
packages/db/schema.ts
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import type { AdapterAccount } from "@auth/core/adapters";
|
||||||
|
import { createId } from "@paralleldrive/cuid2";
|
||||||
|
import { relations } from "drizzle-orm";
|
||||||
|
import {
|
||||||
|
AnySQLiteColumn,
|
||||||
|
index,
|
||||||
|
integer,
|
||||||
|
primaryKey,
|
||||||
|
sqliteTable,
|
||||||
|
text,
|
||||||
|
unique,
|
||||||
|
} from "drizzle-orm/sqlite-core";
|
||||||
|
|
||||||
|
function createdAtField() {
|
||||||
|
return integer("createdAt", { mode: "timestamp" })
|
||||||
|
.notNull()
|
||||||
|
.$defaultFn(() => new Date());
|
||||||
|
}
|
||||||
|
|
||||||
|
export const apiKeys = sqliteTable(
|
||||||
|
"apiKey",
|
||||||
|
{
|
||||||
|
id: text("id")
|
||||||
|
.notNull()
|
||||||
|
.primaryKey()
|
||||||
|
.$defaultFn(() => createId()),
|
||||||
|
name: text("name").notNull(),
|
||||||
|
createdAt: createdAtField(),
|
||||||
|
keyId: text("keyId").notNull().unique(),
|
||||||
|
keyHash: text("keyHash").notNull(),
|
||||||
|
userId: text("userId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => users.id, { onDelete: "cascade" }),
|
||||||
|
},
|
||||||
|
(ak) => ({
|
||||||
|
unq: unique().on(ak.name, ak.userId),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
export const users = sqliteTable("user", {
|
||||||
|
id: text("id")
|
||||||
|
.notNull()
|
||||||
|
.primaryKey()
|
||||||
|
.$defaultFn(() => createId()),
|
||||||
|
name: text("name").notNull(),
|
||||||
|
email: text("email").notNull().unique(),
|
||||||
|
emailVerified: integer("emailVerified", { mode: "timestamp_ms" }),
|
||||||
|
image: text("image"),
|
||||||
|
password: text("password"),
|
||||||
|
role: text("role", { enum: ["admin", "user"] }).default("user"),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const accounts = sqliteTable(
|
||||||
|
"account",
|
||||||
|
{
|
||||||
|
userId: text("userId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => users.id, { onDelete: "cascade" }),
|
||||||
|
type: text("type").$type<AdapterAccount["type"]>().notNull(),
|
||||||
|
provider: text("provider").notNull(),
|
||||||
|
providerAccountId: text("providerAccountId").notNull(),
|
||||||
|
refresh_token: text("refresh_token"),
|
||||||
|
access_token: text("access_token"),
|
||||||
|
expires_at: integer("expires_at"),
|
||||||
|
token_type: text("token_type"),
|
||||||
|
scope: text("scope"),
|
||||||
|
id_token: text("id_token"),
|
||||||
|
session_state: text("session_state"),
|
||||||
|
},
|
||||||
|
(account) => ({
|
||||||
|
compoundKey: primaryKey({
|
||||||
|
columns: [account.provider, account.providerAccountId],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const sessions = sqliteTable("session", {
|
||||||
|
sessionToken: text("sessionToken")
|
||||||
|
.notNull()
|
||||||
|
.primaryKey()
|
||||||
|
.$defaultFn(() => createId()),
|
||||||
|
userId: text("userId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => users.id, { onDelete: "cascade" }),
|
||||||
|
expires: integer("expires", { mode: "timestamp_ms" }).notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const verificationTokens = sqliteTable(
|
||||||
|
"verificationToken",
|
||||||
|
{
|
||||||
|
identifier: text("identifier").notNull(),
|
||||||
|
token: text("token").notNull(),
|
||||||
|
expires: integer("expires", { mode: "timestamp_ms" }).notNull(),
|
||||||
|
},
|
||||||
|
(vt) => ({
|
||||||
|
compoundKey: primaryKey({ columns: [vt.identifier, vt.token] }),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const days = sqliteTable("day", {
|
||||||
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||||
|
mood: integer("mood"),
|
||||||
|
date: text("date").notNull().unique(),
|
||||||
|
comment: text("comment").notNull(),
|
||||||
|
});
|
||||||
99
packages/db/schema/auth.ts
Normal file
99
packages/db/schema/auth.ts
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import type { AdapterAccount } from "@auth/core/adapters";
|
||||||
|
import { createId } from "@paralleldrive/cuid2";
|
||||||
|
import { relations } from "drizzle-orm";
|
||||||
|
import {
|
||||||
|
AnySQLiteColumn,
|
||||||
|
index,
|
||||||
|
integer,
|
||||||
|
primaryKey,
|
||||||
|
sqliteTable,
|
||||||
|
text,
|
||||||
|
unique,
|
||||||
|
} from "drizzle-orm/sqlite-core";
|
||||||
|
|
||||||
|
function createdAtField() {
|
||||||
|
return integer("createdAt", { mode: "timestamp" })
|
||||||
|
.notNull()
|
||||||
|
.$defaultFn(() => new Date());
|
||||||
|
}
|
||||||
|
|
||||||
|
export const apiKeys = sqliteTable(
|
||||||
|
"apiKey",
|
||||||
|
{
|
||||||
|
id: text("id")
|
||||||
|
.notNull()
|
||||||
|
.primaryKey()
|
||||||
|
.$defaultFn(() => createId()),
|
||||||
|
name: text("name").notNull(),
|
||||||
|
createdAt: createdAtField(),
|
||||||
|
keyId: text("keyId").notNull().unique(),
|
||||||
|
keyHash: text("keyHash").notNull(),
|
||||||
|
userId: text("userId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => users.id, { onDelete: "cascade" }),
|
||||||
|
},
|
||||||
|
(ak) => ({
|
||||||
|
unq: unique().on(ak.name, ak.userId),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
export const users = sqliteTable("user", {
|
||||||
|
id: text("id")
|
||||||
|
.notNull()
|
||||||
|
.primaryKey()
|
||||||
|
.$defaultFn(() => createId()),
|
||||||
|
name: text("name").notNull(),
|
||||||
|
email: text("email").notNull().unique(),
|
||||||
|
emailVerified: integer("emailVerified", { mode: "timestamp_ms" }),
|
||||||
|
image: text("image"),
|
||||||
|
password: text("password"),
|
||||||
|
role: text("role", { enum: ["admin", "user"] }).default("user"),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const accounts = sqliteTable(
|
||||||
|
"account",
|
||||||
|
{
|
||||||
|
userId: text("userId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => users.id, { onDelete: "cascade" }),
|
||||||
|
type: text("type").$type<AdapterAccount["type"]>().notNull(),
|
||||||
|
provider: text("provider").notNull(),
|
||||||
|
providerAccountId: text("providerAccountId").notNull(),
|
||||||
|
refresh_token: text("refresh_token"),
|
||||||
|
access_token: text("access_token"),
|
||||||
|
expires_at: integer("expires_at"),
|
||||||
|
token_type: text("token_type"),
|
||||||
|
scope: text("scope"),
|
||||||
|
id_token: text("id_token"),
|
||||||
|
session_state: text("session_state"),
|
||||||
|
},
|
||||||
|
(account) => ({
|
||||||
|
compoundKey: primaryKey({
|
||||||
|
columns: [account.provider, account.providerAccountId],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const sessions = sqliteTable("session", {
|
||||||
|
sessionToken: text("sessionToken")
|
||||||
|
.notNull()
|
||||||
|
.primaryKey()
|
||||||
|
.$defaultFn(() => createId()),
|
||||||
|
userId: text("userId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => users.id, { onDelete: "cascade" }),
|
||||||
|
expires: integer("expires", { mode: "timestamp_ms" }).notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const verificationTokens = sqliteTable(
|
||||||
|
"verificationToken",
|
||||||
|
{
|
||||||
|
identifier: text("identifier").notNull(),
|
||||||
|
token: text("token").notNull(),
|
||||||
|
expires: integer("expires", { mode: "timestamp_ms" }).notNull(),
|
||||||
|
},
|
||||||
|
(vt) => ({
|
||||||
|
compoundKey: primaryKey({ columns: [vt.identifier, vt.token] }),
|
||||||
|
}),
|
||||||
|
);
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { sqliteTable, integer, text } from "drizzle-orm/sqlite-core";
|
import { sqliteTable, integer, text } from "drizzle-orm/sqlite-core";
|
||||||
|
|
||||||
export const days = sqliteTable("days", {
|
export const days = sqliteTable("day", {
|
||||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||||
mood: integer("mood"),
|
mood: integer("mood"),
|
||||||
date: text("date").notNull().unique(),
|
date: text("date").notNull().unique(),
|
||||||
|
|||||||
1
packages/shared/README.md
Normal file
1
packages/shared/README.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
# `@lifetracker/shared`
|
||||||
150
packages/shared/config.ts
Normal file
150
packages/shared/config.ts
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const stringBool = (defaultValue: string) =>
|
||||||
|
z
|
||||||
|
.string()
|
||||||
|
.default(defaultValue)
|
||||||
|
.refine((s) => s === "true" || s === "false")
|
||||||
|
.transform((s) => s === "true");
|
||||||
|
|
||||||
|
const allEnv = z.object({
|
||||||
|
API_URL: z.string().url().default("http://localhost:3000"),
|
||||||
|
DISABLE_SIGNUPS: stringBool("false"),
|
||||||
|
DISABLE_PASSWORD_AUTH: stringBool("false"),
|
||||||
|
OAUTH_ALLOW_DANGEROUS_EMAIL_ACCOUNT_LINKING: stringBool("false"),
|
||||||
|
OAUTH_WELLKNOWN_URL: z.string().url().optional(),
|
||||||
|
OAUTH_CLIENT_SECRET: z.string().optional(),
|
||||||
|
OAUTH_CLIENT_ID: z.string().optional(),
|
||||||
|
OAUTH_SCOPE: z.string().default("openid email profile"),
|
||||||
|
OAUTH_PROVIDER_NAME: z.string().default("Custom Provider"),
|
||||||
|
OPENAI_API_KEY: z.string().optional(),
|
||||||
|
OPENAI_BASE_URL: z.string().url().optional(),
|
||||||
|
OLLAMA_BASE_URL: z.string().url().optional(),
|
||||||
|
OLLAMA_KEEP_ALIVE: z.string().optional(),
|
||||||
|
INFERENCE_JOB_TIMEOUT_SEC: z.coerce.number().default(30),
|
||||||
|
INFERENCE_TEXT_MODEL: z.string().default("gpt-4o-mini"),
|
||||||
|
INFERENCE_IMAGE_MODEL: z.string().default("gpt-4o-mini"),
|
||||||
|
INFERENCE_CONTEXT_LENGTH: z.coerce.number().default(2048),
|
||||||
|
OCR_CACHE_DIR: z.string().optional(),
|
||||||
|
OCR_LANGS: z
|
||||||
|
.string()
|
||||||
|
.default("eng")
|
||||||
|
.transform((val) => val.split(",")),
|
||||||
|
OCR_CONFIDENCE_THRESHOLD: z.coerce.number().default(50),
|
||||||
|
CRAWLER_HEADLESS_BROWSER: stringBool("true"),
|
||||||
|
BROWSER_WEB_URL: z.string().url().optional(),
|
||||||
|
BROWSER_WEBSOCKET_URL: z.string().url().optional(),
|
||||||
|
BROWSER_CONNECT_ONDEMAND: stringBool("false"),
|
||||||
|
CRAWLER_JOB_TIMEOUT_SEC: z.coerce.number().default(60),
|
||||||
|
CRAWLER_NAVIGATE_TIMEOUT_SEC: z.coerce.number().default(30),
|
||||||
|
CRAWLER_NUM_WORKERS: z.coerce.number().default(1),
|
||||||
|
CRAWLER_DOWNLOAD_BANNER_IMAGE: stringBool("true"),
|
||||||
|
CRAWLER_STORE_SCREENSHOT: stringBool("true"),
|
||||||
|
CRAWLER_FULL_PAGE_SCREENSHOT: stringBool("false"),
|
||||||
|
CRAWLER_FULL_PAGE_ARCHIVE: stringBool("false"),
|
||||||
|
CRAWLER_VIDEO_DOWNLOAD: stringBool("false"),
|
||||||
|
CRAWLER_VIDEO_DOWNLOAD_MAX_SIZE: z.coerce.number().default(50),
|
||||||
|
CRAWLER_VIDEO_DOWNLOAD_TIMEOUT_SEC: z.coerce.number().default(10 * 60),
|
||||||
|
MEILI_ADDR: z.string().optional(),
|
||||||
|
MEILI_MASTER_KEY: z.string().default(""),
|
||||||
|
LOG_LEVEL: z.string().default("debug"),
|
||||||
|
DEMO_MODE: stringBool("false"),
|
||||||
|
DEMO_MODE_EMAIL: z.string().optional(),
|
||||||
|
DEMO_MODE_PASSWORD: z.string().optional(),
|
||||||
|
DATA_DIR: z.string().default(""),
|
||||||
|
MAX_ASSET_SIZE_MB: z.coerce.number().default(4),
|
||||||
|
INFERENCE_LANG: z.string().default("english"),
|
||||||
|
// Build only flag
|
||||||
|
SERVER_VERSION: z.string().optional(),
|
||||||
|
DISABLE_NEW_RELEASE_CHECK: stringBool("false"),
|
||||||
|
|
||||||
|
// A flag to detect if the user is running in the old separete containers setup
|
||||||
|
USING_LEGACY_SEPARATE_CONTAINERS: stringBool("false"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const serverConfigSchema = allEnv.transform((val) => {
|
||||||
|
return {
|
||||||
|
apiUrl: val.API_URL,
|
||||||
|
auth: {
|
||||||
|
disableSignups: val.DISABLE_SIGNUPS,
|
||||||
|
disablePasswordAuth: val.DISABLE_PASSWORD_AUTH,
|
||||||
|
oauth: {
|
||||||
|
allowDangerousEmailAccountLinking:
|
||||||
|
val.OAUTH_ALLOW_DANGEROUS_EMAIL_ACCOUNT_LINKING,
|
||||||
|
wellKnownUrl: val.OAUTH_WELLKNOWN_URL,
|
||||||
|
clientSecret: val.OAUTH_CLIENT_SECRET,
|
||||||
|
clientId: val.OAUTH_CLIENT_ID,
|
||||||
|
scope: val.OAUTH_SCOPE,
|
||||||
|
name: val.OAUTH_PROVIDER_NAME,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
inference: {
|
||||||
|
jobTimeoutSec: val.INFERENCE_JOB_TIMEOUT_SEC,
|
||||||
|
openAIApiKey: val.OPENAI_API_KEY,
|
||||||
|
openAIBaseUrl: val.OPENAI_BASE_URL,
|
||||||
|
ollamaBaseUrl: val.OLLAMA_BASE_URL,
|
||||||
|
ollamaKeepAlive: val.OLLAMA_KEEP_ALIVE,
|
||||||
|
textModel: val.INFERENCE_TEXT_MODEL,
|
||||||
|
imageModel: val.INFERENCE_IMAGE_MODEL,
|
||||||
|
inferredTagLang: val.INFERENCE_LANG,
|
||||||
|
contextLength: val.INFERENCE_CONTEXT_LENGTH,
|
||||||
|
},
|
||||||
|
crawler: {
|
||||||
|
numWorkers: val.CRAWLER_NUM_WORKERS,
|
||||||
|
headlessBrowser: val.CRAWLER_HEADLESS_BROWSER,
|
||||||
|
browserWebUrl: val.BROWSER_WEB_URL,
|
||||||
|
browserWebSocketUrl: val.BROWSER_WEBSOCKET_URL,
|
||||||
|
browserConnectOnDemand: val.BROWSER_CONNECT_ONDEMAND,
|
||||||
|
jobTimeoutSec: val.CRAWLER_JOB_TIMEOUT_SEC,
|
||||||
|
navigateTimeoutSec: val.CRAWLER_NAVIGATE_TIMEOUT_SEC,
|
||||||
|
downloadBannerImage: val.CRAWLER_DOWNLOAD_BANNER_IMAGE,
|
||||||
|
storeScreenshot: val.CRAWLER_STORE_SCREENSHOT,
|
||||||
|
fullPageScreenshot: val.CRAWLER_FULL_PAGE_SCREENSHOT,
|
||||||
|
fullPageArchive: val.CRAWLER_FULL_PAGE_ARCHIVE,
|
||||||
|
downloadVideo: val.CRAWLER_VIDEO_DOWNLOAD,
|
||||||
|
maxVideoDownloadSize: val.CRAWLER_VIDEO_DOWNLOAD_MAX_SIZE,
|
||||||
|
downloadVideoTimeout: val.CRAWLER_VIDEO_DOWNLOAD_TIMEOUT_SEC,
|
||||||
|
},
|
||||||
|
ocr: {
|
||||||
|
langs: val.OCR_LANGS,
|
||||||
|
cacheDir: val.OCR_CACHE_DIR,
|
||||||
|
confidenceThreshold: val.OCR_CONFIDENCE_THRESHOLD,
|
||||||
|
},
|
||||||
|
meilisearch: val.MEILI_ADDR
|
||||||
|
? {
|
||||||
|
address: val.MEILI_ADDR,
|
||||||
|
key: val.MEILI_MASTER_KEY,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
logLevel: val.LOG_LEVEL,
|
||||||
|
demoMode: val.DEMO_MODE
|
||||||
|
? {
|
||||||
|
email: val.DEMO_MODE_EMAIL,
|
||||||
|
password: val.DEMO_MODE_PASSWORD,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
dataDir: val.DATA_DIR,
|
||||||
|
maxAssetSizeMb: val.MAX_ASSET_SIZE_MB,
|
||||||
|
serverVersion: val.SERVER_VERSION,
|
||||||
|
disableNewReleaseCheck: val.DISABLE_NEW_RELEASE_CHECK,
|
||||||
|
usingLegacySeparateContainers: val.USING_LEGACY_SEPARATE_CONTAINERS,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const serverConfig = serverConfigSchema.parse(process.env);
|
||||||
|
// Always explicitly pick up stuff from server config to avoid accidentally leaking stuff
|
||||||
|
export const clientConfig = {
|
||||||
|
demoMode: serverConfig.demoMode,
|
||||||
|
auth: {
|
||||||
|
disableSignups: serverConfig.auth.disableSignups,
|
||||||
|
disablePasswordAuth: serverConfig.auth.disablePasswordAuth,
|
||||||
|
},
|
||||||
|
inference: {
|
||||||
|
inferredTagLang: serverConfig.inference.inferredTagLang,
|
||||||
|
},
|
||||||
|
serverVersion: serverConfig.serverVersion,
|
||||||
|
disableNewReleaseCheck: serverConfig.disableNewReleaseCheck,
|
||||||
|
};
|
||||||
|
export type ClientConfig = typeof clientConfig;
|
||||||
|
|
||||||
|
export default serverConfig;
|
||||||
36
packages/shared/logger.ts
Normal file
36
packages/shared/logger.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import winston from "winston";
|
||||||
|
|
||||||
|
import serverConfig from "./config";
|
||||||
|
|
||||||
|
const logger = winston.createLogger({
|
||||||
|
level: serverConfig.logLevel,
|
||||||
|
format: winston.format.combine(
|
||||||
|
winston.format.timestamp(),
|
||||||
|
winston.format.colorize(),
|
||||||
|
winston.format.printf(
|
||||||
|
(info) => `${info.timestamp} ${info.level}: ${info.message}`,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
transports: [new winston.transports.Console()],
|
||||||
|
});
|
||||||
|
|
||||||
|
export default logger;
|
||||||
|
|
||||||
|
export const authFailureLogger = winston.createLogger({
|
||||||
|
level: "debug",
|
||||||
|
format: winston.format.combine(
|
||||||
|
winston.format.timestamp(),
|
||||||
|
winston.format.printf(
|
||||||
|
(info) => `${info.timestamp} ${info.level}: ${info.message}`,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
transports: [
|
||||||
|
new winston.transports.Console(),
|
||||||
|
new winston.transports.File({
|
||||||
|
filename: "auth_failures.log",
|
||||||
|
dirname: serverConfig.dataDir,
|
||||||
|
maxFiles: 2,
|
||||||
|
maxsize: 1024 * 1024,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
27
packages/shared/package.json
Normal file
27
packages/shared/package.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
|
"name": "@lifetracker/shared",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"winston": "^3.17.0",
|
||||||
|
"zod": "^3.23.8"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@lifetracker/eslint-config": "workspace:^",
|
||||||
|
"@lifetracker/typescript-config": "workspace:^"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"format": "prettier . --ignore-path ../../.prettierignore",
|
||||||
|
"lint": "eslint ."
|
||||||
|
},
|
||||||
|
"main": "index.ts",
|
||||||
|
"eslintConfig": {
|
||||||
|
"root": true,
|
||||||
|
"extends": [
|
||||||
|
"@lifetracker/eslint-config/base"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
13
packages/shared/tsconfig.json
Normal file
13
packages/shared/tsconfig.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"extends": "@lifetracker/typescript-config/node.json",
|
||||||
|
"include": [
|
||||||
|
"**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
|
||||||
|
},
|
||||||
|
}
|
||||||
115
packages/trpc/auth.ts
Normal file
115
packages/trpc/auth.ts
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
import { randomBytes } from "crypto";
|
||||||
|
import * as bcrypt from "bcryptjs";
|
||||||
|
|
||||||
|
import { db } from "@lifetracker/db";
|
||||||
|
import { apiKeys } from "@lifetracker/db/schema/auth";
|
||||||
|
import serverConfig from "@lifetracker/shared/config";
|
||||||
|
import { authFailureLogger } from "@lifetracker/shared/logger";
|
||||||
|
|
||||||
|
// API Keys
|
||||||
|
|
||||||
|
const BCRYPT_SALT_ROUNDS = 10;
|
||||||
|
const API_KEY_PREFIX = "ak1";
|
||||||
|
|
||||||
|
export async function generateApiKey(name: string, userId: string) {
|
||||||
|
const id = randomBytes(10).toString("hex");
|
||||||
|
const secret = randomBytes(10).toString("hex");
|
||||||
|
const secretHash = await bcrypt.hash(secret, BCRYPT_SALT_ROUNDS);
|
||||||
|
|
||||||
|
const plain = `${API_KEY_PREFIX}_${id}_${secret}`;
|
||||||
|
|
||||||
|
const key = (
|
||||||
|
await db
|
||||||
|
.insert(apiKeys)
|
||||||
|
.values({
|
||||||
|
name: name,
|
||||||
|
userId: userId,
|
||||||
|
keyId: id,
|
||||||
|
keyHash: secretHash,
|
||||||
|
})
|
||||||
|
.returning()
|
||||||
|
)[0];
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: key.id,
|
||||||
|
name: key.name,
|
||||||
|
createdAt: key.createdAt,
|
||||||
|
key: plain,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function parseApiKey(plain: string) {
|
||||||
|
const parts = plain.split("_");
|
||||||
|
if (parts.length != 3) {
|
||||||
|
throw new Error(
|
||||||
|
`Malformed API key. API keys should have 3 segments, found ${parts.length} instead.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (parts[0] !== API_KEY_PREFIX) {
|
||||||
|
throw new Error(`Malformed API key. Got unexpected key prefix.`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
keyId: parts[1],
|
||||||
|
keySecret: parts[2],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function authenticateApiKey(key: string) {
|
||||||
|
const { keyId, keySecret } = parseApiKey(key);
|
||||||
|
const apiKey = await db.query.apiKeys.findFirst({
|
||||||
|
where: (k, { eq }) => eq(k.keyId, keyId),
|
||||||
|
with: {
|
||||||
|
user: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
throw new Error("API key not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const hash = apiKey.keyHash;
|
||||||
|
|
||||||
|
const validation = await bcrypt.compare(keySecret, hash);
|
||||||
|
if (!validation) {
|
||||||
|
throw new Error("Invalid API Key");
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiKey.user;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function hashPassword(password: string) {
|
||||||
|
return bcrypt.hash(password, BCRYPT_SALT_ROUNDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function validatePassword(email: string, password: string) {
|
||||||
|
if (serverConfig.auth.disablePasswordAuth) {
|
||||||
|
throw new Error("Password authentication is currently disabled");
|
||||||
|
}
|
||||||
|
const user = await db.query.users.findFirst({
|
||||||
|
where: (u, { eq }) => eq(u.email, email),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new Error("User not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.password) {
|
||||||
|
throw new Error("This user doesn't have a password defined");
|
||||||
|
}
|
||||||
|
|
||||||
|
const validation = await bcrypt.compare(password, user.password);
|
||||||
|
if (!validation) {
|
||||||
|
throw new Error("Wrong password");
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logAuthenticationError(
|
||||||
|
user: string,
|
||||||
|
message: string,
|
||||||
|
ip: string | null,
|
||||||
|
): void {
|
||||||
|
authFailureLogger.error(
|
||||||
|
`Authentication error. User: "${user}", Message: "${message}", IP-Address: "${ip}"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,5 +1,23 @@
|
|||||||
import { initTRPC } from "@trpc/server";
|
import { initTRPC } from "@trpc/server";
|
||||||
|
import type { db } from "@lifetracker/db";
|
||||||
|
|
||||||
const t = initTRPC.create();
|
const t = initTRPC.create();
|
||||||
export const router = t.router;
|
export const router = t.router;
|
||||||
export const publicProcedure = t.procedure;
|
export const publicProcedure = t.procedure;
|
||||||
|
|
||||||
|
export const createCallerFactory = t.createCallerFactory;
|
||||||
|
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
id: string;
|
||||||
|
name?: string | null | undefined;
|
||||||
|
email?: string | null | undefined;
|
||||||
|
role: "admin" | "user" | null;
|
||||||
|
}
|
||||||
|
export interface Context {
|
||||||
|
user: User | null;
|
||||||
|
db: typeof db;
|
||||||
|
req: {
|
||||||
|
ip: string | null;
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -11,6 +11,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lifetracker/db": "workspace:*",
|
"@lifetracker/db": "workspace:*",
|
||||||
|
"@lifetracker/shared": "workspace:^",
|
||||||
"@lifetracker/ui": "workspace:*",
|
"@lifetracker/ui": "workspace:*",
|
||||||
"@trpc/server": "^10.45.2",
|
"@trpc/server": "^10.45.2",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
|
|||||||
470
pnpm-lock.yaml
generated
470
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user