refactor: convert to bun workspaces monorepo

- Move web app into apps/web/
- Create packages/shared/ with shared types
- Create plugins/decky-vault/ scaffold
- Root package.json manages workspaces only
This commit is contained in:
2026-06-28 05:20:28 +08:00
parent c4bede20d4
commit cd72b7a948
345 changed files with 488 additions and 126 deletions
+43
View File
@@ -0,0 +1,43 @@
import { describe, it, expect, beforeAll } from "vitest"
// Auth integration tests require a fully-functional PostgreSQL database with
// the application schema (set via DATABASE_URL). If the env var is missing or
// the DB is unreachable, every test is skipped rather than failing.
let dbUsable = false
let dbHelper: any
if (process.env.DATABASE_URL) {
try {
const { db } = await import("@/lib/db/index")
await db.execute("SELECT 1")
dbUsable = true
} catch {
dbUsable = false
}
}
describe(dbUsable ? "Better-Auth integration" : "Better-Auth integration (skipped: database not usable)", () => {
beforeAll(async () => {
if (!dbUsable) return
const { getTestHelpers } = await import("@/lib/auth/test")
dbHelper = await getTestHelpers()
})
it("should create a user", async () => {
if (!dbUsable || !dbHelper) return
const user = await dbHelper.createUser({ email: "test@example.com" })
expect(user.email).toBe("test@example.com")
})
it("should create a session for a user", async () => {
if (!dbUsable || !dbHelper) return
const user = await dbHelper.createUser({ email: "session-test@example.com" })
await dbHelper.saveUser(user)
const { session, headers } = await dbHelper.login({ userId: user.id })
expect(session.userId).toBe(user.id)
expect(headers.get("cookie")).toBeTruthy()
await dbHelper.deleteUser(user.id)
})
})
@@ -0,0 +1,51 @@
import { describe, it, expect } from "vitest"
import { isDeckyVaultEmail, DOMAIN_BLOCK_ERROR } from "../domain-block"
describe("isDeckyVaultEmail", () => {
it("returns true for a @deckyvault.xyz email (lowercase)", () => {
expect(isDeckyVaultEmail("user@deckyvault.xyz")).toBe(true)
})
it("returns true for a @deckyvault.xyz email (mixed case)", () => {
expect(isDeckyVaultEmail("User@DeckyVault.xyz")).toBe(true)
})
it("returns true for a @deckyvault.xyz email with plus addressing", () => {
expect(isDeckyVaultEmail("user+tag@deckyvault.xyz")).toBe(true)
})
it("returns true for a @deckyvault.xyz email with surrounding whitespace", () => {
expect(isDeckyVaultEmail(" admin@deckyvault.xyz ")).toBe(true)
})
it("returns false for a @gmail.com email", () => {
expect(isDeckyVaultEmail("user@gmail.com")).toBe(false)
})
it("returns false for a @example.com email", () => {
expect(isDeckyVaultEmail("user@example.com")).toBe(false)
})
it("returns false for a subdomain like @mail.deckyvault.xyz", () => {
expect(isDeckyVaultEmail("user@mail.deckyvault.xyz")).toBe(false)
})
it("returns false for an empty string", () => {
expect(isDeckyVaultEmail("")).toBe(false)
})
it("returns false for a string without @", () => {
expect(isDeckyVaultEmail("deckyvault.xyz")).toBe(false)
})
it("returns true for a .DECKYVAULT.XYZ email (uppercase domain)", () => {
expect(isDeckyVaultEmail("admin@DECKYVAULT.XYZ")).toBe(true)
})
})
describe("DOMAIN_BLOCK_ERROR", () => {
it("is a non-empty string", () => {
expect(DOMAIN_BLOCK_ERROR).toBeTruthy()
expect(typeof DOMAIN_BLOCK_ERROR).toBe("string")
})
})
+165
View File
@@ -0,0 +1,165 @@
import { describe, it, expect, vi, beforeEach, beforeAll } from "vitest"
// Mock the auth module — no real betterAuth initialisation should run.
vi.mock("@/lib/auth", () => ({
auth: {
api: {
getSession: vi.fn(),
},
},
}))
// Use dynamic imports so vi.mock is guaranteed to be registered before
// any module evaluation in bun test's shared-module-cache multi-file mode.
let requireAuth: typeof import("../guard")["requireAuth"]
let requireRole: typeof import("../guard")["requireRole"]
let requireAdmin: typeof import("../guard")["requireAdmin"]
let requireContributorOrAdmin: typeof import("../guard")["requireContributorOrAdmin"]
let requireModeratorOrAdmin: typeof import("../guard")["requireModeratorOrAdmin"]
let getSession: ReturnType<typeof vi.fn>
beforeAll(async () => {
const guard = await import("../guard")
requireAuth = guard.requireAuth
requireRole = guard.requireRole
requireAdmin = guard.requireAdmin
requireContributorOrAdmin = guard.requireContributorOrAdmin
requireModeratorOrAdmin = guard.requireModeratorOrAdmin
const mocked = await import("@/lib/auth")
getSession = mocked.auth.api.getSession
})
function mockSession(overrides: Record<string, unknown> = {}) {
getSession.mockResolvedValue({
user: {
id: "user-1",
name: "Test User",
email: "test@example.com",
role: "user",
...overrides,
},
session: {
id: "session-1",
userId: "user-1",
token: "token-abc",
},
})
}
function mockNoSession() {
getSession.mockResolvedValue(null)
}
beforeEach(() => {
getSession.mockReset()
})
describe("requireAuth", () => {
it("returns ok with user and session when authenticated", async () => {
mockSession()
const result = await requireAuth(new Headers())
expect(result.ok).toBe(true)
if (result.ok) {
expect(result.user.id).toBe("user-1")
expect(result.session.id).toBe("session-1")
}
})
it("returns error 401 when not authenticated", async () => {
mockNoSession()
const result = await requireAuth(new Headers())
expect(result.ok).toBe(false)
if (!result.ok) {
expect(result.status).toBe(401)
expect(result.error).toBe("Unauthorized")
}
})
})
describe("requireRole", () => {
it("allows user with matching role", async () => {
mockSession({ role: "admin" })
const result = await requireRole(new Headers(), ["admin"])
expect(result.ok).toBe(true)
})
it("denies user with non-matching role (403)", async () => {
mockSession({ role: "user" })
const result = await requireRole(new Headers(), ["admin"])
expect(result.ok).toBe(false)
if (!result.ok) {
expect(result.status).toBe(403)
expect(result.error).toBe("Forbidden")
}
})
it("returns 401 when not authenticated", async () => {
mockNoSession()
const result = await requireRole(new Headers(), ["admin"])
expect(result.ok).toBe(false)
if (!result.ok) {
expect(result.status).toBe(401)
}
})
it("defaults to 'user' role when role is null", async () => {
mockSession({ role: null })
const result = await requireRole(new Headers(), ["user"])
expect(result.ok).toBe(true)
})
})
describe("requireAdmin", () => {
it("allows admin user", async () => {
mockSession({ role: "admin" })
const result = await requireAdmin(new Headers())
expect(result.ok).toBe(true)
})
it("denies non-admin user (403)", async () => {
mockSession({ role: "moderator" })
const result = await requireAdmin(new Headers())
expect(result.ok).toBe(false)
})
})
describe("requireContributorOrAdmin", () => {
it("allows contributor", async () => {
mockSession({ role: "contributor" })
const result = await requireContributorOrAdmin(new Headers())
expect(result.ok).toBe(true)
})
it("allows admin", async () => {
mockSession({ role: "admin" })
const result = await requireContributorOrAdmin(new Headers())
expect(result.ok).toBe(true)
})
it("denies regular user", async () => {
mockSession({ role: "user" })
const result = await requireContributorOrAdmin(new Headers())
expect(result.ok).toBe(false)
})
})
describe("requireModeratorOrAdmin", () => {
it("allows moderator", async () => {
mockSession({ role: "moderator" })
const result = await requireModeratorOrAdmin(new Headers())
expect(result.ok).toBe(true)
})
it("allows admin", async () => {
mockSession({ role: "admin" })
const result = await requireModeratorOrAdmin(new Headers())
expect(result.ok).toBe(true)
})
it("denies contributor", async () => {
mockSession({ role: "contributor" })
const result = await requireModeratorOrAdmin(new Headers())
expect(result.ok).toBe(false)
})
})
@@ -0,0 +1,63 @@
import { describe, it, expect } from "vitest"
import { checkPasswordStrength } from "../password-strength"
describe("checkPasswordStrength", () => {
it("returns weak (score 0) for empty string", () => {
const result = checkPasswordStrength("")
expect(result.level).toBe("weak")
expect(result.score).toBe(0)
expect(result.feedback).toContain("Enter a password")
})
it("returns fair for 'password' (common pattern penalty)", () => {
const result = checkPasswordStrength("password")
expect(result.level).toBe("fair")
expect(result.feedback).toContain("Avoid common passwords")
})
it("returns fair for short password with only letters", () => {
const result = checkPasswordStrength("abc")
expect(result.level).toBe("fair")
})
it("returns good for 10-char lowercase-only password", () => {
const result = checkPasswordStrength("abcdefghij")
expect(result.level).toBe("good")
})
it("returns good for 10-char mixed case + numbers", () => {
const result = checkPasswordStrength("AbcDef1234")
expect(["good", "strong", "excellent"]).toContain(result.level)
})
it("returns strong for 14-char with all character types", () => {
const result = checkPasswordStrength("MyP@ssw0rd!2024")
expect(["strong", "excellent"]).toContain(result.level)
})
it("returns excellent for 18+ char complex password", () => {
const result = checkPasswordStrength("C0mpl3x!P@ssw0rd#2024")
expect(result.level).toBe("excellent")
})
it("flags repeated characters", () => {
const result = checkPasswordStrength("aaaBBB111@@@")
expect(result.feedback).toContain("Avoid repeated characters")
})
it("flags common patterns like 'letmein'", () => {
const result = checkPasswordStrength("letmein1234")
expect(result.feedback).toContain("Avoid common passwords")
})
it("gives positive feedback for excellent passwords", () => {
const result = checkPasswordStrength("Tr0ub4dor&3X7r3m3ly!")
expect(result.feedback).toContain("Great password!")
})
it("handles unicode characters", () => {
const result = checkPasswordStrength("パスワード1234Abc!")
expect(result.level).toBeDefined()
expect(result.score).toBeGreaterThan(0)
})
})
@@ -0,0 +1,236 @@
import { describe, it, expect } from "vitest"
import {
loginEmailSchema,
loginSchema,
signupSchema,
otpSchema,
forgotPasswordSchema,
resetPasswordSchema,
} from "../validation"
describe("loginEmailSchema", () => {
it("accepts a valid email", () => {
const result = loginEmailSchema.safeParse({ email: "user@example.com" })
expect(result.success).toBe(true)
})
it("rejects empty string", () => {
const result = loginEmailSchema.safeParse({ email: "" })
expect(result.success).toBe(false)
})
it("rejects invalid email format", () => {
const result = loginEmailSchema.safeParse({ email: "not-an-email" })
expect(result.success).toBe(false)
})
it("rejects missing email field", () => {
const result = loginEmailSchema.safeParse({})
expect(result.success).toBe(false)
})
})
describe("loginSchema", () => {
it("accepts valid email and password", () => {
const result = loginSchema.safeParse({
email: "user@example.com",
password: "password123",
})
expect(result.success).toBe(true)
})
it("rejects empty password", () => {
const result = loginSchema.safeParse({
email: "user@example.com",
password: "",
})
expect(result.success).toBe(false)
if (!result.success) {
expect(result.error.issues[0].message).toBe("Password is required")
}
})
it("rejects missing password field", () => {
const result = loginSchema.safeParse({ email: "user@example.com" })
expect(result.success).toBe(false)
})
})
describe("signupSchema", () => {
it("accepts valid name, email, and password (≥10 chars)", () => {
const result = signupSchema.safeParse({
name: "Test User",
email: "user@example.com",
password: "abcdefghij",
})
expect(result.success).toBe(true)
})
it("rejects short password (<10 chars)", () => {
const result = signupSchema.safeParse({
name: "Test User",
email: "user@example.com",
password: "short",
})
expect(result.success).toBe(false)
if (!result.success) {
expect(result.error.issues[0].message).toContain("at least 10 characters")
}
})
it("rejects empty name", () => {
const result = signupSchema.safeParse({
name: "",
email: "user@example.com",
password: "abcdefghij",
})
expect(result.success).toBe(false)
})
it("rejects name longer than 100 chars", () => {
const result = signupSchema.safeParse({
name: "a".repeat(101),
email: "user@example.com",
password: "abcdefghij",
})
expect(result.success).toBe(false)
})
})
describe("otpSchema", () => {
it("accepts exactly 6 digits", () => {
const result = otpSchema.safeParse({ otp: "123456" })
expect(result.success).toBe(true)
})
it("rejects 5 digits", () => {
const result = otpSchema.safeParse({ otp: "12345" })
expect(result.success).toBe(false)
})
it("rejects 7 digits", () => {
const result = otpSchema.safeParse({ otp: "1234567" })
expect(result.success).toBe(false)
})
it("accepts any 6-char string (schema only validates length)", () => {
const result = otpSchema.safeParse({ otp: "abc123" })
expect(result.success).toBe(true)
})
})
describe("forgotPasswordSchema", () => {
it("accepts valid email", () => {
const result = forgotPasswordSchema.safeParse({ email: "user@example.com" })
expect(result.success).toBe(true)
})
it("rejects invalid email", () => {
const result = forgotPasswordSchema.safeParse({ email: "bad" })
expect(result.success).toBe(false)
})
})
describe("resetPasswordSchema", () => {
it("accepts matching passwords with valid OTP", () => {
const result = resetPasswordSchema.safeParse({
otp: "123456",
newPassword: "abcdefghij",
confirmPassword: "abcdefghij",
})
expect(result.success).toBe(true)
})
it("rejects mismatched passwords", () => {
const result = resetPasswordSchema.safeParse({
otp: "123456",
newPassword: "abcdefghij",
confirmPassword: "different",
})
expect(result.success).toBe(false)
if (!result.success) {
expect(result.error.issues[0].message).toBe("Passwords do not match")
}
})
it("rejects short new password", () => {
const result = resetPasswordSchema.safeParse({
otp: "123456",
newPassword: "short",
confirmPassword: "short",
})
expect(result.success).toBe(false)
})
it("rejects empty confirm password", () => {
const result = resetPasswordSchema.safeParse({
otp: "123456",
newPassword: "abcdefghij",
confirmPassword: "",
})
expect(result.success).toBe(false)
})
it("rejects short OTP", () => {
const result = resetPasswordSchema.safeParse({
otp: "12345",
newPassword: "abcdefghij",
confirmPassword: "abcdefghij",
})
expect(result.success).toBe(false)
})
})
describe("signupSchema — domain block", () => {
it("rejects @deckyvault.xyz email when NODE_ENV is not development", () => {
const result = signupSchema.safeParse({
name: "Test User",
email: "user@deckyvault.xyz",
password: "abcdefghij",
})
expect(result.success).toBe(false)
if (!result.success) {
const emailIssues = result.error.issues.filter(
(i) => i.path[0] === "email"
)
expect(emailIssues.length).toBeGreaterThan(0)
expect(emailIssues[0].message).toContain("deckyvault.xyz")
}
})
it("rejects @DECKYVAULT.XYZ email (uppercase) when not in dev", () => {
const result = signupSchema.safeParse({
name: "Test User",
email: "admin@DECKYVAULT.XYZ",
password: "abcdefghij",
})
expect(result.success).toBe(false)
})
it("rejects @deckyvault.xyz with plus addressing when not in dev", () => {
const result = signupSchema.safeParse({
name: "Test User",
email: "user+spam@deckyvault.xyz",
password: "abcdefghij",
})
expect(result.success).toBe(false)
})
it("accepts @gmail.com email (non-deckyvault domain)", () => {
const result = signupSchema.safeParse({
name: "Test User",
email: "user@gmail.com",
password: "abcdefghij",
})
expect(result.success).toBe(true)
})
it("accepts valid email from any non-deckyvault domain", () => {
const result = signupSchema.safeParse({
name: "Test User",
email: "hello@outlook.com",
password: "abcdefghij",
})
expect(result.success).toBe(true)
})
})