Compare commits

...

3 Commits

Author SHA1 Message Date
8ff4f9be1c docs: add documentation on registration disable feature 2025-04-08 16:54:55 -04:00
192e38cd67 feat: allow registration disabling 2025-04-08 16:54:38 -04:00
064b17fc88 chore: bump 2025-04-08 16:53:44 -04:00
7 changed files with 266 additions and 191 deletions

View File

@ -31,12 +31,19 @@ export default async function Login() {
<SiAuthentik /> <SiAuthentik />
Sign in with Authentik Sign in with Authentik
</Button> </Button>
<Link href="/account/signup" className="text-sm underline"> {process.env.SIGNUP_ENABLED === "true" ? (
<Link href="/account/signup">
<Button variant="outline" className="w-full"> <Button variant="outline" className="w-full">
<UserPlus /> <UserPlus />
Create an Account Create an Account
</Button> </Button>
</Link> </Link>
) : (
<Button variant="outline" className="w-full cursor-not-allowed" disabled>
<UserPlus />
Registration is Closed
</Button>
)}
</form> </form>
</CardContent> </CardContent>
<CardFooter className="justify-center"> <CardFooter className="justify-center">

View File

@ -0,0 +1,17 @@
import React from "react"
export default function SignupLayout({
children,
}: {
children: React.ReactNode
}) {
if (process.env.SIGNUP_ENABLED === "false") {
return <div>Signup is disabled</div>
} else if (process.env.SIGNUP_ENABLED === "true") {
return (
<div className="min-h-screen bg-background">{children}</div>
)
} else {
return <div>Invalid SIGNUP_ENABLED environment variable</div>
}
}

View File

@ -7,12 +7,15 @@ import { validateToken } from "@/lib/utils"
// (2) Migrate a p0ntus mail account to a LibreCloud account (creates/migrates Authentik/Email) // (2) Migrate a p0ntus mail account to a LibreCloud account (creates/migrates Authentik/Email)
async function createEmail(email: string, password: string, migrate: boolean) { async function createEmail(email: string, password: string, migrate: boolean) {
// Signup status check
if (process.env.SIGNUP_ENABLED === "false") {
return { success: false, message: "Signups are disabled" }
} else if (process.env.SIGNUP_ENABLED === "true") {
try { try {
if (!process.env.MAIL_CONNECT_API_URL) { if (!process.env.MAIL_CONNECT_API_URL) {
console.error("[!] Missing MAIL_CONNECT_API_URL environment variable") console.error("[!] Missing MAIL_CONNECT_API_URL environment variable")
return { success: false, message: "Server configuration error" } return { success: false, message: "Server configuration error" }
} } else {
const response = await axios.post(`${process.env.MAIL_CONNECT_API_URL}/accounts/add`, { const response = await axios.post(`${process.env.MAIL_CONNECT_API_URL}/accounts/add`, {
email, email,
password, password,
@ -29,6 +32,7 @@ async function createEmail(email: string, password: string, migrate: boolean) {
console.error("[!] Email creation failed with unknown error") console.error("[!] Email creation failed with unknown error")
return { success: false, message: "Failed to create email account" } return { success: false, message: "Failed to create email account" }
} }
}
} catch (error) { } catch (error) {
console.error("[!] Email creation error:", error) console.error("[!] Email creation error:", error)
if (axios.isAxiosError(error)) { if (axios.isAxiosError(error)) {
@ -39,9 +43,13 @@ async function createEmail(email: string, password: string, migrate: boolean) {
} }
return { success: false, message: "Failed to create email account" } return { success: false, message: "Failed to create email account" }
} }
} else {
return { success: false, message: "Account signup is not configured in your environment variables!" }
}
} }
export async function POST(request: Request) { export async function POST(request: Request) {
if (process.env.SIGNUP_ENABLED === "true") {
try { try {
const body = await request.json() const body = await request.json()
const { name, email, password, migrate, token } = body const { name, email, password, migrate, token } = body
@ -168,5 +176,10 @@ export async function POST(request: Request) {
console.error("[!] Unhandled error while creating user:", error) console.error("[!] Unhandled error while creating user:", error)
return NextResponse.json({ success: false, message: "An unexpected error occurred" }, { status: 500 }) return NextResponse.json({ success: false, message: "An unexpected error occurred" }, { status: 500 })
} }
} else if (process.env.SIGNUP_ENABLED === "false") {
return NextResponse.json({ success: false, message: "Signups are disabled" }, { status: 403 })
} else {
return NextResponse.json({ success: false, message: "Account signup is not configured in your environment variables!" }, { status: 500 })
}
} }

View File

@ -1,7 +1,7 @@
"use client" "use client"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { ArrowRight } from "lucide-react" import { ArrowRight, XCircle } from "lucide-react"
import { ReactTyped } from "react-typed" import { ReactTyped } from "react-typed"
import Link from "next/link"; import Link from "next/link";
@ -23,12 +23,19 @@ const Hero = () => {
</p> </p>
<div className="mt-10 max-w-md mx-auto sm:flex sm:justify-center"> <div className="mt-10 max-w-md mx-auto sm:flex sm:justify-center">
<div className="rounded-md shadow-sm"> <div className="rounded-md shadow-sm">
{process.env.SIGNUP_ENABLED === "true" ? (
<Link href="/account/login"> <Link href="/account/login">
<Button className="py-6 px-8"> <Button className="py-6 px-8">
Get started Get Started
<ArrowRight className="ml-2 h-5 w-5" /> <ArrowRight className="ml-2 h-5 w-5" />
</Button> </Button>
</Link> </Link>
) : (
<Button className="py-6 px-8 cursor-not-allowed" disabled>
<XCircle className="mr-2 h-5 w-5" />
Registration Closed
</Button>
)}
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,7 +1,7 @@
"use client" "use client"
import type React from "react" import type React from "react"
import { Check, ChevronRight, Clock } from "lucide-react" import { Check, ChevronRight, Clock, XCircle } from "lucide-react"
import { Separator } from "@/components/ui/separator" import { Separator } from "@/components/ui/separator"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
@ -145,6 +145,21 @@ const PricingCard: React.FC<PricingCardProps> = ({
)} )}
</div> </div>
<div> <div>
{title !== "Everything" ? (
<motion.span
variants={textVariants}
className="text-sm text-muted-foreground block mb-1 group-hover:text-muted-foreground/90 transition-colors duration-300"
>
Starting at
</motion.span>
) : (
<motion.span
variants={textVariants}
className="text-sm text-muted-foreground italic block mb-1 group-hover:text-muted-foreground/90 transition-colors duration-300"
>
Always
</motion.span>
)}
<motion.span <motion.span
variants={textVariants} variants={textVariants}
className="text-6xl font-bold text-foreground group-hover:text-foreground/90 transition-colors duration-300" className="text-6xl font-bold text-foreground group-hover:text-foreground/90 transition-colors duration-300"
@ -166,15 +181,22 @@ const PricingCard: React.FC<PricingCardProps> = ({
</motion.p> </motion.p>
{isComingSoon ? ( {isComingSoon ? (
<Button className="w-full" size="lg" variant="outline" disabled> <Button className="w-full" size="lg" variant="outline" disabled>
<Clock className="mr-2" /> Coming Soon <Clock /> Coming Soon
</Button> </Button>
) : buttonText ? ( ) : buttonText ? (
process.env.SIGNUP_ENABLED === "true" ? (
<Link href="/account/login" className="block"> <Link href="/account/login" className="block">
<Button className="w-full group" size="lg"> <Button className="w-full group" size="lg">
{buttonText} {buttonText}
{buttonIcon} {buttonIcon}
</Button> </Button>
</Link> </Link>
) : (
<Button className="w-full" size="lg" variant="outline" disabled>
<XCircle className="h-5 w-5" />
Registration Closed
</Button>
)
) : null} ) : null}
</div> </div>
<Separator className="bg-border" /> <Separator className="bg-border" />
@ -215,7 +237,7 @@ export default function Pricing(): React.ReactElement {
features={features.everything} features={features.everything}
badge="Most Popular" badge="Most Popular"
buttonText="Get Started" buttonText="Get Started"
buttonIcon={<ChevronRight className="ml-2 h-4 w-4" />} buttonIcon={<ChevronRight className="h-4 w-4" />}
/> />
<PricingCard <PricingCard

View File

@ -4,6 +4,15 @@ At the time of writing, LibreCloud is not in the state of perfection,
and as such we are expecting that you have a setup exact to ours. and as such we are expecting that you have a setup exact to ours.
While this will change in the future, we still suggest that provide all the listed environment variables. While this will change in the future, we still suggest that provide all the listed environment variables.
## Primary
These are the environment variables which handle how `librecloud/web` functions.
With these variables, you can disable entire parts of the dashboard, such as registration.
| Environment Variable | Description | Expected Value |
|----------------------|-----------------------------------------------------------|----------------------------------------|
| SIGNUP_ENABLED | Controls if the signup page and APIs are enabled/disabled | `true` (Enabled) or `false` (Disabled) |
## Authentik ## Authentik
We use [Auth.js](https://authjs.dev) to provide authentication for users through Authentik. We use [Auth.js](https://authjs.dev) to provide authentication for users through Authentik.

View File

@ -10,20 +10,20 @@
}, },
"dependencies": { "dependencies": {
"@hookform/resolvers": "^3.10.0", "@hookform/resolvers": "^3.10.0",
"@prisma/client": "^6.5.0", "@prisma/client": "^6.6.0",
"@radix-ui/react-avatar": "^1.1.3", "@radix-ui/react-avatar": "^1.1.4",
"@radix-ui/react-collapsible": "^1.1.3", "@radix-ui/react-collapsible": "^1.1.4",
"@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-dropdown-menu": "^2.1.7",
"@radix-ui/react-label": "^2.1.2", "@radix-ui/react-label": "^2.1.3",
"@radix-ui/react-popover": "^1.1.6", "@radix-ui/react-popover": "^1.1.7",
"@radix-ui/react-progress": "^1.1.2", "@radix-ui/react-progress": "^1.1.3",
"@radix-ui/react-radio-group": "^1.2.3", "@radix-ui/react-radio-group": "^1.2.4",
"@radix-ui/react-select": "^2.1.6", "@radix-ui/react-select": "^2.1.7",
"@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-separator": "^1.1.3",
"@radix-ui/react-switch": "^1.1.3", "@radix-ui/react-switch": "^1.1.4",
"@radix-ui/react-tabs": "^1.1.3", "@radix-ui/react-tabs": "^1.1.4",
"@radix-ui/react-tooltip": "^1.1.8", "@radix-ui/react-tooltip": "^1.2.0",
"@web3icons/react": "^4.0.10", "@web3icons/react": "^4.0.13",
"axios": "^1.8.4", "axios": "^1.8.4",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
@ -32,16 +32,16 @@
"geist": "^1.3.1", "geist": "^1.3.1",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"lucide-react": "^0.474.0", "lucide-react": "^0.474.0",
"motion": "^12.6.2", "motion": "^12.6.3",
"next": "^15.2.4", "next": "^15.2.4",
"next-auth": "^5.0.0-beta.25", "next-auth": "^5.0.0-beta.25",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"next-turnstile": "^1.0.2", "next-turnstile": "^1.0.2",
"password-validator": "^5.3.0", "password-validator": "^5.3.0",
"prisma": "^6.5.0", "prisma": "^6.6.0",
"react": "^19.0.0", "react": "^19.1.0",
"react-dom": "^19.0.0", "react-dom": "^19.1.0",
"react-hook-form": "^7.54.2", "react-hook-form": "^7.55.0",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-typed": "^2.0.12", "react-typed": "^2.0.12",
"tailwind-merge": "^2.6.0", "tailwind-merge": "^2.6.0",
@ -50,16 +50,16 @@
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.3.1", "@eslint/eslintrc": "^3.3.1",
"@tailwindcss/postcss": "^4.0.17", "@tailwindcss/postcss": "^4.1.3",
"@types/js-cookie": "^3.0.6", "@types/js-cookie": "^3.0.6",
"@types/node": "^20.17.28", "@types/node": "^20.17.30",
"@types/react": "^19.0.12", "@types/react": "^19.1.0",
"@types/react-dom": "^19.0.4", "@types/react-dom": "^19.1.1",
"@types/validator": "^13.12.3", "@types/validator": "^13.12.3",
"eslint": "^9.23.0", "eslint": "^9.24.0",
"eslint-config-next": "15.1.6", "eslint-config-next": "15.1.6",
"postcss": "^8.5.3", "postcss": "^8.5.3",
"tailwindcss": "^4.0.17", "tailwindcss": "^4.1.3",
"typescript": "^5.8.2" "typescript": "^5.8.3"
} }
} }