ui: Done join page

This commit is contained in:
nemunaire 2022-11-26 22:48:49 +01:00
parent 275a71982b
commit c5d12b9e9f
7 changed files with 265 additions and 3 deletions

11
ui/src/lib/api/user.ts Normal file
View File

@ -0,0 +1,11 @@
import { handleApiResponse } from '$lib/errors';
import type { SignUpForm } from '$lib/model/user';
export async function registerUser(form: SignUpForm): Promise<any> {
const res = await fetch('api/users', {
method: 'POST',
headers: {'Accept': 'application/json'},
body: JSON.stringify(form),
});
return await handleApiResponse(res);
}

View File

@ -0,0 +1,152 @@
<script lang="ts">
import { goto } from '$app/navigation';
import {
Button,
FormGroup,
Icon,
Input,
Label,
Spinner,
} from 'sveltestrap';
import { t, locale } from '$lib/translations';
import { registerUser } from '$lib/api/user';
import type { SignUpForm } from '$lib/model/user';
import { checkWeakPassword, checkPasswordConfirmation } from '$lib/password';
import { toasts } from '$lib/stores/toasts';
let signupForm: SignUpForm = {
email: "",
password: "",
wantReceiveUpdate: false,
lang: "",
};
let passwordConfirmation: string = "";
let emailState: boolean|undefined;
let passwordState: boolean|undefined;
let passwordConfirmState: boolean|undefined;
let formSent = false;
$: {
if (passwordState == false) {
passwordState = checkWeakPassword(signupForm.password);
}
}
let formElm: HTMLFormElement;
function goSignUp() {
const valid = formElm.checkValidity()
if (valid && emailState && passwordState && passwordConfirmState) {
formSent = true;
signupForm.lang = $locale
registerUser(signupForm)
.then(
() => {
formSent = false;
toasts.addToast({
title: $t('account.signup.success'),
message: $t('email.instruction.check-inbox'),
type: 'success',
timeout: 5000,
});
goto('/login');
},
(error) => {
formSent = false;
toasts.addErrorToast({
title: $t('errors.registration'),
message: error,
timeout: 10000,
})
}
)
}
}
</script>
<form
class="container my-1"
bind:this={formElm}
on:submit|preventDefault={goSignUp}
>
<FormGroup>
<Label for="email-input">{$t('email.address')}</Label>
<Input
aria-describedby="emailHelpBlock"
autocomplete="username"
autofocus
feedback={!emailState?$t('errors.address-valid'):null}
id="email-input"
placeholder="jPostel@isi.edu"
required
type="email"
invalid={emailState !== undefined && !emailState}
valid={emailState}
bind:value={signupForm.email}
on:change={() => emailState = signupForm.email.indexOf('@') > 0}
/>
<div id="emailHelpBlock" class="form-text">
{$t('account.signup.address-why', {
identify: $t('account.signup.identify'),
'security-operations': $t('account.signup.security-operations'),
})}
</div>
</FormGroup>
<FormGroup>
<Label for="password-input">{$t('common.password')}</Label>
<Input
autocomplete="new-password"
feedback={!passwordState?$t('errors.password-weak'):null}
id="password-input"
placeholder="xXxXxXxXxX"
required
type="password"
invalid={passwordState !== undefined && !passwordState}
valid={passwordState}
bind:value={signupForm.password}
on:change={() => passwordState = checkWeakPassword(signupForm.password)}
/>
</FormGroup>
<FormGroup>
<Label for="passwordconfirm-input">{$t('password.confirmation')}</Label>
<Input
feedback={!passwordConfirmState?$t('errors.password-match'):null}
id="passwordconfirm-input"
placeholder="xXxXxXxXxX"
required
type="password"
invalid={passwordConfirmState !== undefined && !passwordConfirmState}
valid={passwordConfirmState}
bind:value={passwordConfirmation}
on:change={() => passwordConfirmState = checkPasswordConfirmation(signupForm.password, passwordConfirmation)}
/>
</FormGroup>
<FormGroup>
<Input
id="signup-newsletter"
type="checkbox"
label={$t('account.signup.receive-update')}
bind:value={signupForm.wantReceiveUpdate}
/>
</FormGroup>
<div class="d-flex justify-content-around">
<Button type="submit" color="primary" disabled={formSent}>
{#if formSent}
<Spinner
label="Spinning"
size="sm"
/>
{:else}
<Icon name="person-plus" />
{/if}
{$t('account.signup.signup')}
</Button>
<Button href="/login" outline color="dark">
{$t('account.signup.already')}
</Button>
</div>
</form>

13
ui/src/lib/errors.ts Normal file
View File

@ -0,0 +1,13 @@
export async function handleApiResponse(res: Response): Promise<any> {
const data = await res.json();
if (res.status == 200) {
return data;
} else if (res.status == 401) {
throw new Error(data.errmsg);
} else if (data.errmsg) {
throw new Error(data.errmsg);
} else {
throw new Error("A " + res.status + " error occurs.");
}
}

View File

@ -16,7 +16,7 @@
"signup": {
"already": "Already a member?",
"join-call": "Join our nice platform in less than 2 minutes!",
"address-why": "You'll use your address to {identify} yourself on the platform, and it could be used to contact you for {security-operations}.",
"address-why": "We'll use your address to {identify} yourself on the platform, and it could be used to contact you for {security-operations}.",
"identify": "identify",
"security-operations": "security related operations",
"receive-update": "Keep me informed of future big improvements",

View File

@ -1,6 +1,6 @@
import type UserSettings from './usersettings';
interface User {
export interface User {
id: string;
email: string;
CreatedAt: Date;
@ -8,4 +8,9 @@ interface User {
settings: UserSettings;
}
export default User;
export interface SignUpForm {
email: string;
password: string;
wantReceiveUpdate: boolean;
lang: string;
}

45
ui/src/lib/password.ts Normal file
View File

@ -0,0 +1,45 @@
// Copyright or © or Copr. happyDNS (2022)
//
// contact@happydomain.org
//
// This software is a computer program whose purpose is to provide a modern
// interface to interact with DNS systems.
//
// This software is governed by the CeCILL license under French law and abiding
// by the rules of distribution of free software. You can use, modify and/or
// redistribute the software under the terms of the CeCILL license as
// circulated by CEA, CNRS and INRIA at the following URL
// "http://www.cecill.info".
//
// As a counterpart to the access to the source code and rights to copy, modify
// and redistribute granted by the license, users are provided only with a
// limited warranty and the software's author, the holder of the economic
// rights, and the successive licensors have only limited liability.
//
// In this respect, the user's attention is drawn to the risks associated with
// loading, using, modifying and/or developing or reproducing the software by
// the user in light of its specific status of free software, that may mean
// that it is complicated to manipulate, and that also therefore means that it
// is reserved for developers and experienced professionals having in-depth
// computer knowledge. Users are therefore encouraged to load and test the
// software's suitability as regards their requirements in conditions enabling
// the security of their systems and/or data to be ensured and, more generally,
// to use and operate it in the same conditions as regards security.
//
// The fact that you are presently reading this means that you have had
// knowledge of the CeCILL license and that you accept its terms.
export function checkWeakPassword (password: string): boolean|undefined {
if (password.length === 0) {
return undefined;
}
return password.length >= 8 && /[A-Z]/.test(password) && /[a-z]/.test(password) && /[0-9]/.test(password) && (/\W/.test(password) || password.length >= 11);
}
export function checkPasswordConfirmation (password: string, passwordConfirm: string): boolean|undefined {
if (passwordConfirm.length === 0) {
return undefined;
}
return password === passwordConfirm;
}

View File

@ -0,0 +1,36 @@
<script lang="ts">
import {
Card,
CardBody,
CardHeader,
Container,
Col,
Row,
} from 'sveltestrap';
import { t } from '$lib/translations';
import SignUpForm from '$lib/components/SignUpForm.svelte';
</script>
<Container class="my-3">
<Row>
<Col sm="4" class="d-none d-sm-flex flex-column align-items-center">
<img src="img/screenshot.png" alt="HappyDNS screenshoot" style="max-height: 100%; max-width: 100%;" class="mt-auto">
<div class="mt-3 mb-auto text-justify">
Join now our open source and free (as freedom) DNS platform, to manage your domains easily!
</div>
</Col>
<Col sm="8">
<Card>
<CardHeader>
<h6 class="card-title my-1 fw-bold">
{$t('account.signup.join-call')}
</h6>
</CardHeader>
<CardBody>
<SignUpForm />
</CardBody>
</Card>
</Col>
</Row>
</Container>