Compare commits

..

No commits in common. "8ff4f9be1cd01a05b0afd8e7020753e7b34a3271" and "49e38337563de79ddd9ae3cf1b1ec9d1a2e28a6f" have entirely different histories.

7 changed files with 196 additions and 271 deletions

View File

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

View File

@ -1,17 +0,0 @@
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,179 +7,166 @@ 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 try {
if (process.env.SIGNUP_ENABLED === "false") { if (!process.env.MAIL_CONNECT_API_URL) {
return { success: false, message: "Signups are disabled" } console.error("[!] Missing MAIL_CONNECT_API_URL environment variable")
} else if (process.env.SIGNUP_ENABLED === "true") { return { success: false, message: "Server configuration error" }
try { }
if (!process.env.MAIL_CONNECT_API_URL) {
console.error("[!] Missing MAIL_CONNECT_API_URL environment variable") const response = await axios.post(`${process.env.MAIL_CONNECT_API_URL}/accounts/add`, {
return { success: false, message: "Server configuration error" } email,
} else { password,
const response = await axios.post(`${process.env.MAIL_CONNECT_API_URL}/accounts/add`, { migrate,
email, })
password,
migrate, const responseData = response.data
}) if (responseData.success) {
return response.data
const responseData = response.data } else if (responseData.error) {
if (responseData.success) { console.error("[!] Email creation failed:", responseData.error)
return response.data return { success: false, message: responseData.error }
} else if (responseData.error) { } else {
console.error("[!] Email creation failed:", responseData.error) console.error("[!] Email creation failed with unknown error")
return { success: false, message: responseData.error }
} else {
console.error("[!] Email creation failed with unknown error")
return { success: false, message: "Failed to create email account" }
}
}
} catch (error) {
console.error("[!] Email creation error:", error)
if (axios.isAxiosError(error)) {
return {
success: false,
message: error.response?.data?.error || "Failed to connect to email service",
}
}
return { success: false, message: "Failed to create email account" } return { success: false, message: "Failed to create email account" }
} }
} else { } catch (error) {
return { success: false, message: "Account signup is not configured in your environment variables!" } console.error("[!] Email creation error:", error)
if (axios.isAxiosError(error)) {
return {
success: false,
message: error.response?.data?.error || "Failed to connect to email service",
}
}
return { success: false, message: "Failed to create email account" }
} }
} }
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
// Validate fields // Validate fields
if (!name || !email || !password) { if (!name || !email || !password) {
return NextResponse.json({ success: false, message: "The form you submitted is incomplete" }, { status: 400 }) return NextResponse.json({ success: false, message: "The form you submitted is incomplete" }, { status: 400 })
}
const tokenValidation = await validateToken(token)
if (!tokenValidation.success) {
console.error("Turnstile validation failed:", tokenValidation.error)
return NextResponse.json({ success: false, message: "Robot check failed, try refreshing" }, { status: 400 })
}
if (!process.env.AUTHENTIK_API_URL || !process.env.AUTHENTIK_API_KEY) {
console.error("Missing Authentik environment variables")
return NextResponse.json({ success: false, message: "Server configuration error" }, { status: 500 })
}
// Create Authentik user
const genUser = email.split("@")[0]
const userData = {
username: genUser,
name,
is_active: true,
groups: [
"b2c38bad-1d15-4ffd-b6d4-d95370a092ca" // this represents the "Users" group in Authentik
],
email,
type: "internal",
}
console.log("[i] Creating user in Authentik:", { username: genUser, email })
const response = await axios.request({
method: "post",
maxBodyLength: Infinity,
url: `${process.env.AUTHENTIK_API_URL}/core/users/`,
headers: {
"Content-Type": "application/json",
Accept: "application/json",
Authorization: `Bearer ${process.env.AUTHENTIK_API_KEY}`,
},
data: JSON.stringify(userData),
validateStatus: () => true, // capture response even for error status codes
})
if (response.data?.detail) {
console.error("[!] Authentik user creation issue:", response.data.detail)
}
if (response.status !== 201) {
if (response.data.username && response.data.username[0] === "This field must be unique.") {
return NextResponse.json({ success: false, message: "Username already exists" }, { status: 409 })
}
console.error("Failed to create user in Authentik:", response.status, response.data)
return NextResponse.json({ success: false, message: "Failed to create user account" }, { status: 500 })
}
// User created successfully, now set password
const userID = response.data.pk
const updData = {
password,
}
console.log("[i] Setting password for user:", userID)
const updCfg = await axios.request({
method: "post",
maxBodyLength: Number.POSITIVE_INFINITY,
url: `${process.env.AUTHENTIK_API_URL}/core/users/${userID}/set_password/`,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.AUTHENTIK_API_KEY}`,
},
data: updData,
validateStatus: () => true, // capture response even for error status codes
})
if (updCfg.data?.detail) {
console.error("[!] Password setting issue:", updCfg.data.detail)
}
if (updCfg.status === 204) {
// account created successfully, now create email
console.log("[i] Creating email account for:", email)
const emailRes = await createEmail(email, password, migrate)
if (emailRes.success) {
console.log("[S] Account creation successful for:", email)
} else {
console.error("[!] Email creation failed for:", email, emailRes.message)
}
return NextResponse.json(emailRes, {
status: emailRes.success ? 200 : 500,
})
} else if (updCfg.status === 400) {
console.error("[!] Failed to set password:", updCfg.data)
return NextResponse.json({ success: false, message: "Invalid password format" }, { status: 400 })
} else {
console.error("[!] Unknown error setting password:", updCfg.status, updCfg.data)
return NextResponse.json({ success: false, message: "Failed to complete account setup" }, { status: 500 })
}
} catch (error: unknown) {
if (axios.isAxiosError(error)) {
if (error.response?.data?.detail) {
console.error("[!] Request error with detail:", error.response.data.detail)
return NextResponse.json({ success: false, message: "Server error - Failed to create user" }, { status: 500 })
} else if (error.response?.data?.error) {
console.error("[!] Request error (passed from Authentik):", error.response.data.error)
return NextResponse.json({ success: false, message: error.response.data.error }, { status: 500 })
} else if (error.code === "ECONNREFUSED" || error.code === "ENOTFOUND") {
console.error("[!] Connection error:", error.message)
return NextResponse.json(
{ success: false, message: "Failed to connect to authentication service" },
{ status: 503 },
)
}
}
console.error("[!] Unhandled error while creating user:", error)
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 }) const tokenValidation = await validateToken(token)
} else { if (!tokenValidation.success) {
return NextResponse.json({ success: false, message: "Account signup is not configured in your environment variables!" }, { status: 500 }) console.error("Turnstile validation failed:", tokenValidation.error)
return NextResponse.json({ success: false, message: "Robot check failed, try refreshing" }, { status: 400 })
}
if (!process.env.AUTHENTIK_API_URL || !process.env.AUTHENTIK_API_KEY) {
console.error("Missing Authentik environment variables")
return NextResponse.json({ success: false, message: "Server configuration error" }, { status: 500 })
}
// Create Authentik user
const genUser = email.split("@")[0]
const userData = {
username: genUser,
name,
is_active: true,
groups: [
"b2c38bad-1d15-4ffd-b6d4-d95370a092ca" // this represents the "Users" group in Authentik
],
email,
type: "internal",
}
console.log("[i] Creating user in Authentik:", { username: genUser, email })
const response = await axios.request({
method: "post",
maxBodyLength: Infinity,
url: `${process.env.AUTHENTIK_API_URL}/core/users/`,
headers: {
"Content-Type": "application/json",
Accept: "application/json",
Authorization: `Bearer ${process.env.AUTHENTIK_API_KEY}`,
},
data: JSON.stringify(userData),
validateStatus: () => true, // capture response even for error status codes
})
if (response.data?.detail) {
console.error("[!] Authentik user creation issue:", response.data.detail)
}
if (response.status !== 201) {
if (response.data.username && response.data.username[0] === "This field must be unique.") {
return NextResponse.json({ success: false, message: "Username already exists" }, { status: 409 })
}
console.error("Failed to create user in Authentik:", response.status, response.data)
return NextResponse.json({ success: false, message: "Failed to create user account" }, { status: 500 })
}
// User created successfully, now set password
const userID = response.data.pk
const updData = {
password,
}
console.log("[i] Setting password for user:", userID)
const updCfg = await axios.request({
method: "post",
maxBodyLength: Number.POSITIVE_INFINITY,
url: `${process.env.AUTHENTIK_API_URL}/core/users/${userID}/set_password/`,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.AUTHENTIK_API_KEY}`,
},
data: updData,
validateStatus: () => true, // capture response even for error status codes
})
if (updCfg.data?.detail) {
console.error("[!] Password setting issue:", updCfg.data.detail)
}
if (updCfg.status === 204) {
// account created successfully, now create email
console.log("[i] Creating email account for:", email)
const emailRes = await createEmail(email, password, migrate)
if (emailRes.success) {
console.log("[S] Account creation successful for:", email)
} else {
console.error("[!] Email creation failed for:", email, emailRes.message)
}
return NextResponse.json(emailRes, {
status: emailRes.success ? 200 : 500,
})
} else if (updCfg.status === 400) {
console.error("[!] Failed to set password:", updCfg.data)
return NextResponse.json({ success: false, message: "Invalid password format" }, { status: 400 })
} else {
console.error("[!] Unknown error setting password:", updCfg.status, updCfg.data)
return NextResponse.json({ success: false, message: "Failed to complete account setup" }, { status: 500 })
}
} catch (error: unknown) {
if (axios.isAxiosError(error)) {
if (error.response?.data?.detail) {
console.error("[!] Request error with detail:", error.response.data.detail)
return NextResponse.json({ success: false, message: "Server error - Failed to create user" }, { status: 500 })
} else if (error.response?.data?.error) {
console.error("[!] Request error (passed from Authentik):", error.response.data.error)
return NextResponse.json({ success: false, message: error.response.data.error }, { status: 500 })
} else if (error.code === "ECONNREFUSED" || error.code === "ENOTFOUND") {
console.error("[!] Connection error:", error.message)
return NextResponse.json(
{ success: false, message: "Failed to connect to authentication service" },
{ status: 503 },
)
}
}
console.error("[!] Unhandled error while creating user:", error)
return NextResponse.json({ success: false, message: "An unexpected error occurred" }, { 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, XCircle } from "lucide-react" import { ArrowRight } from "lucide-react"
import { ReactTyped } from "react-typed" import { ReactTyped } from "react-typed"
import Link from "next/link"; import Link from "next/link";
@ -23,19 +23,12 @@ 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>
</Link>
) : (
<Button className="py-6 px-8 cursor-not-allowed" disabled>
<XCircle className="mr-2 h-5 w-5" />
Registration Closed
</Button> </Button>
)} </Link>
</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, XCircle } from "lucide-react" import { Check, ChevronRight, Clock } 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,21 +145,6 @@ 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"
@ -181,22 +166,15 @@ 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 /> Coming Soon <Clock className="mr-2" /> 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>
</Link>
) : (
<Button className="w-full" size="lg" variant="outline" disabled>
<XCircle className="h-5 w-5" />
Registration Closed
</Button> </Button>
) </Link>
) : null} ) : null}
</div> </div>
<Separator className="bg-border" /> <Separator className="bg-border" />
@ -237,7 +215,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="h-4 w-4" />} buttonIcon={<ChevronRight className="ml-2 h-4 w-4" />}
/> />
<PricingCard <PricingCard

