Merge pull request 'Implement Altcha over CF Turnstile' (#9) from altcha into main
Reviewed-on: #9
This commit is contained in:
commit
8c56456911
@ -15,14 +15,7 @@ import { useRouter } from "next/navigation"
|
||||
import { validateEmail, validatePassword } from "@/lib/utils"
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
|
||||
import EmailField from "@/components/custom/signup/EmailField"
|
||||
import Turnstile from "@/components/custom/Turnstile"
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
onTurnstileSuccess?: (token: string) => void
|
||||
onloadTurnstileCallback?: () => void
|
||||
}
|
||||
}
|
||||
import Altcha from "@/components/custom/Altcha"
|
||||
|
||||
export default function Signup() {
|
||||
const router = useRouter()
|
||||
@ -40,10 +33,11 @@ export default function Signup() {
|
||||
const [isValid, setIsValid] = useState(false)
|
||||
const [validationMessage, setValidationMessage] = useState("")
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [turnstileStatus, setTurnstileStatus] = useState<"success" | "error" | "expired" | "required">("required")
|
||||
const [altchaStatus, setAltchaStatus] = useState<"success" | "error" | "expired" | "required">("required")
|
||||
const formRef = useRef<HTMLFormElement>(null)
|
||||
const [errorAlert, setErrorAlert] = useState<string | null>(null)
|
||||
const [forceRefresh, setForceRefresh] = useState(false)
|
||||
const [altchaToken, setAltchaToken] = useState<string | null>(null)
|
||||
|
||||
const fadeInOut = {
|
||||
initial: { opacity: 0, y: 20 },
|
||||
@ -69,17 +63,15 @@ export default function Signup() {
|
||||
}
|
||||
}
|
||||
|
||||
const turnstileCallback = () => {
|
||||
console.log("[i] Turnstile token received")
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
window.onTurnstileSuccess = turnstileCallback
|
||||
|
||||
return () => {
|
||||
delete window.onTurnstileSuccess
|
||||
const handleAltchaStateChange = (e: Event | CustomEvent) => {
|
||||
if ('detail' in e && e.detail?.payload) {
|
||||
setAltchaToken(e.detail.payload)
|
||||
setAltchaStatus("success")
|
||||
} else {
|
||||
setAltchaToken(null)
|
||||
setAltchaStatus("required")
|
||||
}
|
||||
}, [])
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (formType === "create") {
|
||||
@ -110,7 +102,7 @@ export default function Signup() {
|
||||
return
|
||||
}
|
||||
|
||||
if (turnstileStatus !== "success") {
|
||||
if (altchaStatus !== "success") {
|
||||
setIsValid(false)
|
||||
setValidationMessage("Please verify you are not a robot")
|
||||
return
|
||||
@ -146,7 +138,7 @@ export default function Signup() {
|
||||
return
|
||||
}
|
||||
|
||||
if (turnstileStatus !== "success") {
|
||||
if (altchaStatus !== "success") {
|
||||
setIsValid(false)
|
||||
setValidationMessage("Please verify you are not a robot")
|
||||
return
|
||||
@ -155,7 +147,7 @@ export default function Signup() {
|
||||
setIsValid(true)
|
||||
setValidationMessage("Migrate Account")
|
||||
}
|
||||
}, [formData, formType, turnstileStatus])
|
||||
}, [formData, formType, altchaStatus])
|
||||
|
||||
const getButtonIcon = () => {
|
||||
if (isValid) return <CheckCircle2 size={30} />
|
||||
@ -173,7 +165,7 @@ export default function Signup() {
|
||||
setErrorAlert(null)
|
||||
|
||||
try {
|
||||
if (turnstileStatus !== "success") {
|
||||
if (altchaStatus !== "success") {
|
||||
setValidationMessage("Please verify you are not a robot")
|
||||
setIsSubmitting(false)
|
||||
return
|
||||
@ -181,10 +173,10 @@ export default function Signup() {
|
||||
|
||||
const email = `${formData.emailUsername}@${formData.emailDomain}`
|
||||
const formDataObj = new FormData(formRef.current as HTMLFormElement)
|
||||
const token = formDataObj.get("cf-turnstile-response") as string
|
||||
const token = formDataObj.get("altcha-token") as string
|
||||
|
||||
if (!token) {
|
||||
setErrorAlert("Cloudflare Turnstile token is missing. Please refresh")
|
||||
setErrorAlert("Altcha token is missing. Please refresh")
|
||||
setIsSubmitting(false)
|
||||
setForceRefresh(true)
|
||||
return
|
||||
@ -246,11 +238,11 @@ export default function Signup() {
|
||||
<AnimatePresence mode="wait">
|
||||
{formType === "initial" && (
|
||||
<motion.div key="initial" {...fadeInOut} className="space-y-4">
|
||||
<Button onClick={() => setFormType("create")} className="w-full h-16 text-lg">
|
||||
<Button onClick={() => setFormType("create")} className="w-full h-16 text-lg cursor-pointer">
|
||||
<UserPlus className="mr-2" />
|
||||
Create New Account
|
||||
</Button>
|
||||
<Button onClick={() => setFormType("migrate")} className="w-full h-16 text-lg">
|
||||
<Button onClick={() => setFormType("migrate")} className="w-full h-16 text-lg cursor-pointer">
|
||||
<UserCog className="mr-2" />
|
||||
Migrate p0ntus mail Account
|
||||
</Button>
|
||||
@ -319,10 +311,17 @@ export default function Signup() {
|
||||
</Label>
|
||||
</div>
|
||||
{!forceRefresh && (
|
||||
<Turnstile
|
||||
setTurnstileStatus={setTurnstileStatus}
|
||||
setValidationMessage={setValidationMessage}
|
||||
/>
|
||||
<input type="hidden" name="altcha-token" value={altchaToken ?? ""} />
|
||||
)}
|
||||
{!forceRefresh && (
|
||||
<>
|
||||
<div id="altcha-description" className="sr-only">
|
||||
A CAPTCHA box. You must solve the challenge to make an account.
|
||||
</div>
|
||||
<Altcha
|
||||
onStateChange={handleAltchaStateChange}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</motion.form>
|
||||
)}
|
||||
@ -389,10 +388,17 @@ export default function Signup() {
|
||||
</Label>
|
||||
</div>
|
||||
{!forceRefresh && (
|
||||
<Turnstile
|
||||
setTurnstileStatus={setTurnstileStatus}
|
||||
setValidationMessage={setValidationMessage}
|
||||
/>
|
||||
<input type="hidden" name="altcha-token" value={altchaToken ?? ""} />
|
||||
)}
|
||||
{!forceRefresh && (
|
||||
<>
|
||||
<div id="altcha-description" className="sr-only">
|
||||
A CAPTCHA box. You must solve the challenge to make an account.
|
||||
</div>
|
||||
<Altcha
|
||||
onStateChange={handleAltchaStateChange}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</motion.form>
|
||||
)}
|
||||
@ -405,7 +411,7 @@ export default function Signup() {
|
||||
<motion.div key="buttons" {...fadeInOut} className="w-full space-y-2">
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full mb-4"
|
||||
className="w-full mb-4 cursor-pointer"
|
||||
disabled={!isValid || isSubmitting}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
@ -416,7 +422,7 @@ export default function Signup() {
|
||||
)}
|
||||
{isSubmitting ? "Submitting..." : validationMessage}
|
||||
</Button>
|
||||
<Button variant="outline" className="w-full" onClick={() => setFormType("initial")}>
|
||||
<Button variant="outline" className="w-full cursor-pointer" onClick={() => setFormType("initial")}>
|
||||
Back
|
||||
</Button>
|
||||
</motion.div>
|
||||
@ -428,7 +434,7 @@ export default function Signup() {
|
||||
)
|
||||
) : (
|
||||
<motion.div key="buttons" {...fadeInOut} className="w-full space-y-2">
|
||||
<Button className="w-full" onClick={() => { setFormType("initial"); setForceRefresh(false); setErrorAlert(null) }}>
|
||||
<Button className="w-full cursor-pointer" onClick={() => { setFormType("initial"); setForceRefresh(false); setErrorAlert(null) }}>
|
||||
<ArrowLeft size={30} />
|
||||
Back
|
||||
</Button>
|
||||
|
22
app/api/captcha/create/route.ts
Normal file
22
app/api/captcha/create/route.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { createChallenge } from 'altcha-lib'
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
const hmacKey = process.env.ALTCHA_SECRETKEY
|
||||
|
||||
async function getChallenge() {
|
||||
if (!hmacKey) {
|
||||
console.error("ALTCHA_SECRETKEY is not set")
|
||||
return NextResponse.json({ error: "ALTCHA_SECRETKEY is not set" }, { status: 500 })
|
||||
}
|
||||
|
||||
const challenge = await createChallenge({
|
||||
hmacKey,
|
||||
maxNumber: 1200000, // Max random number
|
||||
})
|
||||
|
||||
return NextResponse.json(challenge)
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
return getChallenge()
|
||||
}
|
@ -61,7 +61,7 @@ export async function POST(request: Request) {
|
||||
|
||||
const tokenValidation = await validateToken(token)
|
||||
if (!tokenValidation.success) {
|
||||
console.error("Turnstile validation failed:", tokenValidation.error)
|
||||
console.error("Altcha validation failed:", tokenValidation.error)
|
||||
return NextResponse.json({ success: false, message: "Robot check failed, try refreshing" }, { status: 400 })
|
||||
}
|
||||
|
||||
|
57
components/custom/Altcha.tsx
Normal file
57
components/custom/Altcha.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useRef, useState, forwardRef, useImperativeHandle } from 'react'
|
||||
|
||||
interface AltchaProps {
|
||||
onStateChange?: (ev: Event | CustomEvent) => void
|
||||
}
|
||||
|
||||
const Altcha = forwardRef<{ value: string | null }, AltchaProps>(({ onStateChange }, ref) => {
|
||||
const widgetRef = useRef<AltchaWidget & AltchaWidgetMethods & HTMLElement>(null)
|
||||
const [value, setValue] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
import('altcha')
|
||||
}, [])
|
||||
|
||||
useImperativeHandle(ref, () => {
|
||||
return {
|
||||
get value() {
|
||||
return value
|
||||
}
|
||||
}
|
||||
}, [value])
|
||||
|
||||
useEffect(() => {
|
||||
const handleStateChange = (ev: Event | CustomEvent) => {
|
||||
if ('detail' in ev) {
|
||||
setValue(ev.detail.payload || null)
|
||||
onStateChange?.(ev)
|
||||
}
|
||||
}
|
||||
|
||||
const { current } = widgetRef
|
||||
|
||||
if (current) {
|
||||
current.addEventListener('statechange', handleStateChange)
|
||||
return () => current.removeEventListener('statechange', handleStateChange)
|
||||
}
|
||||
}, [onStateChange])
|
||||
|
||||
return (
|
||||
<altcha-widget
|
||||
challengeurl="/api/captcha/create"
|
||||
ref={widgetRef}
|
||||
style={{
|
||||
'--altcha-max-width': '100%',
|
||||
}}
|
||||
debug={process.env.NODE_ENV === "development"}
|
||||
aria-label="Security verification"
|
||||
aria-describedby="altcha-description"
|
||||
></altcha-widget>
|
||||
)
|
||||
})
|
||||
|
||||
Altcha.displayName = 'Altcha'
|
||||
|
||||
export default Altcha
|
@ -1,36 +0,0 @@
|
||||
import type React from "react"
|
||||
import { Turnstile as CloudflareTS } from "next-turnstile"
|
||||
|
||||
interface TurnstileProps {
|
||||
setTurnstileStatus: React.Dispatch<React.SetStateAction<"error" | "expired" | "required" | "success">>
|
||||
setValidationMessage: (message: string) => void
|
||||
}
|
||||
|
||||
export default function Turnstile({ setTurnstileStatus, setValidationMessage }: TurnstileProps) {
|
||||
return (
|
||||
<CloudflareTS
|
||||
siteKey={process.env.NEXT_PUBLIC_CF_SITEKEY!}
|
||||
retry="auto"
|
||||
refreshExpired="auto"
|
||||
onError={() => {
|
||||
setTurnstileStatus("error")
|
||||
setValidationMessage("Security check failed. Please try again.")
|
||||
console.error("[!] Turnstile error occurred")
|
||||
}}
|
||||
onExpire={() => {
|
||||
setTurnstileStatus("expired")
|
||||
setValidationMessage("Security check expired. Please verify again.")
|
||||
console.warn("[!] Turnstile token expired")
|
||||
}}
|
||||
onLoad={() => {
|
||||
setTurnstileStatus("required")
|
||||
}}
|
||||
onVerify={() => {
|
||||
setTurnstileStatus("success")
|
||||
console.log("[S] Turnstile verification successful")
|
||||
}}
|
||||
className="flex justify-center"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -35,18 +35,18 @@ export default function EmailField({ formData, handleInputChange, handleSelectCh
|
||||
value={formData.emailDomain}
|
||||
onValueChange={handleSelectChange}
|
||||
>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectTrigger className="w-[180px] cursor-pointer">
|
||||
<SelectValue placeholder="Select domain" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="librecloud.cc">librecloud.cc</SelectItem>
|
||||
<SelectItem value="pontusmail.org">pontusmail.org</SelectItem>
|
||||
<SelectItem value="p0ntus.com">p0ntus.com</SelectItem>
|
||||
<SelectItem value="ihate.college">ihate.college</SelectItem>
|
||||
<SelectItem value="pontus.pics">pontus.pics</SelectItem>
|
||||
<SelectItem value="dontbeevil.lol">dontbeevil.lol</SelectItem>
|
||||
<SelectItem value="dont-be-evil.lol">dont-be-evil.lol</SelectItem>
|
||||
<SelectItem value="strongintegrity.life">strongintegrity.life</SelectItem>
|
||||
<SelectItem value="librecloud.cc" className="cursor-pointer">librecloud.cc</SelectItem>
|
||||
<SelectItem value="pontusmail.org" className="cursor-pointer">pontusmail.org</SelectItem>
|
||||
<SelectItem value="p0ntus.com" className="cursor-pointer">p0ntus.com</SelectItem>
|
||||
<SelectItem value="ihate.college" className="cursor-pointer">ihate.college</SelectItem>
|
||||
<SelectItem value="pontus.pics" className="cursor-pointer">pontus.pics</SelectItem>
|
||||
<SelectItem value="dontbeevil.lol" className="cursor-pointer">dontbeevil.lol</SelectItem>
|
||||
<SelectItem value="dont-be-evil.lol" className="cursor-pointer">dont-be-evil.lol</SelectItem>
|
||||
<SelectItem value="strongintegrity.life" className="cursor-pointer">strongintegrity.life</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
@ -4,15 +4,16 @@
|
||||
|
||||
* Get Started
|
||||
|
||||
* [Start with Docker](getstarted/docker.md)
|
||||
* [Dev Server Setup](getstarted/dev.md)
|
||||
* [Start with Docker](getstarted/docker.md)
|
||||
* [Dev Server Setup](getstarted/dev.md)
|
||||
|
||||
* Reference
|
||||
|
||||
* [Environment Variables](reference/env.md)
|
||||
* [Database Migration Guide](reference/db-migration.md)
|
||||
* [Editing Documentation](reference/editing-docs.md)
|
||||
* [Environment Variables](reference/env.md)
|
||||
* [Database Migration Guide](reference/db-migration.md)
|
||||
* [Altcha Implementation](reference/altcha.md)
|
||||
* [Editing Documentation](reference/editing-docs.md)
|
||||
|
||||
* Updates
|
||||
|
||||
* [v1.2.0](updates/1.2.0.md)
|
||||
* [v1.2.0](updates/1.2.0.md)
|
@ -29,11 +29,20 @@
|
||||
bunx auth secret
|
||||
```
|
||||
|
||||
5. **Configure environment variables**
|
||||
5. **Generate Altcha token**
|
||||
|
||||
If you plan to use the signup forms, you will need to use Altcha, a private proof-of-work CAPTCHA. All you need to do is execute the script below, and it will be written to your `.env` or `.env.local`.
|
||||
|
||||
```bash
|
||||
$ bun tools/hmac.ts
|
||||
Successfully wrote ALTCHA_SECRETKEY to .env.local
|
||||
```
|
||||
|
||||
6. **Configure environment variables**
|
||||
|
||||
Following the environment variables section of this README, update your newly created `.env.local` file with your configuration.
|
||||
|
||||
6. **Initialize Prisma**
|
||||
7. **Initialize Prisma**
|
||||
|
||||
Because `web` uses a database for storing Git link statuses (and other things to come), you will need to initialize the SQLite database.
|
||||
|
||||
@ -45,7 +54,7 @@
|
||||
bunx prisma migrate dev --name init
|
||||
```
|
||||
|
||||
7. **Start dev server**
|
||||
8. **Start dev server**
|
||||
|
||||
```bash
|
||||
bun dev
|
||||
|
@ -36,11 +36,20 @@ A Docker setup requires both Docker *and* Docker Compose.
|
||||
bunx auth secret
|
||||
```
|
||||
|
||||
4. **Configure environment variables**
|
||||
4. **Generate Altcha token**
|
||||
|
||||
If you plan to use the signup forms, you will need to use Altcha, a private proof-of-work CAPTCHA. All you need to do is execute the script below, and it will be written to your `.env` or `.env.local`.
|
||||
|
||||
```bash
|
||||
$ bun tools/hmac.ts
|
||||
Successfully wrote ALTCHA_SECRETKEY to .env.local
|
||||
```
|
||||
|
||||
5. **Configure environment variables**
|
||||
|
||||
Following the environment variables section of this README, update your newly created `.env.local` file with your configuration.
|
||||
|
||||
5. **Initialize Prisma**
|
||||
6. **Initialize Prisma**
|
||||
|
||||
Because `web` uses a database for storing Git link statuses (and other things to come),
|
||||
you will need to initialize the SQLite database.
|
||||
@ -52,11 +61,11 @@ A Docker setup requires both Docker *and* Docker Compose.
|
||||
bunx prisma migrate dev --name init
|
||||
```
|
||||
|
||||
6. **Setup environment variables**
|
||||
7. **Setup environment variables**
|
||||
|
||||
Now is the time to go to the "Environment Variables" section and configure them in your `.env.local` file.
|
||||
|
||||
7. **Bring the container up**
|
||||
8. **Bring the container up**
|
||||
|
||||
```bash
|
||||
docker compose up -d --build
|
||||
@ -66,7 +75,7 @@ A Docker setup requires both Docker *and* Docker Compose.
|
||||
|
||||
You may customize the container with the included `docker-compose.yml` file if needed. Your server will start on port `3019` by default. We suggest using a reverse proxy to serve the site on a domain.
|
||||
|
||||
8. **Complete Setup**
|
||||
9. **Complete Setup**
|
||||
|
||||
If you would like to host the entire LibreCloud frontend and backend,
|
||||
you will also need to set up the following repositories and edit this project to work with *your* setup.
|
||||
|
47
docs/reference/altcha.md
Normal file
47
docs/reference/altcha.md
Normal file
@ -0,0 +1,47 @@
|
||||
# Altcha Implementation
|
||||
|
||||
Altcha is a privacy-friendly CAPTCHA alternative that's used in the LibreCloud signup form to verify that users are not bots. This doc explains the implementation, setup, and configuration of Altcha with LibreCloud.
|
||||
|
||||
## Basic Overview
|
||||
|
||||
Altcha works by serving a proof-of-work-based CAPTCHA which doesn't require any fingerprinting/data collection. Our implementation does not use their API, and instead uses an endpoint, `/api/captcha/create`. The setup of this is nearly automatically.
|
||||
|
||||
## Components
|
||||
|
||||
1. **Widget** (`components/custom/Altcha.tsx`)
|
||||
|
||||
This component renders the Altcha widget for use in pages.
|
||||
|
||||
2. **API Endpoint** (`app/api/captcha/create/route.ts`)
|
||||
|
||||
This endpoint generates the challenges with the altcha library.
|
||||
|
||||
3. **Token Validation** (`lib/utils.ts`)
|
||||
|
||||
The utils file provides verification for submitted tokens from the client. This is what gets implemented into an API route which needs to be protected by Altcha. You can see an example of this in `app/api/users/create/route.ts`, which connects to `app/account/signup/page.tsx`.
|
||||
|
||||
## Setup
|
||||
|
||||
### Environment Variables
|
||||
|
||||
To use Altcha, you need to set the following environment variable:
|
||||
|
||||
```text
|
||||
ALTCHA_SECRETKEY=
|
||||
```
|
||||
|
||||
#### Automatic Setup
|
||||
|
||||
You should generate this with the provided `tools/hmac.ts` script. This is an important part of setup for people who want to take advantage of the signup forms.
|
||||
|
||||
The script requires the existance of an `.env` or `.env.local` file. You should have had this created when you setup Auth.js with `bunx auth secret`.
|
||||
|
||||
Execute the script like so:
|
||||
|
||||
```bash
|
||||
bun tools/hmac.ts
|
||||
```
|
||||
|
||||
## Debug Mode
|
||||
|
||||
The Altcha widget is in debug mode when `NODE_ENV` is set to `"development"`. This is nice for testing but you should disable it in production.
|
@ -17,6 +17,23 @@ With these variables, you can disable entire parts of the dashboard, such as reg
|
||||
| NEXT_PUBLIC_TELEGRAM_CHANNEL_URL | Changes the default Telegram channel link in Support dash | String - `https://t.me/...` |
|
||||
| NEXT_PUBLIC_TELEGRAM_GROUP_URL | Changes the default Telegram group link in Support dash | String - `https://t.me/...` |
|
||||
|
||||
## Altcha
|
||||
|
||||
Altcha is a privacy-friendly CAPTCHA alternative we use for bot traffic mitigation and anti-spam.
|
||||
It requires a secret key for generating and verifying challenges. The rest is handled by the API.
|
||||
|
||||
| Environment Variable | Description | Example |
|
||||
|----------------------|-----------------------------------------------------------|----------------------------------------|
|
||||
| ALTCHA_SECRETKEY | Secret key for generating and verifying Altcha challenges | N/A |
|
||||
|
||||
### Automatic Secret Key Setup
|
||||
|
||||
We've included an automatic setup script for your Altcha secret key. It generates and writes a secure token to your `.env` or `.env.local` file. You can execute this script like so:
|
||||
|
||||
```bash
|
||||
bun tools/hmac.ts
|
||||
```
|
||||
|
||||
## Authentik
|
||||
|
||||
We use [Auth.js](https://authjs.dev) to provide authentication for users through Authentik.
|
||||
|
22
lib/utils.ts
22
lib/utils.ts
@ -1,6 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import { validateTurnstileToken } from "next-turnstile"
|
||||
import { verifySolution } from "altcha-lib"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
@ -9,29 +9,25 @@ export function cn(...inputs: ClassValue[]) {
|
||||
export async function validateToken(token: string) {
|
||||
try {
|
||||
if (!token) {
|
||||
console.error("Validation failed: No token provided")
|
||||
console.error("Altcha error: No token provided")
|
||||
return { success: false, error: "No token provided" }
|
||||
}
|
||||
|
||||
if (!process.env.CF_SECRETKEY) {
|
||||
console.error("Validation failed: Missing CF_SECRETKEY environment variable")
|
||||
if (!process.env.ALTCHA_SECRETKEY) {
|
||||
console.error("Altcha error: Missing ALTCHA_SECRETKEY environment variable")
|
||||
return { success: false, error: "Server configuration error" }
|
||||
}
|
||||
|
||||
const result = await validateTurnstileToken({
|
||||
token,
|
||||
secretKey: process.env.CF_SECRETKEY,
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
const ok = await verifySolution(token, process.env.ALTCHA_SECRETKEY)
|
||||
if (ok) {
|
||||
return { success: true }
|
||||
} else {
|
||||
console.error("Validation failed:", result)
|
||||
console.error("Altcha error: Invalid token")
|
||||
return { success: false, error: "Invalid token" }
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Turnstile validation error:", error)
|
||||
return { success: false, error: "Validation service error" }
|
||||
console.error("Altcha error:", error)
|
||||
return { success: false, error: "An error occurred with Altcha" }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -25,6 +25,8 @@
|
||||
"@radix-ui/react-tabs": "^1.1.4",
|
||||
"@radix-ui/react-tooltip": "^1.2.0",
|
||||
"@web3icons/react": "^4.0.13",
|
||||
"altcha": "^1.4.2",
|
||||
"altcha-lib": "^1.2.0",
|
||||
"axios": "^1.8.4",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
@ -38,7 +40,6 @@
|
||||
"next": "^15.3.0",
|
||||
"next-auth": "^5.0.0-beta.25",
|
||||
"next-themes": "^0.4.6",
|
||||
"next-turnstile": "^1.0.2",
|
||||
"password-validator": "^5.3.0",
|
||||
"prisma": "^6.6.0",
|
||||
"react": "^19.1.0",
|
||||
|
22
tools/hmac.ts
Normal file
22
tools/hmac.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import crypto from 'crypto'
|
||||
import fs from 'fs'
|
||||
|
||||
const hmacKey = crypto.randomBytes(32).toString('hex')
|
||||
|
||||
if (fs.existsSync('.env.local')) {
|
||||
const envFile = fs.readFileSync('.env.local', 'utf8')
|
||||
// Double-check it's not already set
|
||||
if (!envFile.includes('ALTCHA_SECRETKEY')) {
|
||||
fs.appendFileSync('.env.local', `\nALTCHA_SECRETKEY=${hmacKey}`)
|
||||
}
|
||||
console.log(`Successfully wrote ALTCHA_SECRETKEY to .env.local`)
|
||||
} else if (fs.existsSync('.env')) {
|
||||
const envFile = fs.readFileSync('.env', 'utf8')
|
||||
// Double-check it's not already set
|
||||
if (!envFile.includes('ALTCHA_SECRETKEY')) {
|
||||
fs.appendFileSync('.env', `\nALTCHA_SECRETKEY=${hmacKey}`)
|
||||
}
|
||||
console.log(`Successfully wrote ALTCHA_SECRETKEY to .env`)
|
||||
} else {
|
||||
console.error('No .env/.env.local file found, please create one first.')
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user