├── .npmrc ├── server ├── tsconfig.json ├── middleware │ └── prisma.ts └── api │ ├── folders │ ├── index.post.ts │ └── index.get.ts │ ├── documents │ ├── index.get.ts │ ├── index.post.ts │ └── [documentId] │ │ ├── index.get.ts │ │ └── index.put.ts │ ├── auth │ └── [...].ts │ ├── generate.ts │ └── files.get.ts ├── public └── favicon.ico ├── tsconfig.json ├── prisma ├── migrations │ ├── migration_lock.toml │ ├── 20230905124348_timestamps │ │ └── migration.sql │ ├── 20230905122741_init │ │ └── migration.sql │ └── 20230906044822_auth_schema │ │ └── migration.sql └── schema.prisma ├── types └── next-auth.d.ts ├── .gitignore ├── .env.example ├── docker-compose.yml ├── components ├── icon │ └── Google.vue ├── Breadcrumb.vue └── Folder │ └── Form.vue ├── nuxt.config.ts ├── package.json ├── README.md ├── pages ├── index.vue ├── d │ └── [documentId].vue └── files │ └── [...slugs].vue └── app.vue /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | strict-peer-dependencies=false 3 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.nuxt/tsconfig.server.json" 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naveennaidu/novuel/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json" 4 | } 5 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /types/next-auth.d.ts: -------------------------------------------------------------------------------- 1 | import NextAuth from "next-auth"; 2 | 3 | declare module "next-auth" { 4 | interface Session { 5 | user: { 6 | id: string; 7 | } & DefaultSession["user"]; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Nuxt dev/build outputs 2 | .output 3 | .data 4 | .nuxt 5 | .nitro 6 | .cache 7 | dist 8 | 9 | # Node dependencies 10 | node_modules 11 | 12 | # Logs 13 | logs 14 | *.log 15 | 16 | # Misc 17 | .DS_Store 18 | .fleet 19 | .idea 20 | 21 | # Local env files 22 | .env 23 | .env.* 24 | !.env.example 25 | -------------------------------------------------------------------------------- /server/middleware/prisma.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | let prisma: PrismaClient; 4 | 5 | declare module "h3" { 6 | interface H3EventContext { 7 | prisma: PrismaClient; 8 | } 9 | } 10 | 11 | export default eventHandler((event) => { 12 | if (!prisma) { 13 | prisma = new PrismaClient(); 14 | } 15 | event.context.prisma = prisma; 16 | }); 17 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | DEFAULT_DATABASE_HOSTNAME=localhost 2 | DEFAULT_DATABASE_USER=postgres 3 | DEFAULT_DATABASE_PASSWORD=postgres 4 | DEFAULT_DATABASE_PORT=5432 5 | DEFAULT_DATABASE_DB=novuel 6 | 7 | DATABASE_URL="postgres://postgres:postgres@localhost:5432/novuel?schema=public" 8 | 9 | GOOGLE_CLIENT_ID= 10 | GOOGLE_CLIENT_SECRET= 11 | API_ROUTE_SECRET= 12 | AUTH_ORIGIN=http://localhost:3000 13 | 14 | NUXT_OPENAI_API_KEY= -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | 3 | # For local development, only database is running 4 | # 5 | # docker-compose up -d 6 | # 7 | 8 | services: 9 | default_database: 10 | restart: unless-stopped 11 | image: postgres:latest 12 | volumes: 13 | - default_database_data:/var/lib/postgresql/data 14 | environment: 15 | - POSTGRES_DB=${DEFAULT_DATABASE_DB} 16 | - POSTGRES_USER=${DEFAULT_DATABASE_USER} 17 | - POSTGRES_PASSWORD=${DEFAULT_DATABASE_PASSWORD} 18 | env_file: 19 | - .env 20 | ports: 21 | - "${DEFAULT_DATABASE_PORT}:5432" 22 | 23 | volumes: 24 | default_database_data: 25 | -------------------------------------------------------------------------------- /prisma/migrations/20230905124348_timestamps/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Document" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 3 | ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; 4 | 5 | -- AlterTable 6 | ALTER TABLE "Folder" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 7 | ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; 8 | 9 | -- AlterTable 10 | ALTER TABLE "User" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 11 | ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; 12 | -------------------------------------------------------------------------------- /components/icon/Google.vue: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /nuxt.config.ts: -------------------------------------------------------------------------------- 1 | // https://nuxt.com/docs/api/configuration/nuxt-config 2 | export default defineNuxtConfig({ 3 | devtools: { enabled: true }, 4 | modules: ["@nuxthq/ui", "@sidebase/nuxt-auth"], 5 | runtimeConfig: { 6 | GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET, 7 | API_ROUTE_SECRET: process.env.API_ROUTE_SECRET, 8 | openaiApiKey: "", 9 | public: { 10 | GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID, 11 | }, 12 | }, 13 | colorMode: { 14 | preference: "light", 15 | }, 16 | auth: { 17 | origin: 18 | process.env.ENVIRONMENT === "production" 19 | ? "https://novuel.vercel.app" 20 | : "http://localhost:3000", 21 | }, 22 | }); 23 | -------------------------------------------------------------------------------- /server/api/folders/index.post.ts: -------------------------------------------------------------------------------- 1 | import { getServerSession } from "#auth"; 2 | import { z, parseBodyAs } from "@sidebase/nuxt-parse"; 3 | 4 | const bodySchema = z.object({ 5 | name: z.string(), 6 | parentFolderId: z.number().optional(), 7 | }); 8 | 9 | export default defineEventHandler(async (event) => { 10 | const session = await getServerSession(event); 11 | const body = await parseBodyAs(event, bodySchema); 12 | 13 | if (!session) { 14 | throw createError({ statusMessage: "Unauthenticated", statusCode: 403 }); 15 | } 16 | 17 | const { prisma } = event.context; 18 | 19 | const folder = await prisma.folder.create({ 20 | data: { 21 | userId: session.user.id, 22 | name: body.name, 23 | parentFolderId: body.parentFolderId, 24 | }, 25 | }); 26 | return { folder }; 27 | }); 28 | -------------------------------------------------------------------------------- /server/api/documents/index.get.ts: -------------------------------------------------------------------------------- 1 | import { getServerSession } from "#auth"; 2 | import { z, parseQueryAs } from "@sidebase/nuxt-parse"; 3 | 4 | const querySchema = z.object({ 5 | folderId: z 6 | .string() 7 | .optional() 8 | .transform((value) => Number(value)), 9 | }); 10 | 11 | export default defineEventHandler(async (event) => { 12 | const session = await getServerSession(event); 13 | const query = parseQueryAs(event, querySchema); 14 | 15 | if (!session) { 16 | throw createError({ statusMessage: "Unauthenticated", statusCode: 403 }); 17 | } 18 | 19 | const { prisma } = event.context; 20 | 21 | const documents = await prisma.document.findMany({ 22 | where: { 23 | userId: session.user.id, 24 | folderId: query.folderId, 25 | }, 26 | }); 27 | return { documents }; 28 | }); 29 | -------------------------------------------------------------------------------- /server/api/folders/index.get.ts: -------------------------------------------------------------------------------- 1 | import { getServerSession } from "#auth"; 2 | import { z, parseQueryAs } from "@sidebase/nuxt-parse"; 3 | 4 | const querySchema = z.object({ 5 | parentFolderId: z 6 | .string() 7 | .optional() 8 | .transform((value) => Number(value)), 9 | }); 10 | 11 | export default defineEventHandler(async (event) => { 12 | const session = await getServerSession(event); 13 | const query = parseQueryAs(event, querySchema); 14 | 15 | if (!session) { 16 | throw createError({ statusMessage: "Unauthenticated", statusCode: 403 }); 17 | } 18 | 19 | const { prisma } = event.context; 20 | 21 | const folders = await prisma.folder.findMany({ 22 | where: { 23 | userId: session.user.id, 24 | parentFolderId: query.parentFolderId, 25 | }, 26 | }); 27 | return { folders }; 28 | }); 29 | -------------------------------------------------------------------------------- /server/api/documents/index.post.ts: -------------------------------------------------------------------------------- 1 | import { getServerSession } from "#auth"; 2 | import { z, parseBodyAs } from "@sidebase/nuxt-parse"; 3 | 4 | const bodySchema = z.object({ 5 | folderId: z 6 | .string() 7 | .optional() 8 | .transform((value) => Number(value)), 9 | }); 10 | 11 | export default defineEventHandler(async (event) => { 12 | const session = await getServerSession(event); 13 | const body = await parseBodyAs(event, bodySchema); 14 | 15 | if (!session) { 16 | throw createError({ statusMessage: "Unauthenticated", statusCode: 403 }); 17 | } 18 | 19 | const { prisma } = event.context; 20 | 21 | const document = await prisma.document.create({ 22 | data: { 23 | userId: session.user.id, 24 | title: "", 25 | content: "{}", 26 | folderId: body.folderId, 27 | }, 28 | }); 29 | return { document }; 30 | }); 31 | -------------------------------------------------------------------------------- /server/api/documents/[documentId]/index.get.ts: -------------------------------------------------------------------------------- 1 | import { getServerSession } from "#auth"; 2 | import { z, parseParamsAs } from "@sidebase/nuxt-parse"; 3 | 4 | const paramSchema = z.object({ 5 | documentId: z 6 | .string() 7 | .optional() 8 | .transform((value) => Number(value)), 9 | }); 10 | 11 | export default defineEventHandler(async (event) => { 12 | const session = await getServerSession(event); 13 | const { documentId } = parseParamsAs(event, paramSchema); 14 | 15 | if (!session) { 16 | throw createError({ statusMessage: "Unauthenticated", statusCode: 403 }); 17 | } 18 | 19 | const { prisma } = event.context; 20 | 21 | const document = await prisma.document.findUnique({ 22 | where: { 23 | id: documentId, 24 | userId: session.user.id, 25 | }, 26 | }); 27 | if (!document) { 28 | throw createError({ statusMessage: "Not Found", statusCode: 404 }); 29 | } 30 | return { document }; 31 | }); 32 | -------------------------------------------------------------------------------- /components/Breadcrumb.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "novuel", 3 | "private": true, 4 | "scripts": { 5 | "build": "nuxt build", 6 | "dev": "nuxt dev", 7 | "generate": "nuxt generate", 8 | "vercel-build": "prisma generate && prisma migrate deploy && nuxt build", 9 | "preview": "nuxt preview", 10 | "postinstall": "nuxt prepare", 11 | "prisma:migrate:dev": "npx prisma migrate dev", 12 | "prisma:migrate:generate": "npx prisma generate", 13 | "prisma:studio": "npx prisma studio" 14 | }, 15 | "devDependencies": { 16 | "@nuxt/devtools": "latest", 17 | "@nuxthq/ui": "^2.7.0", 18 | "@sidebase/nuxt-auth": "^0.6.0-beta.5", 19 | "nuxt": "^3.7.0", 20 | "prisma": "^5.2.0" 21 | }, 22 | "dependencies": { 23 | "@next-auth/prisma-adapter": "^1.0.7", 24 | "@prisma/client": "5.2.0", 25 | "@sidebase/nuxt-parse": "^0.3.0", 26 | "ai": "^2.2.11", 27 | "next-auth": "4.21.1", 28 | "novel-vue": "^0.0.3", 29 | "openai": "^4.4.0", 30 | "zod": "^3.22.2" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /server/api/auth/[...].ts: -------------------------------------------------------------------------------- 1 | import { NuxtAuthHandler } from "#auth"; 2 | import GoogleProvider from "next-auth/providers/google"; 3 | import { PrismaAdapter } from "@next-auth/prisma-adapter"; 4 | import { PrismaClient } from "@prisma/client"; 5 | 6 | const runtimeConfig = useRuntimeConfig(); 7 | const prisma = new PrismaClient(); 8 | 9 | export default NuxtAuthHandler({ 10 | adapter: PrismaAdapter(prisma), 11 | secret: useRuntimeConfig().API_ROUTE_SECRET, 12 | session: { 13 | strategy: "jwt", 14 | }, 15 | debug: process.env.NODE_ENV === "development", 16 | providers: [ 17 | // @ts-expect-error 18 | GoogleProvider.default({ 19 | clientId: useRuntimeConfig().public.GOOGLE_CLIENT_ID, 20 | clientSecret: runtimeConfig.GOOGLE_CLIENT_SECRET, 21 | }), 22 | ], 23 | callbacks: { 24 | session({ session, token }) { 25 | session.user.id = token.id; 26 | return session; 27 | }, 28 | jwt({ token, account, user }) { 29 | if (account) { 30 | token.accessToken = account.access_token; 31 | token.id = user?.id; 32 | } 33 | return token; 34 | }, 35 | }, 36 | }); 37 | -------------------------------------------------------------------------------- /server/api/documents/[documentId]/index.put.ts: -------------------------------------------------------------------------------- 1 | import { getServerSession } from "#auth"; 2 | import { z, parseBodyAs, parseParamsAs } from "@sidebase/nuxt-parse"; 3 | 4 | const bodySchema = z.object({ 5 | content: z.string(), 6 | }); 7 | 8 | const paramSchema = z.object({ 9 | documentId: z 10 | .string() 11 | .optional() 12 | .transform((value) => Number(value)), 13 | }); 14 | 15 | export default defineEventHandler(async (event) => { 16 | const session = await getServerSession(event); 17 | const body = await parseBodyAs(event, bodySchema); 18 | const { documentId } = parseParamsAs(event, paramSchema); 19 | 20 | if (!session) { 21 | throw createError({ statusMessage: "Unauthenticated", statusCode: 403 }); 22 | } 23 | 24 | const { prisma } = event.context; 25 | 26 | const parsedContent = JSON.parse(body.content); 27 | let title = ""; 28 | try { 29 | title = parsedContent.content[0]?.content[0]?.text; 30 | } catch (error) {} 31 | 32 | const document = await prisma.document.update({ 33 | where: { 34 | id: documentId, 35 | userId: session.user.id, 36 | }, 37 | data: { 38 | title, 39 | content: body.content, 40 | }, 41 | }); 42 | return { document }; 43 | }); 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Novuel - Open Source AI Writing App 2 | 3 | Novuel is an open-source AI writing application built with Nuxt 3, Nuxt UI, and Novel Vue. 4 | 5 | This guide will help you set up the project on your local machine. 6 | 7 | ## Prerequisites 8 | 9 | Before you begin, ensure you have the following installed on your local machine: 10 | 11 | - Node.js (version 18 or higher) 12 | - Yarn package manager 13 | - Docker 14 | - Docker Compose 15 | 16 | ## Setup 17 | 18 | 1. Clone the repository: 19 | 20 | ```bash 21 | git clone https://github.com/your-repo/novuel.git 22 | cd novuel 23 | ``` 24 | 25 | 2. Install dependencies: 26 | 27 | ```bash 28 | yarn install 29 | ``` 30 | 31 | 3. Create a `.env` file in the root directory of the project by copying the `.env.example` file: 32 | 33 | ```bash 34 | cp .env.example .env 35 | ``` 36 | 37 | 4. Start the PostgreSQL database using Docker Compose: 38 | 39 | ```bash 40 | docker-compose up -d 41 | ``` 42 | 43 | 5. Run the prisma database migrations and generate the Prisma client: 44 | 45 | ```bash 46 | yarn prisma:migrate:dev 47 | yarn prisma:migrate:generate 48 | ``` 49 | 50 | 6. Start the development server: 51 | 52 | ```bash 53 | yarn dev 54 | ``` 55 | 56 | Now, you can access the application at http://localhost:3000. 57 | -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /prisma/migrations/20230905122741_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "User" ( 3 | "id" SERIAL NOT NULL, 4 | "email" TEXT NOT NULL, 5 | 6 | CONSTRAINT "User_pkey" PRIMARY KEY ("id") 7 | ); 8 | 9 | -- CreateTable 10 | CREATE TABLE "Folder" ( 11 | "id" SERIAL NOT NULL, 12 | "name" TEXT NOT NULL, 13 | "userId" INTEGER NOT NULL, 14 | 15 | CONSTRAINT "Folder_pkey" PRIMARY KEY ("id") 16 | ); 17 | 18 | -- CreateTable 19 | CREATE TABLE "Document" ( 20 | "id" SERIAL NOT NULL, 21 | "title" TEXT NOT NULL, 22 | "content" TEXT NOT NULL, 23 | "folderId" INTEGER, 24 | "userId" INTEGER NOT NULL, 25 | 26 | CONSTRAINT "Document_pkey" PRIMARY KEY ("id") 27 | ); 28 | 29 | -- CreateIndex 30 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); 31 | 32 | -- AddForeignKey 33 | ALTER TABLE "Folder" ADD CONSTRAINT "Folder_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 34 | 35 | -- AddForeignKey 36 | ALTER TABLE "Document" ADD CONSTRAINT "Document_folderId_fkey" FOREIGN KEY ("folderId") REFERENCES "Folder"("id") ON DELETE SET NULL ON UPDATE CASCADE; 37 | 38 | -- AddForeignKey 39 | ALTER TABLE "Document" ADD CONSTRAINT "Document_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 40 | -------------------------------------------------------------------------------- /app.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 49 | -------------------------------------------------------------------------------- /server/api/generate.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from "openai"; 2 | import { OpenAIStream } from "ai"; 3 | 4 | export default defineLazyEventHandler(async () => { 5 | const apiKey = useRuntimeConfig().openaiApiKey; 6 | if (!apiKey) throw new Error("Missing OpenAI API key"); 7 | const openai = new OpenAI({ 8 | apiKey: apiKey, 9 | }); 10 | 11 | return defineEventHandler(async (event) => { 12 | // Extract the `prompt` from the body of the request 13 | const body = await readBody(event); 14 | const { prompt } = JSON.parse(body); 15 | 16 | const response = await openai.chat.completions.create({ 17 | model: "gpt-3.5-turbo", 18 | messages: [ 19 | { 20 | role: "system", 21 | content: 22 | "You are an AI writing assistant that continues existing text based on context from prior text. " + 23 | "Give more weight/priority to the later characters than the beginning ones. " + 24 | "Limit your response to no more than 200 characters, but make sure to construct complete sentences.", 25 | }, 26 | { 27 | role: "user", 28 | content: prompt, 29 | }, 30 | ], 31 | temperature: 0.7, 32 | top_p: 1, 33 | frequency_penalty: 0, 34 | presence_penalty: 0, 35 | stream: true, 36 | n: 1, 37 | }); 38 | 39 | // Convert the response into a friendly text-stream 40 | return OpenAIStream(response); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /server/api/files.get.ts: -------------------------------------------------------------------------------- 1 | import { getServerSession } from "#auth"; 2 | import { z, parseQueryAs } from "@sidebase/nuxt-parse"; 3 | 4 | const querySchema = z.object({ 5 | folderId: z 6 | .string() 7 | .optional() 8 | .transform((value) => Number(value)), 9 | }); 10 | 11 | export default defineEventHandler(async (event) => { 12 | const session = await getServerSession(event); 13 | const { folderId } = parseQueryAs(event, querySchema); 14 | 15 | if (!session) { 16 | throw createError({ statusMessage: "Unauthenticated", statusCode: 403 }); 17 | } 18 | 19 | const { prisma } = event.context; 20 | 21 | const folders = await prisma.folder.findMany({ 22 | where: { 23 | userId: session.user.id, 24 | parentFolderId: folderId, 25 | }, 26 | }); 27 | 28 | const documents = await prisma.document.findMany({ 29 | where: { 30 | userId: session.user.id, 31 | folderId: folderId, 32 | }, 33 | }); 34 | 35 | let files: { 36 | id: number; 37 | title: string; 38 | type: "folder" | "document"; 39 | updatedAt: Date; 40 | }[] = []; 41 | 42 | folders.forEach((folder) => { 43 | files.push({ 44 | id: folder.id, 45 | title: folder.name, 46 | type: "folder", 47 | updatedAt: folder.updatedAt, 48 | }); 49 | }); 50 | 51 | documents.forEach((document) => { 52 | files.push({ 53 | id: document.id, 54 | title: document.title, 55 | type: "document", 56 | updatedAt: document.updatedAt, 57 | }); 58 | }); 59 | 60 | return { files }; 61 | }); 62 | -------------------------------------------------------------------------------- /pages/d/[documentId].vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /components/Folder/Form.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "postgresql" 10 | url = env("DATABASE_URL") 11 | } 12 | 13 | model Account { 14 | id String @id @default(cuid()) 15 | userId String @unique 16 | type String 17 | provider String 18 | providerAccountId String 19 | refresh_token String? 20 | access_token String? 21 | expires_at Int? 22 | token_type String? 23 | scope String? 24 | id_token String? 25 | session_state String? 26 | subscribed Boolean @default(false) 27 | 28 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 29 | 30 | @@unique([provider, providerAccountId]) 31 | } 32 | 33 | model Session { 34 | id String @id @default(cuid()) 35 | sessionToken String @unique 36 | userId String 37 | expires DateTime 38 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 39 | } 40 | 41 | model User { 42 | id String @id @default(cuid()) 43 | name String? 44 | email String? @unique 45 | emailVerified DateTime? 46 | image String? 47 | account Account[] 48 | sessions Session[] 49 | folders Folder[] 50 | documents Document[] 51 | } 52 | 53 | model VerificationToken { 54 | identifier String 55 | token String @unique 56 | expires DateTime 57 | 58 | @@unique([identifier, token]) 59 | } 60 | 61 | model Folder { 62 | id Int @id @default(autoincrement()) 63 | name String 64 | parentFolderId Int? 65 | parentFolder Folder? @relation("FolderToFolder", fields: [parentFolderId], references: [id]) 66 | childFolders Folder[] @relation("FolderToFolder") 67 | userId String 68 | user User @relation(fields: [userId], references: [id]) 69 | documents Document[] 70 | createdAt DateTime @default(now()) 71 | updatedAt DateTime @default(now()) @updatedAt 72 | } 73 | 74 | model Document { 75 | id Int @id @default(autoincrement()) 76 | title String 77 | content String 78 | folderId Int? 79 | folder Folder? @relation(fields: [folderId], references: [id]) 80 | userId String 81 | user User @relation(fields: [userId], references: [id]) 82 | createdAt DateTime @default(now()) 83 | updatedAt DateTime @default(now()) @updatedAt 84 | } 85 | -------------------------------------------------------------------------------- /prisma/migrations/20230906044822_auth_schema/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - The primary key for the `User` table will be changed. If it partially fails, the table could be left without primary key constraint. 5 | - You are about to drop the column `createdAt` on the `User` table. All the data in the column will be lost. 6 | - You are about to drop the column `updatedAt` on the `User` table. All the data in the column will be lost. 7 | 8 | */ 9 | -- DropForeignKey 10 | ALTER TABLE "Document" DROP CONSTRAINT "Document_userId_fkey"; 11 | 12 | -- DropForeignKey 13 | ALTER TABLE "Folder" DROP CONSTRAINT "Folder_userId_fkey"; 14 | 15 | -- AlterTable 16 | ALTER TABLE "Document" ALTER COLUMN "userId" SET DATA TYPE TEXT; 17 | 18 | -- AlterTable 19 | ALTER TABLE "Folder" ADD COLUMN "parentFolderId" INTEGER, 20 | ALTER COLUMN "userId" SET DATA TYPE TEXT; 21 | 22 | -- AlterTable 23 | ALTER TABLE "User" DROP CONSTRAINT "User_pkey", 24 | DROP COLUMN "createdAt", 25 | DROP COLUMN "updatedAt", 26 | ADD COLUMN "emailVerified" TIMESTAMP(3), 27 | ADD COLUMN "image" TEXT, 28 | ADD COLUMN "name" TEXT, 29 | ALTER COLUMN "id" DROP DEFAULT, 30 | ALTER COLUMN "id" SET DATA TYPE TEXT, 31 | ALTER COLUMN "email" DROP NOT NULL, 32 | ADD CONSTRAINT "User_pkey" PRIMARY KEY ("id"); 33 | DROP SEQUENCE "User_id_seq"; 34 | 35 | -- CreateTable 36 | CREATE TABLE "Account" ( 37 | "id" TEXT NOT NULL, 38 | "userId" TEXT NOT NULL, 39 | "type" TEXT NOT NULL, 40 | "provider" TEXT NOT NULL, 41 | "providerAccountId" TEXT NOT NULL, 42 | "refresh_token" TEXT, 43 | "access_token" TEXT, 44 | "expires_at" INTEGER, 45 | "token_type" TEXT, 46 | "scope" TEXT, 47 | "id_token" TEXT, 48 | "session_state" TEXT, 49 | "subscribed" BOOLEAN NOT NULL DEFAULT false, 50 | 51 | CONSTRAINT "Account_pkey" PRIMARY KEY ("id") 52 | ); 53 | 54 | -- CreateTable 55 | CREATE TABLE "Session" ( 56 | "id" TEXT NOT NULL, 57 | "sessionToken" TEXT NOT NULL, 58 | "userId" TEXT NOT NULL, 59 | "expires" TIMESTAMP(3) NOT NULL, 60 | 61 | CONSTRAINT "Session_pkey" PRIMARY KEY ("id") 62 | ); 63 | 64 | -- CreateTable 65 | CREATE TABLE "VerificationToken" ( 66 | "identifier" TEXT NOT NULL, 67 | "token" TEXT NOT NULL, 68 | "expires" TIMESTAMP(3) NOT NULL 69 | ); 70 | 71 | -- CreateIndex 72 | CREATE UNIQUE INDEX "Account_userId_key" ON "Account"("userId"); 73 | 74 | -- CreateIndex 75 | CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId"); 76 | 77 | -- CreateIndex 78 | CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken"); 79 | 80 | -- CreateIndex 81 | CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token"); 82 | 83 | -- CreateIndex 84 | CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token"); 85 | 86 | -- AddForeignKey 87 | ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 88 | 89 | -- AddForeignKey 90 | ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 91 | 92 | -- AddForeignKey 93 | ALTER TABLE "Folder" ADD CONSTRAINT "Folder_parentFolderId_fkey" FOREIGN KEY ("parentFolderId") REFERENCES "Folder"("id") ON DELETE SET NULL ON UPDATE CASCADE; 94 | 95 | -- AddForeignKey 96 | ALTER TABLE "Folder" ADD CONSTRAINT "Folder_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 97 | 98 | -- AddForeignKey 99 | ALTER TABLE "Document" ADD CONSTRAINT "Document_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 100 | -------------------------------------------------------------------------------- /pages/files/[...slugs].vue: -------------------------------------------------------------------------------- 1 | 79 | 80 | 162 | 163 | 164 | --------------------------------------------------------------------------------