feat: initial commit - EZBio Next.js 16 fullstack URL shortener & bio link
This commit is contained in:
commit
569c10342f
64 files changed
+15670
No files matched your search
@@ -0,0 +1,127 @@
|
||||
// Prisma Schema for EZBio - Modern URL Shortener & Link in Bio
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
email String @unique
|
||||
password String
|
||||
role String @default("user") // "admin" | "user"
|
||||
username String @unique
|
||||
isActive Boolean @default(true)
|
||||
emailVerified DateTime?
|
||||
avatar String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
shortUrls ShortUrl[]
|
||||
portal Portal?
|
||||
uploadedFiles UploadedFile[]
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
model ShortUrl {
|
||||
id String @id @default(cuid())
|
||||
originalUrl String
|
||||
shortCode String @unique
|
||||
title String?
|
||||
password String? // bcrypt hash if password protected
|
||||
expiresAt DateTime?
|
||||
displayOnPortal Boolean @default(false)
|
||||
clicksCount Int @default(0)
|
||||
userId String?
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
tags Tag[] @relation("ShortUrlTags")
|
||||
clicks UrlClick[]
|
||||
|
||||
@@index([userId])
|
||||
@@index([shortCode])
|
||||
@@map("short_urls")
|
||||
}
|
||||
|
||||
model Tag {
|
||||
id String @id @default(cuid())
|
||||
name String @unique
|
||||
shortUrls ShortUrl[] @relation("ShortUrlTags")
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@map("tags")
|
||||
}
|
||||
|
||||
model UrlClick {
|
||||
id String @id @default(cuid())
|
||||
shortUrlId String
|
||||
shortUrl ShortUrl @relation(fields: [shortUrlId], references: [id], onDelete: Cascade)
|
||||
ipAddress String?
|
||||
userAgent String?
|
||||
deviceType String? // "desktop" | "mobile" | "tablet"
|
||||
operatingSystem String?
|
||||
browser String?
|
||||
countryName String?
|
||||
countryCode String?
|
||||
city String?
|
||||
referrer String?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([shortUrlId])
|
||||
@@index([createdAt])
|
||||
@@map("url_clicks")
|
||||
}
|
||||
|
||||
model Portal {
|
||||
id String @id @default(cuid())
|
||||
userId String @unique
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
title String?
|
||||
description String?
|
||||
avatar String?
|
||||
backgroundImage String?
|
||||
headerColor String? @default("#000000")
|
||||
titleColorStyle String? @default("gradient-1")
|
||||
backgroundStyle String? @default("gradient-1")
|
||||
vcard Json? // { fullName, phone, email, company, website }
|
||||
socialLinks Json? // { facebook, twitter, instagram, threads, tiktok, youtube, telegram }
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("portals")
|
||||
}
|
||||
|
||||
model Setting {
|
||||
id String @id @default(cuid())
|
||||
key String @unique
|
||||
value String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("settings")
|
||||
}
|
||||
|
||||
model UploadedFile {
|
||||
id String @id @default(cuid())
|
||||
key String @unique // Storage key or local filename
|
||||
url String // Public URL or relative path
|
||||
originalName String
|
||||
mimeType String
|
||||
sizeBytes BigInt // File size in bytes
|
||||
category String // "avatar", "background", "logo", "favicon", "og_image"
|
||||
storageType String @default("cloudflare_r2") // "cloudflare_r2" | "local"
|
||||
userId String?
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([category])
|
||||
@@map("uploaded_files")
|
||||
}
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import bcrypt from "bcryptjs";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
console.log("Seeding database...");
|
||||
|
||||
// 1. Initial System Settings
|
||||
const initialSettings = [
|
||||
{ key: "default_url_length", value: "5" },
|
||||
{ key: "site_name", value: "EZBio - URL Shortener & Link in Bio" },
|
||||
{ key: "allow_registration", value: "1" },
|
||||
{ key: "header_color", value: "#4F46E5" },
|
||||
{ key: "background_color", value: "#F9FAFB" },
|
||||
{ key: "seo_title", value: "EZBio - Fast URL Shortener & Dynamic Bio Link" },
|
||||
{ key: "seo_description", value: "Rút gọn link thông minh, tạo mã QR và trang Bio Link đỉnh cao." },
|
||||
{ key: "seo_keywords", value: "url shortener, link in bio, bio link, qr code generator" },
|
||||
{ key: "r2_storage_limit_mb", value: "10240" },
|
||||
{ key: "r2_warning_threshold", value: "80" },
|
||||
];
|
||||
|
||||
for (const s of initialSettings) {
|
||||
await prisma.setting.upsert({
|
||||
where: { key: s.key },
|
||||
update: { value: s.value },
|
||||
create: { key: s.key, value: s.value },
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Admin User
|
||||
const adminPassword = await bcrypt.hash("password123", 10);
|
||||
const admin = await prisma.user.upsert({
|
||||
where: { email: "[email protected]" },
|
||||
update: {},
|
||||
create: {
|
||||
name: "Admin User",
|
||||
email: "[email protected]",
|
||||
username: "admin",
|
||||
password: adminPassword,
|
||||
role: "admin",
|
||||
isActive: true,
|
||||
portal: {
|
||||
create: {
|
||||
title: "Admin Official Bio",
|
||||
description: "Quản trị viên hệ thống EZBio Platform",
|
||||
headerColor: "#000000",
|
||||
titleColorStyle: "gradient-1",
|
||||
backgroundStyle: "gradient-2",
|
||||
vcard: {
|
||||
fullName: "EZBio Administrator",
|
||||
email: "[email protected]",
|
||||
company: "EZBio Corp",
|
||||
website: "https://ezbio.io",
|
||||
},
|
||||
socialLinks: {
|
||||
facebook: "https://facebook.com",
|
||||
twitter: "https://x.com",
|
||||
youtube: "https://youtube.com",
|
||||
telegram: "https://t.me",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 3. Demo User
|
||||
const demoPassword = await bcrypt.hash("password123", 10);
|
||||
const demo = await prisma.user.upsert({
|
||||
where: { email: "[email protected]" },
|
||||
update: {},
|
||||
create: {
|
||||
name: "Demo Creator",
|
||||
email: "[email protected]",
|
||||
username: "demo",
|
||||
password: demoPassword,
|
||||
role: "user",
|
||||
isActive: true,
|
||||
portal: {
|
||||
create: {
|
||||
title: "Trần Tiến • Content Creator",
|
||||
description: "Chia sẻ kiến thức công nghệ, lập trình Fullstack & AI Agent",
|
||||
headerColor: "#000000",
|
||||
titleColorStyle: "gradient-5",
|
||||
backgroundStyle: "gradient-5",
|
||||
vcard: {
|
||||
fullName: "Trần Tiến",
|
||||
phone: "+84 901 234 567",
|
||||
email: "[email protected]",
|
||||
company: "Tech Creator",
|
||||
website: "https://github.com",
|
||||
},
|
||||
socialLinks: {
|
||||
facebook: "https://facebook.com",
|
||||
twitter: "https://x.com",
|
||||
instagram: "https://instagram.com",
|
||||
tiktok: "https://tiktok.com",
|
||||
youtube: "https://youtube.com",
|
||||
telegram: "https://t.me",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 4. Sample Links
|
||||
const sampleLinks = [
|
||||
{
|
||||
originalUrl: "https://github.com",
|
||||
shortCode: "github",
|
||||
title: "GitHub - Build and ship software",
|
||||
displayOnPortal: true,
|
||||
userId: demo.id,
|
||||
tags: ["tech", "coding"],
|
||||
},
|
||||
{
|
||||
originalUrl: "https://nextjs.org",
|
||||
shortCode: "nextjs",
|
||||
title: "Next.js by Vercel - The React Framework for the Web",
|
||||
displayOnPortal: true,
|
||||
userId: demo.id,
|
||||
tags: ["framework", "react"],
|
||||
},
|
||||
{
|
||||
originalUrl: "https://vercel.com",
|
||||
shortCode: "vercel",
|
||||
title: "Vercel - Build and deploy web applications",
|
||||
displayOnPortal: true,
|
||||
userId: demo.id,
|
||||
tags: ["cloud", "deploy"],
|
||||
},
|
||||
];
|
||||
|
||||
for (const link of sampleLinks) {
|
||||
await prisma.shortUrl.upsert({
|
||||
where: { shortCode: link.shortCode },
|
||||
update: {},
|
||||
create: {
|
||||
originalUrl: link.originalUrl,
|
||||
shortCode: link.shortCode,
|
||||
title: link.title,
|
||||
displayOnPortal: link.displayOnPortal,
|
||||
userId: link.userId,
|
||||
tags: {
|
||||
connectOrCreate: link.tags.map((name) => ({
|
||||
where: { name },
|
||||
create: { name },
|
||||
})),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Database seeded successfully!");
|
||||
console.log("Admin account: [email protected] / password123");
|
||||
console.log("Demo account: [email protected] / password123");
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
Reference in new issue
Block a user