feat: allow registration disabling
This commit is contained in:
parent
064b17fc88
commit
192e38cd67
@ -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" ? (
|
||||||
<Button variant="outline" className="w-full">
|
<Link href="/account/signup">
|
||||||
|
<Button variant="outline" className="w-full">
|
||||||
|
<UserPlus />
|
||||||
|
Create an Account
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<Button variant="outline" className="w-full cursor-not-allowed" disabled>
|
||||||
<UserPlus />
|
<UserPlus />
|
||||||
Create an Account
|
Registration is Closed
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
)}
|
||||||
</form>
|
</form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter className="justify-center">
|
<CardFooter className="justify-center">
|
||||||
|
17
app/account/signup/layout.tsx
Normal file
17
app/account/signup/layout.tsx
Normal 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>
|
||||||
|
}
|
||||||
|
}
|
@ -7,166 +7,179 @@ 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) {
|
||||||
try {
|
// Signup status check
|
||||||
if (!process.env.MAIL_CONNECT_API_URL) {
|
if (process.env.SIGNUP_ENABLED === "false") {
|
||||||
console.error("[!] Missing MAIL_CONNECT_API_URL environment variable")
|
return { success: false, message: "Signups are disabled" }
|
||||||
return { success: false, message: "Server configuration error" }
|
} else if (process.env.SIGNUP_ENABLED === "true") {
|
||||||
}
|
try {
|
||||||
|
if (!process.env.MAIL_CONNECT_API_URL) {
|
||||||
const response = await axios.post(`${process.env.MAIL_CONNECT_API_URL}/accounts/add`, {
|
console.error("[!] Missing MAIL_CONNECT_API_URL environment variable")
|
||||||
email,
|
return { success: false, message: "Server configuration error" }
|
||||||
password,
|
} else {
|
||||||
migrate,
|
const response = await axios.post(`${process.env.MAIL_CONNECT_API_URL}/accounts/add`, {
|
||||||
})
|
email,
|
||||||
|
password,
|
||||||
const responseData = response.data
|
migrate,
|
||||||
if (responseData.success) {
|
})
|
||||||
return response.data
|
|
||||||
} else if (responseData.error) {
|
const responseData = response.data
|
||||||
console.error("[!] Email creation failed:", responseData.error)
|
if (responseData.success) {
|
||||||
return { success: false, message: responseData.error }
|
return response.data
|
||||||
} else {
|
} else if (responseData.error) {
|
||||||
console.error("[!] Email creation failed with unknown error")
|
console.error("[!] Email creation failed:", responseData.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" }
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} else {
|
||||||
console.error("[!] Email creation error:", error)
|
return { success: false, message: "Account signup is not configured in your environment variables!" }
|
||||||
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) {
|
||||||
try {
|
if (process.env.SIGNUP_ENABLED === "true") {
|
||||||
const body = await request.json()
|
try {
|
||||||
const { name, email, password, migrate, token } = body
|
const body = await request.json()
|
||||||
|
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)
|
const tokenValidation = await validateToken(token)
|
||||||
return NextResponse.json({ success: false, message: "Failed to create user account" }, { status: 500 })
|
if (!tokenValidation.success) {
|
||||||
}
|
console.error("Turnstile validation failed:", tokenValidation.error)
|
||||||
|
return NextResponse.json({ success: false, message: "Robot check failed, try refreshing" }, { status: 400 })
|
||||||
// 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, {
|
if (!process.env.AUTHENTIK_API_URL || !process.env.AUTHENTIK_API_KEY) {
|
||||||
status: emailRes.success ? 200 : 500,
|
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
|
||||||
})
|
})
|
||||||
} 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)
|
if (response.data?.detail) {
|
||||||
return NextResponse.json({ success: false, message: "An unexpected error occurred" }, { status: 500 })
|
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 })
|
||||||
|
} else {
|
||||||
|
return NextResponse.json({ success: false, message: "Account signup is not configured in your environment variables!" }, { status: 500 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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">
|
||||||
<Link href="/account/login">
|
{process.env.SIGNUP_ENABLED === "true" ? (
|
||||||
<Button className="py-6 px-8">
|
<Link href="/account/login">
|
||||||
Get started
|
<Button className="py-6 px-8">
|
||||||
<ArrowRight className="ml-2 h-5 w-5" />
|
Get Started
|
||||||
|
<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>
|
||||||
|
@ -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 ? (
|
||||||
<Link href="/account/login" className="block">
|
process.env.SIGNUP_ENABLED === "true" ? (
|
||||||
<Button className="w-full group" size="lg">
|
<Link href="/account/login" className="block">
|
||||||
{buttonText}
|
<Button className="w-full group" size="lg">
|
||||||
{buttonIcon}
|
{buttonText}
|
||||||
|
{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" />
|
||||||
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user