aboutsummaryrefslogtreecommitdiff
path: root/apps/web/server
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/server')
-rw-r--r--apps/web/server/auth.ts34
-rw-r--r--apps/web/server/db/index.ts5
-rw-r--r--apps/web/server/db/schema.ts202
-rw-r--r--apps/web/server/encrypt.ts95
4 files changed, 336 insertions, 0 deletions
diff --git a/apps/web/server/auth.ts b/apps/web/server/auth.ts
new file mode 100644
index 00000000..c4e426d4
--- /dev/null
+++ b/apps/web/server/auth.ts
@@ -0,0 +1,34 @@
+import NextAuth, { NextAuthResult } from "next-auth";
+import Google from "next-auth/providers/google";
+import { DrizzleAdapter } from "@auth/drizzle-adapter";
+import { db } from "./db";
+import { accounts, sessions, users, verificationTokens } from "./db/schema";
+
+export const {
+ handlers: { GET, POST },
+ signIn,
+ signOut,
+ auth,
+} = NextAuth({
+ secret: process.env.BACKEND_SECURITY_KEY,
+ // callbacks: {
+ // session: ({ session, token, user }) => ({
+ // ...session,
+ // user: {
+ // ...session.user,
+ // },
+ // }),
+ // },
+ adapter: DrizzleAdapter(db, {
+ usersTable: users,
+ accountsTable: accounts,
+ sessionsTable: sessions,
+ verificationTokensTable: verificationTokens,
+ }),
+ providers: [
+ Google({
+ clientId: process.env.GOOGLE_CLIENT_ID,
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET,
+ }),
+ ],
+});
diff --git a/apps/web/server/db/index.ts b/apps/web/server/db/index.ts
new file mode 100644
index 00000000..4d671bea
--- /dev/null
+++ b/apps/web/server/db/index.ts
@@ -0,0 +1,5 @@
+import { drizzle } from "drizzle-orm/d1";
+
+import * as schema from "./schema";
+
+export const db = drizzle(process.env.DATABASE, { schema, logger: true });
diff --git a/apps/web/server/db/schema.ts b/apps/web/server/db/schema.ts
new file mode 100644
index 00000000..69372a35
--- /dev/null
+++ b/apps/web/server/db/schema.ts
@@ -0,0 +1,202 @@
+import { relations, sql } from "drizzle-orm";
+import {
+ index,
+ int,
+ primaryKey,
+ sqliteTableCreator,
+ text,
+ integer,
+} from "drizzle-orm/sqlite-core";
+import type { AdapterAccountType } from "next-auth/adapters";
+
+export const createTable = sqliteTableCreator((name) => `${name}`);
+
+export const users = createTable(
+ "user",
+ {
+ id: text("id")
+ .primaryKey()
+ .$defaultFn(() => crypto.randomUUID()),
+ name: text("name"),
+ email: text("email").notNull(),
+ emailVerified: integer("emailVerified", { mode: "timestamp_ms" }),
+ image: text("image"),
+ telegramId: text("telegramId"),
+ },
+ (user) => ({
+ emailIdx: index("users_email_idx").on(user.email),
+ telegramIdx: index("users_telegram_idx").on(user.telegramId),
+ idIdx: index("users_id_idx").on(user.id),
+ }),
+);
+
+export type User = typeof users.$inferSelect;
+
+export const accounts = createTable(
+ "account",
+ {
+ userId: text("userId")
+ .notNull()
+ .references(() => users.id, { onDelete: "cascade" }),
+ type: text("type").$type<AdapterAccountType>().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 = createTable("session", {
+ sessionToken: text("sessionToken").primaryKey(),
+ userId: text("userId")
+ .notNull()
+ .references(() => users.id, { onDelete: "cascade" }),
+ expires: integer("expires", { mode: "timestamp_ms" }).notNull(),
+});
+
+export const verificationTokens = createTable(
+ "verificationToken",
+ {
+ identifier: text("identifier").notNull(),
+ token: text("token").notNull(),
+ expires: integer("expires", { mode: "timestamp_ms" }).notNull(),
+ },
+ (verificationToken) => ({
+ compositePk: primaryKey({
+ columns: [verificationToken.identifier, verificationToken.token],
+ }),
+ }),
+);
+
+export const authenticators = createTable(
+ "authenticator",
+ {
+ credentialID: text("credentialID").notNull().unique(),
+ userId: text("userId")
+ .notNull()
+ .references(() => users.id, { onDelete: "cascade" }),
+ providerAccountId: text("providerAccountId").notNull(),
+ credentialPublicKey: text("credentialPublicKey").notNull(),
+ counter: integer("counter").notNull(),
+ credentialDeviceType: text("credentialDeviceType").notNull(),
+ credentialBackedUp: integer("credentialBackedUp", {
+ mode: "boolean",
+ }).notNull(),
+ transports: text("transports"),
+ },
+ (authenticator) => ({
+ compositePK: primaryKey({
+ columns: [authenticator.userId, authenticator.credentialID],
+ }),
+ }),
+);
+
+export const storedContent = createTable(
+ "storedContent",
+ {
+ id: integer("id").notNull().primaryKey({ autoIncrement: true }),
+ content: text("content").notNull(),
+ title: text("title", { length: 255 }),
+ description: text("description", { length: 255 }),
+ url: text("url").notNull(),
+ savedAt: int("savedAt", { mode: "timestamp" }).notNull(),
+ baseUrl: text("baseUrl", { length: 255 }),
+ ogImage: text("ogImage", { length: 255 }),
+ type: text("type").default("page"),
+ image: text("image", { length: 255 }),
+ userId: text("user").references(() => users.id, {
+ onDelete: "cascade",
+ }),
+ },
+ (sc) => ({
+ urlIdx: index("storedContent_url_idx").on(sc.url),
+ savedAtIdx: index("storedContent_savedAt_idx").on(sc.savedAt),
+ titleInx: index("storedContent_title_idx").on(sc.title),
+ userIdx: index("storedContent_user_idx").on(sc.userId),
+ }),
+);
+
+export type Content = typeof storedContent.$inferSelect;
+
+export const contentToSpace = createTable(
+ "contentToSpace",
+ {
+ contentId: integer("contentId")
+ .notNull()
+ .references(() => storedContent.id, { onDelete: "cascade" }),
+ spaceId: integer("spaceId")
+ .notNull()
+ .references(() => space.id, { onDelete: "cascade" }),
+ },
+ (cts) => ({
+ compoundKey: primaryKey({ columns: [cts.contentId, cts.spaceId] }),
+ }),
+);
+
+export const space = createTable(
+ "space",
+ {
+ id: integer("id").notNull().primaryKey({ autoIncrement: true }),
+ name: text("name").notNull().unique().default("none"),
+ user: text("user", { length: 255 }).references(() => users.id, {
+ onDelete: "cascade",
+ }),
+ },
+ (space) => ({
+ nameIdx: index("spaces_name_idx").on(space.name),
+ userIdx: index("spaces_user_idx").on(space.user),
+ }),
+);
+
+export type StoredContent = Omit<typeof storedContent.$inferSelect, "user">;
+export type StoredSpace = typeof space.$inferSelect;
+export type ChachedSpaceContent = StoredContent & {
+ space: number;
+};
+
+export const chatThreads = createTable(
+ "chatThread",
+ {
+ id: text("id")
+ .notNull()
+ .primaryKey()
+ .$defaultFn(() => crypto.randomUUID()),
+ firstMessage: text("firstMessage").notNull(),
+ userId: text("userId")
+ .notNull()
+ .references(() => users.id, { onDelete: "cascade" }),
+ },
+ (thread) => ({
+ userIdx: index("chatThread_user_idx").on(thread.userId),
+ }),
+);
+
+export const chatHistory = createTable(
+ "chatHistory",
+ {
+ id: integer("id").notNull().primaryKey({ autoIncrement: true }),
+ threadId: text("threadId")
+ .notNull()
+ .references(() => chatThreads.id, { onDelete: "cascade" }),
+ question: text("question").notNull(),
+ answer: text("answerParts"), // Single answer part as string
+ answerSources: text("answerSources"), // JSON stringified array of objects
+ answerJustification: text("answerJustification"),
+ },
+ (history) => ({
+ threadIdx: index("chatHistory_thread_idx").on(history.threadId),
+ }),
+);
+
+export type ChatThread = typeof chatThreads.$inferSelect;
+export type ChatHistory = typeof chatHistory.$inferSelect;
diff --git a/apps/web/server/encrypt.ts b/apps/web/server/encrypt.ts
new file mode 100644
index 00000000..bb2f5050
--- /dev/null
+++ b/apps/web/server/encrypt.ts
@@ -0,0 +1,95 @@
+function convertStringToFixedNumber(input: string): number {
+ let hash = 0;
+ for (let i = 0; i < input.length; i++) {
+ const char = input.charCodeAt(i);
+ hash = (hash * 31 + char) % 1000000007; // Hashing by a large prime number
+ }
+ return hash;
+}
+
+const chars = `abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-1234567890`;
+const shuffled = shuffle(
+ chars.split(""),
+ convertStringToFixedNumber(process.env.BACKEND_SECURITY_KEY),
+);
+
+function random(seed: number) {
+ const x = Math.sin(seed++) * 10000;
+ return x - Math.floor(x);
+}
+
+function shuffle(array: string[], seed: number) {
+ let m = array.length,
+ t,
+ i;
+
+ while (m) {
+ i = Math.floor(random(seed) * m--);
+
+ t = array[m];
+ array[m] = array[i]!;
+ array[i] = t!;
+ ++seed;
+ }
+
+ return array;
+}
+
+export const cipher = (text: string) => {
+ let returned_text = "";
+
+ for (let i = 0; i < text.length; i++) {
+ returned_text += shuffled[chars.indexOf(text[i]!)];
+ }
+
+ return extend(returned_text);
+};
+
+export const decipher = (text: string) => {
+ let returned_text = "";
+ const index = Math.floor(
+ random(convertStringToFixedNumber(process.env.BACKEND_SECURITY_KEY)) *
+ (text.length / 2),
+ );
+
+ for (let i = 0; i < text.length; i++) {
+ returned_text += chars[shuffled.indexOf(text[i]!)];
+ }
+ const total = parseInt(text[index]!);
+ const str = parseInt(text.slice(index + 1, index + total + 1));
+ return returned_text.slice(text.length - str);
+};
+
+const extend = (text: string, length = 60) => {
+ const extra = length - text.length;
+
+ if (extra < 0) {
+ return text;
+ }
+
+ // Random index to store the length of the string
+ const index = Math.floor(
+ random(convertStringToFixedNumber(process.env.BACKEND_SECURITY_KEY)) *
+ (length / 2),
+ );
+
+ const storage_string =
+ text.length.toString().length.toString() + text.length.toString();
+ let returned = "";
+ let total = storage_string.length + text.length;
+
+ for (let i = 0; i < extra; i++) {
+ if (i == index) {
+ returned += storage_string;
+ } else {
+ if (total >= length) {
+ break;
+ }
+ // Add a random character
+ returned += shuffled[Math.floor(random(Math.random()) * shuffled.length)];
+ total++;
+ }
+ }
+ returned += text;
+ return returned;
+};