Create a registration form #135

Merged
bustikiller merged 4 commits from registration-form into main 2024-12-07 18:13:04 +00:00
6 changed files with 178 additions and 11 deletions

View File

@ -1,11 +1,8 @@
/* Copyright (C) 2024 Manuel Bustillo*/
'use client';
import Link from 'next/link';
import styles from '@/app/ui/home.module.css';
import LoginForm from '@/app/ui/components/login-form';
import RegistrationForm from '@/app/ui/components/registration-form';
export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
@ -14,17 +11,15 @@ export default async function Page({ params }: { params: Promise<{ slug: string
<main className="flex min-h-screen flex-col p-6">
<div className="flex flex-row">
<div className="w-1/2">
Already have an account? Sign in
<LoginForm />
</div>
<div className="w-1/2">
Don't have an account? Register now!
<RegistrationForm />
</div>
</div>
</main>
);
}

View File

@ -1,10 +1,9 @@
/* Copyright (C) 2024 Manuel Bustillo*/
import { getCsrfToken, getSlug } from '@/app/lib/utils';
import { User } from '@/app/lib/definitions';
import { asArray, getCsrfToken, getSlug } from '@/app/lib/utils';
import { Captcha, StructuredErrors, User } from '@/app/lib/definitions';
export function login({ email, password, onLogin }: { email: string, password: string, onLogin: (user: User) => void }) {
console.log(email, password);
return fetch(`/api/${getSlug()}/users/sign_in`, {
method: 'POST',
body: JSON.stringify({ user: { email, password } }),
@ -34,3 +33,45 @@ export function logout({ onLogout }: { onLogout: () => void }) {
}).then(onLogout)
.catch((error) => console.error(error));
}
function flattenErrors(errors: StructuredErrors): string[] {
if(errors instanceof Array) {
return errors;
}
return Object.keys(errors).map((key) => {
return `${key}: ${asArray(errors[key]).join(', ')}`;
});
}
export function register({slug, email, password, passwordConfirmation, captcha, onRegister, onError}: {
slug: string,
email: string,
password: string,
passwordConfirmation: string,
captcha: Captcha,
onRegister: () => void,
onError: (errors: string[]) => void
}){
fetch(`/api/${slug}/users`, {
method: 'POST',
body: JSON.stringify(
{
user: { email, password, password_confirmation: passwordConfirmation },
captcha: { id: captcha.id, answer: captcha.answer }
}
),
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-CSRF-TOKEN': getCsrfToken(),
}
}).then((response) => {
if(response.ok) {
response.json().then(onRegister);
} else {
response.json().then((data: any) => {
onError(data.errors && flattenErrors(data.errors) || [data.error]);
});
}
})
}

19
app/api/captcha.tsx Normal file
View File

@ -0,0 +1,19 @@
/* Copyright (C) 2024 Manuel Bustillo*/
import { data } from "autoprefixer";
import { getCsrfToken } from "../lib/utils";
export function getCaptchaChallenge({onRetrieve}: {onRetrieve: (id: string, url: string) => void}){
return fetch('/api/captcha', {
method: 'POST',
headers: {
'Accept': 'application/json',
'X-CSRF-TOKEN': getCsrfToken(),
}
})
.then((response) => response.json())
.then((data: any) => {
onRetrieve(data.id, data.media_url)
})
.catch((error) => console.error(error));
}

View File

@ -64,4 +64,13 @@ export type guestsTable = {
export type User = {
id: string;
email: string;
}
}
export type Captcha = {
id: string;
answer: string;
}
export type StructuredErrors = {
[key: string]: string[]|string;
};

View File

@ -13,3 +13,8 @@ export const getSlug = () => localStorage.getItem('slug') || 'default';
export const capitalize = (val:string) => {
return String(val).charAt(0).toUpperCase() + String(val).slice(1);
}
// From https://stackoverflow.com/a/62118163/3607039
export function asArray<T>(value: T | T[]): T[] {
return ([] as T[]).concat(value)
}

View File

@ -0,0 +1,98 @@
/* Copyright (C) 2024 Manuel Bustillo*/
'use client';
import { FloatLabel } from 'primereact/floatlabel';
import { InputText } from 'primereact/inputtext';
import { useState, useEffect } from 'react';
import { classNames } from './button';
import { getSlug } from '@/app/lib/utils';
import { register } from '@/app/api/authentication';
import { getCaptchaChallenge } from '@/app/api/captcha';
export default function RegistrationForm() {
const [submitted, setSubmitted] = useState<boolean>(false);
const [errors, setErrors] = useState<string[]>([]);
const [email, setEmail] = useState<string>("");
const [password, setPassword] = useState<string>("");
const [passwordConfirmation, setPasswordConfirmation] = useState<string>("");
const [slug, setSlug] = useState<string>(getSlug());
const [captchaId, setCaptchaId] = useState<string>("");
const [captchaUrl, setCaptchaUrl] = useState<string>("");
const [captchaAnswer, setCaptchaAnswer] = useState<string>("");
const refreshCaptcha = () => {
getCaptchaChallenge({
onRetrieve: (id, url) => {
console.log(id, url);
setCaptchaId(id);
setCaptchaUrl(url);
setCaptchaAnswer("");
}
});
}
useEffect(refreshCaptcha, [])
return (
submitted ? (
<div className="card flex justify-evenly py-5 flex-col">
<div className="text-green-500">Registration successful. Check your email for a confirmation link.</div>
</div>
) : (
<div className="card flex justify-evenly py-5 flex-col">
<FloatLabel className="my-4">
<InputText id="email" type="email" className='rounded-sm' onChange={(e) => setEmail(e.target.value)} />
<label htmlFor="email">Email</label>
</FloatLabel>
<FloatLabel className="my-4">
<InputText id="password" type="password" className='rounded-sm' onChange={(e) => setPassword(e.target.value)} />
<label htmlFor="password">Password</label>
</FloatLabel>
<FloatLabel className="my-4">
<InputText id="passwordConfirmation" type="password" className='rounded-sm' onChange={(e) => setPasswordConfirmation(e.target.value)} />
<label htmlFor="passwordConfirmation">Confirm Password</label>
</FloatLabel>
<FloatLabel className="my-4">
<InputText id="slug" type="text" className='rounded-sm' onChange={(e) => setSlug(e.target.value)} />
<label htmlFor="slug">Slug</label>
</FloatLabel>
<img className="w-96" src={captchaUrl} alt="captcha" />
<FloatLabel className="my-4">
<InputText id="captcha" type="text" className='rounded-sm' value={captchaAnswer} onChange={(e) => setCaptchaAnswer(e.target.value)} />
<label htmlFor="captcha">Captcha</label>
</FloatLabel>
{errors.map((error, index) => (
<div key={index} className="text-red-500">{error}</div>
))}
<button
className={classNames('primary')}
disabled={!(email && password && passwordConfirmation && slug && captchaAnswer)}
onClick={() => register(
{
slug: slug,
email: email,
password: password,
passwordConfirmation: passwordConfirmation,
captcha: {
id: captchaId,
answer: captchaAnswer
},
onRegister: () => { setErrors([]); setSubmitted(true) },
onError: (errors) => { refreshCaptcha(); setErrors(errors) }
}
)}
>
Register
</button>
</div>
)
);
}