View File

@ -4,15 +4,6 @@ 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.6.0", "@prisma/client": "^6.5.0",
"@radix-ui/react-avatar": "^1.1.4", "@radix-ui/react-avatar": "^1.1.3",
"@radix-ui/react-collapsible": "^1.1.4", "@radix-ui/react-collapsible": "^1.1.3",
"@radix-ui/react-dropdown-menu": "^2.1.7", "@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-label": "^2.1.3", "@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-popover": "^1.1.7", "@radix-ui/react-popover": "^1.1.6",
"@radix-ui/react-progress": "^1.1.3", "@radix-ui/react-progress": "^1.1.2",
"@radix-ui/react-radio-group": "^1.2.4", "@radix-ui/react-radio-group": "^1.2.3",
"@radix-ui/react-select": "^2.1.7", "@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-separator": "^1.1.3", "@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-switch": "^1.1.4", "@radix-ui/react-switch": "^1.1.3",
"@radix-ui/react-tabs": "^1.1.4", "@radix-ui/react-tabs": "^1.1.3",
"@radix-ui/react-tooltip": "^1.2.0", "@radix-ui/react-tooltip": "^1.1.8",
"@web3icons/react": "^4.0.13", "@web3icons/react": "^4.0.10",
"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.3", "motion": "^12.6.2",
"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.6.0", "prisma": "^6.5.0",
"react": "^19.1.0", "react": "^19.0.0",
"react-dom": "^19.1.0", "react-dom": "^19.0.0",
"react-hook-form": "^7.55.0", "react-hook-form": "^7.54.2",
"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.1.3", "@tailwindcss/postcss": "^4.0.17",
"@types/js-cookie": "^3.0.6", "@types/js-cookie": "^3.0.6",
"@types/node": "^20.17.30", "@types/node": "^20.17.28",
"@types/react": "^19.1.0", "@types/react": "^19.0.12",
"@types/react-dom": "^19.1.1", "@types/react-dom": "^19.0.4",
"@types/validator": "^13.12.3", "@types/validator": "^13.12.3",
"eslint": "^9.24.0", "eslint": "^9.23.0",
"eslint-config-next": "15.1.6", "eslint-config-next": "15.1.6",
"postcss": "^8.5.3", "postcss": "^8.5.3",
"tailwindcss": "^4.1.3", "tailwindcss": "^4.0.17",
"typescript": "^5.8.3" "typescript": "^5.8.2"
} }
} }