Initialize Stage 0: Next.js 16 scaffold with auth and role-based routing
- Next.js 16.2.2 + React 19 + TypeScript + Tailwind v4 - Better Auth with email/password and role system (student/curator/admin) - Prisma 7 schema: User, Session, Account, Verification + full LMS model - Role-based dashboards: student /dashboard, curator /curator/dashboard, admin /admin/dashboard - Auth pages: login, register, verify-email - Better Auth API route handler - Middleware for route protection - Docker Compose with PostgreSQL 16 - Seed script with test users (admin/curator/student) - CLAUDE.md and ROADMAP.md project documentation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,266 @@
|
||||
generator client {
|
||||
provider = "prisma-client"
|
||||
output = "../src/generated/prisma"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Better Auth core tables
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
email String @unique
|
||||
emailVerified Boolean @default(false)
|
||||
image String?
|
||||
role String @default("student") // student | curator | admin
|
||||
banned Boolean? @default(false)
|
||||
banReason String?
|
||||
banExpires DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
sessions Session[]
|
||||
accounts Account[]
|
||||
enrollments CourseEnrollment[]
|
||||
progress LessonProgress[]
|
||||
submissions HomeworkSubmission[]
|
||||
comments LessonComment[]
|
||||
feedbacks HomeworkFeedback[]
|
||||
}
|
||||
|
||||
model Session {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
token String @unique
|
||||
expiresAt DateTime
|
||||
ipAddress String?
|
||||
userAgent String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
model Account {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
accountId String
|
||||
providerId String
|
||||
accessToken String?
|
||||
refreshToken String?
|
||||
idToken String?
|
||||
accessTokenExpiresAt DateTime?
|
||||
refreshTokenExpiresAt DateTime?
|
||||
scope String?
|
||||
password String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
model Verification {
|
||||
id String @id @default(cuid())
|
||||
identifier String
|
||||
value String
|
||||
expiresAt DateTime
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// LMS core tables
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
model Course {
|
||||
id String @id @default(cuid())
|
||||
slug String @unique
|
||||
title String
|
||||
description String?
|
||||
coverImage String?
|
||||
published Boolean @default(false)
|
||||
order Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
modules Module[]
|
||||
enrollments CourseEnrollment[]
|
||||
}
|
||||
|
||||
model Module {
|
||||
id String @id @default(cuid())
|
||||
courseId String
|
||||
title String
|
||||
order Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
course Course @relation(fields: [courseId], references: [id], onDelete: Cascade)
|
||||
lessons Lesson[]
|
||||
}
|
||||
|
||||
model Lesson {
|
||||
id String @id @default(cuid())
|
||||
moduleId String
|
||||
title String
|
||||
content Json?
|
||||
kinescopeId String?
|
||||
order Int @default(0)
|
||||
published Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
module Module @relation(fields: [moduleId], references: [id], onDelete: Cascade)
|
||||
progress LessonProgress[]
|
||||
quiz Quiz?
|
||||
homework Homework?
|
||||
comments LessonComment[]
|
||||
files LessonFile[]
|
||||
}
|
||||
|
||||
model LessonFile {
|
||||
id String @id @default(cuid())
|
||||
lessonId String
|
||||
name String
|
||||
url String
|
||||
size Int
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
model CourseEnrollment {
|
||||
userId String
|
||||
courseId String
|
||||
enrolledAt DateTime @default(now())
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
course Course @relation(fields: [courseId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@id([userId, courseId])
|
||||
}
|
||||
|
||||
model LessonProgress {
|
||||
userId String
|
||||
lessonId String
|
||||
completedAt DateTime @default(now())
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@id([userId, lessonId])
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Quizzes
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
model Quiz {
|
||||
id String @id @default(cuid())
|
||||
lessonId String @unique
|
||||
showAnswers Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade)
|
||||
questions QuizQuestion[]
|
||||
attempts QuizAttempt[]
|
||||
}
|
||||
|
||||
model QuizQuestion {
|
||||
id String @id @default(cuid())
|
||||
quizId String
|
||||
text String
|
||||
type QuizQuestionType
|
||||
order Int @default(0)
|
||||
|
||||
quiz Quiz @relation(fields: [quizId], references: [id], onDelete: Cascade)
|
||||
options QuizOption[]
|
||||
}
|
||||
|
||||
model QuizOption {
|
||||
id String @id @default(cuid())
|
||||
questionId String
|
||||
text String
|
||||
isCorrect Boolean @default(false)
|
||||
order Int @default(0)
|
||||
|
||||
question QuizQuestion @relation(fields: [questionId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
model QuizAttempt {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
quizId String
|
||||
score Int
|
||||
answers Json
|
||||
completedAt DateTime @default(now())
|
||||
|
||||
quiz Quiz @relation(fields: [quizId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
enum QuizQuestionType {
|
||||
SINGLE
|
||||
MULTIPLE
|
||||
TEXT
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Homework
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
model Homework {
|
||||
id String @id @default(cuid())
|
||||
lessonId String @unique
|
||||
description String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade)
|
||||
submissions HomeworkSubmission[]
|
||||
}
|
||||
|
||||
model HomeworkSubmission {
|
||||
id String @id @default(cuid())
|
||||
homeworkId String
|
||||
userId String
|
||||
text String?
|
||||
files Json?
|
||||
submittedAt DateTime @default(now())
|
||||
|
||||
homework Homework @relation(fields: [homeworkId], references: [id], onDelete: Cascade)
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
feedbacks HomeworkFeedback[]
|
||||
}
|
||||
|
||||
model HomeworkFeedback {
|
||||
id String @id @default(cuid())
|
||||
submissionId String
|
||||
curatorId String
|
||||
text String
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
submission HomeworkSubmission @relation(fields: [submissionId], references: [id], onDelete: Cascade)
|
||||
curator User @relation(fields: [curatorId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Comments
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
model LessonComment {
|
||||
id String @id @default(cuid())
|
||||
lessonId String
|
||||
userId String
|
||||
text String
|
||||
deleted Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade)
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import "dotenv/config";
|
||||
import { PrismaClient } from "../src/generated/prisma";
|
||||
import bcrypt from "bcryptjs";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
console.log("Seeding database...");
|
||||
|
||||
const hashedPassword = await bcrypt.hash("Password123!", 10);
|
||||
|
||||
// Admin
|
||||
const admin = await prisma.user.upsert({
|
||||
where: { email: "admin@second-brain.ru" },
|
||||
update: {},
|
||||
create: {
|
||||
email: "admin@second-brain.ru",
|
||||
name: "Администратор",
|
||||
emailVerified: true,
|
||||
role: "admin",
|
||||
accounts: {
|
||||
create: {
|
||||
accountId: "admin@second-brain.ru",
|
||||
providerId: "credential",
|
||||
password: hashedPassword,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Curator
|
||||
const curator = await prisma.user.upsert({
|
||||
where: { email: "curator@second-brain.ru" },
|
||||
update: {},
|
||||
create: {
|
||||
email: "curator@second-brain.ru",
|
||||
name: "Куратор",
|
||||
emailVerified: true,
|
||||
role: "curator",
|
||||
accounts: {
|
||||
create: {
|
||||
accountId: "curator@second-brain.ru",
|
||||
providerId: "credential",
|
||||
password: hashedPassword,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Student
|
||||
const student = await prisma.user.upsert({
|
||||
where: { email: "student@second-brain.ru" },
|
||||
update: {},
|
||||
create: {
|
||||
email: "student@second-brain.ru",
|
||||
name: "Ученик",
|
||||
emailVerified: true,
|
||||
role: "student",
|
||||
accounts: {
|
||||
create: {
|
||||
accountId: "student@second-brain.ru",
|
||||
providerId: "credential",
|
||||
password: hashedPassword,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
console.log("Created users:");
|
||||
console.log(` Admin: ${admin.email}`);
|
||||
console.log(` Curator: ${curator.email}`);
|
||||
console.log(` Student: ${student.email}`);
|
||||
console.log(" Password for all: Password123!");
|
||||
console.log("Done.");
|
||||
}
|
||||
|
||||
main()
|
||||
.catch(console.error)
|
||||
.finally(() => prisma.$disconnect());
|
||||
Reference in New Issue
Block a user