parent
970045446b
commit
9014a0e154
@ -1,2 +1,2 @@
|
|||||||
VITE_API_ENDPOINT=/api
|
VITE_API_ENDPOINT=http://localhost:5254
|
||||||
VITE_BASE=
|
VITE_BASE=
|
@ -1,16 +1,57 @@
|
|||||||
import { API } from "./Constants"
|
import { API, BASE } from "./Constants"
|
||||||
|
import { Session } from "./api/session.ts"
|
||||||
|
|
||||||
|
|
||||||
|
export function redirect(url: string) {
|
||||||
|
location.pathname = BASE + url
|
||||||
|
}
|
||||||
|
|
||||||
export function fetchAPI(
|
export function fetchAPI(
|
||||||
url: string,
|
url: string,
|
||||||
payload: unknown,
|
payload: unknown,
|
||||||
method = "POST",
|
method = "POST",
|
||||||
|
session?: Session,
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
|
|
||||||
|
const token = session?.auth?.token
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
"Accept": "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
headers.Authorization = token
|
||||||
|
}
|
||||||
|
|
||||||
return fetch(`${API}/${url}`, {
|
return fetch(`${API}/${url}`, {
|
||||||
method,
|
method,
|
||||||
headers: {
|
headers,
|
||||||
Accept: "application/json",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function fetchAPIGet(
|
||||||
|
url: string,
|
||||||
|
session?: Session,
|
||||||
|
): Promise<Response> {
|
||||||
|
|
||||||
|
const token = session?.auth?.token
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
"Accept": "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
headers.Authorization = token
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch(`${API}/${url}`, {
|
||||||
|
method: "GET",
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
|
||||||
|
export interface Failure {
|
||||||
|
type: string
|
||||||
|
messages: string[]
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
import { createContext } from "react"
|
||||||
|
|
||||||
|
export interface Session {
|
||||||
|
auth?: Authentication
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Authentication {
|
||||||
|
token: string
|
||||||
|
expirationDate: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
const SESSION_KEY = "session"
|
||||||
|
|
||||||
|
// export const SessionContext = createContext<Session>(getSession())
|
||||||
|
|
||||||
|
export function saveSession(session: Session) {
|
||||||
|
localStorage.setItem(SESSION_KEY, JSON.stringify(session))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSession(): Session {
|
||||||
|
const json = localStorage.getItem(SESSION_KEY)
|
||||||
|
return json ? JSON.parse(json) : {}
|
||||||
|
}
|
@ -0,0 +1,63 @@
|
|||||||
|
import { FormEvent, useRef, useState } from "react"
|
||||||
|
import { BASE } from "../Constants.ts"
|
||||||
|
import { fetchAPI, redirect } from "../Fetcher.ts"
|
||||||
|
import { Failure } from "../api/failure.ts"
|
||||||
|
import { saveSession } from "../api/session.ts"
|
||||||
|
import "../style/form.css"
|
||||||
|
|
||||||
|
export default function LoginApp() {
|
||||||
|
|
||||||
|
|
||||||
|
const [errors, setErrors] = useState<Failure[]>([])
|
||||||
|
|
||||||
|
const emailRef = useRef<HTMLInputElement>(null)
|
||||||
|
const passwordRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
|
async function handleSubmit(e: FormEvent) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
const email = emailRef.current!.value
|
||||||
|
const password = passwordRef.current!.value
|
||||||
|
|
||||||
|
const response = await fetchAPI("auth/token", {email, password})
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const { token, expirationDate } = await response.json()
|
||||||
|
saveSession({ auth: { token, expirationDate } })
|
||||||
|
redirect("/")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const failures = await response.json()
|
||||||
|
setErrors(Object.entries<string[]>(failures).map(([type, messages]) => ({ type, messages })))
|
||||||
|
} catch (e) {
|
||||||
|
setErrors([{ type: "internal error", messages: ["an internal error occurred."] }])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="container">
|
||||||
|
<center>
|
||||||
|
<h2>Se connecter</h2>
|
||||||
|
</center>
|
||||||
|
|
||||||
|
{errors.map(({ type, messages }) =>
|
||||||
|
messages.map(message => <p key={type} style={{ color: "red" }}>{type} : {message}</p>))}
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="email">Email :</label>
|
||||||
|
<input ref={emailRef} type="text" id="email" name="email" required />
|
||||||
|
|
||||||
|
<label htmlFor="password">Mot de passe :</label>
|
||||||
|
<input ref={passwordRef} type="password" id="password" name="password" required />
|
||||||
|
|
||||||
|
<a href={BASE + "/register"} className="inscr">Vous n'avez pas de compte ?</a>
|
||||||
|
<br /><br />
|
||||||
|
<div id="buttons">
|
||||||
|
<input className="button" type="submit" value="Se connecter" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
}
|
@ -0,0 +1,92 @@
|
|||||||
|
import { FormEvent, useRef, useState } from "react"
|
||||||
|
import { BASE } from "../Constants.ts"
|
||||||
|
|
||||||
|
import "../style/form.css"
|
||||||
|
import { Failure } from "../api/failure.ts"
|
||||||
|
import { fetchAPI, redirect } from "../Fetcher.ts"
|
||||||
|
import { saveSession } from "../api/session.ts"
|
||||||
|
|
||||||
|
export default function RegisterPage() {
|
||||||
|
|
||||||
|
const usernameField = useRef<HTMLInputElement>(null)
|
||||||
|
const passwordField = useRef<HTMLInputElement>(null)
|
||||||
|
const confirmpasswordField = useRef<HTMLInputElement>(null)
|
||||||
|
const emailField = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
|
|
||||||
|
const [errors, setErrors] = useState<Failure[]>([])
|
||||||
|
|
||||||
|
async function handleSubmit(e: FormEvent) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
const username = usernameField.current!.value
|
||||||
|
const password = passwordField.current!.value
|
||||||
|
const confirmpassword = confirmpasswordField.current!.value
|
||||||
|
const email = emailField.current!.value
|
||||||
|
|
||||||
|
if (confirmpassword !== password) {
|
||||||
|
setErrors([{
|
||||||
|
type: "password",
|
||||||
|
messages: ["le mot de passe et la confirmation du mot de passe ne sont pas equivalent."],
|
||||||
|
}])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetchAPI("auth/register", { username, password, email })
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const { token, expirationDate } = await response.json()
|
||||||
|
saveSession({ auth: { token, expirationDate } })
|
||||||
|
redirect("/")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const failures = await response.json()
|
||||||
|
setErrors(Object.entries<string[]>(failures).map(([type, messages]) => ({ type, messages })))
|
||||||
|
} catch (e) {
|
||||||
|
setErrors([{ type: "internal error", messages: ["an internal error occurred."] }])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return <div className="container">
|
||||||
|
<center>
|
||||||
|
<h2>S'enregistrer</h2>
|
||||||
|
</center>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{errors.map(({ type, messages }) =>
|
||||||
|
messages.map(message => <p key={type} style={{ color: "red" }}>{type} : {message}</p>))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className="form-group">
|
||||||
|
|
||||||
|
<label htmlFor="username">Nom d'utilisateur :</label>
|
||||||
|
<input ref={usernameField} type="text" id="username" name="username" required />
|
||||||
|
|
||||||
|
<label htmlFor="password">Mot de passe :</label>
|
||||||
|
<input ref={passwordField} type="password" id="password" name="password" required />
|
||||||
|
|
||||||
|
<label htmlFor="confirmpassword">Confirmer le mot de passe :</label>
|
||||||
|
<input ref={confirmpasswordField} type="password" id="confirmpassword" name="confirmpassword"
|
||||||
|
required />
|
||||||
|
|
||||||
|
<label htmlFor="email">Email :</label>
|
||||||
|
<input ref={emailField} type="text" id="email" name="email" required />
|
||||||
|
|
||||||
|
<label className="consentement">
|
||||||
|
<input type="checkbox" name="consentement" required />
|
||||||
|
En cochant cette case, j'accepte que mes données personnelles, tel que mon adresse e-mail, soient
|
||||||
|
collectées et traitées conformément à la politique de confidentialité de Sportify.
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<a href={BASE + "/login"} className="inscr">Vous avez déjà un compte ?</a>
|
||||||
|
</div>
|
||||||
|
<div id="buttons">
|
||||||
|
<input className="button" type="submit" value="Créer votre compte" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
}
|
@ -0,0 +1,67 @@
|
|||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
background-color: #f1f1f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"], input[type="password"] {
|
||||||
|
width: 95%;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-messages {
|
||||||
|
color: #ff331a;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inscr{
|
||||||
|
font-size: small;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.consentement{
|
||||||
|
font-size: small;
|
||||||
|
}
|
||||||
|
|
||||||
|
#buttons{
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 10px 20px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.button{
|
||||||
|
background-color: #007bff;
|
||||||
|
color: #fff;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover{
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
Loading…
Reference in new issue