feat: initial commit - EZBio Next.js 16 fullstack URL shortener & bio link

This commit is contained in:
Tien Tran committed 2026-09-28 00:28:09 -07:00
commit 569c10342f
64 files changed
+15670

No files matched your search

+127
View File
@@ -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
View File
@@ -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();
});