Nano index (#8)

* begin appearance

* update

* update

* add profile page

* begin

* add login and logout functionality

* update index

* update
This commit is contained in:
nano
2023-04-27 17:50:06 +02:00
committed by GitHub
parent f0180f220f
commit 22a1cad926
11 changed files with 544 additions and 25 deletions

134
api.php Normal file
View File

@@ -0,0 +1,134 @@
<?php
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
require_once 'resources/config.php';
require_once 'resources/database.php';
require_once LIBRARY_PATH . '/redirect.php';
require_once LIBRARY_PATH . '/exceptions.php';
$pathInfo = explode('/', trim($_SERVER['PATH_INFO'], '/\\'));
header('content-type: application/json; charset=utf-8');
$db = new Database();
function getAuthorizationToken(): ?string{
$headers = getallheaders();
$authorization = $headers['Authorization'];
if (!isset($authorization)) {
APIErrors::invalidHeader();
}
$authorization = explode(' ', trim($authorization), 2)[1];
if (empty($authorization)) {
APIErrors::invalidGrant();
}
return $authorization;
}
class APIErrors{
public static function invalidGrant()
{
http_response_code(400);
die(json_encode(array(
'error' => 'invalid_grant',
'error_description' => 'The authorization code is invalid or expired.'
)));
}
public static function invalidHeader()
{
http_response_code(400);
die(json_encode(array(
'error' => 'invalid_header',
'error_description' => 'The request is missing the Authorization header or the Authorization header is invalid.'
)));
}
public static function invalidRequest()
{
http_response_code(400);
die(json_encode(array(
'error' => 'invalid_request',
'error_description' => 'The request is missing a parameter, uses an unsupported parameter, uses an invalid parameter or repeats a parameter.'
)));
}
public static function invalidCredential()
{
http_response_code(400);
die(json_encode(array(
'error' => 'invalid_credential',
'error_description' => 'The request has error(s) in the credentials gave.'
)));
}
public static function internalError()
{
http_response_code(500);
die();
}
}
switch ($pathInfo[0] . $_SERVER['REQUEST_METHOD']) {
case 'login' . 'POST':
$email = $_POST['email'];
$password = $_POST['pwd'];
if (!isset($email) || !isset($password)) {
APIErrors::invalidRequest();
}
try {
$result = $db->connectUser($email, $password, time()+14400);
} catch (AuthenticationException $_) {
APIErrors::invalidGrant();
}
http_response_code(200);
die(json_encode(array(
/*'access_token' => $access_token,
'created_at' => time(),
'token_type' => 'bearer'*/
'pass' => 'OK'
)));
case 'logout' . 'POST':
$authorization = getAuthorizationToken();
try {
$db->disconnectUser($authorization);
} catch (AuthenticationException $_) {
APIErrors::invalidGrant();
}
http_response_code(200);
die(json_encode(array(
'message' => 'Authorization code delete successfully.'
)));
case 'getEmail' . 'POST':
$authorization = getAuthorizationToken();
try {
$result = $db->getEmail($authorization);
} catch (AuthenticationException $_) {
APIErrors::invalidGrant();
}
http_response_code(200);
die(json_encode($result));
case 'test' . 'GET' :
http_response_code(200);
die(json_encode(test()));
default:
http_response_code(404);
die();
}
?>

View File

View File

@@ -1,14 +1,18 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdn.tailwindcss.com"></script>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- JQuery integration -->
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script>
<script src="https://cdn.tailwindcss.com"></script>
<script src="public_html/js/utils.js"></script>
<script src="public_html/js/home.js" defer></script>
</head>
<body>
<header class="fixed flex justify-between p-2.5 bg-lime-600 top-0 w-full">
<h2 class="text-white">Fermentardoise</h2>
<div class="flex gap-x-5 flex-wrap">
<div class="flex gap-x-5 flex-wrap" id="header-right">
<div class="lg:inline-flex sm:max-lg:flex-col space-x-3 sm:max-lg:space-y-2 group">
<input type="search" name="" id=""
class="bg-green-500 rounded-full align-middle hidden scale-0 px-2.5
@@ -16,17 +20,17 @@
focus:scale-100 group-focus-within:scale-100 focus:flex group-focus-within:flex"
placeholder="Rechercher">
<button class="bg-slate-400 rounded-xl px-1.5 focus:ring focus:ring-red-600">
rechercher
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
</svg>
</button>
<a href="login.html" class="rounded-full border bg-rose-600 text-white px-1.5 hover:bg-green-500">
Connexion
</a>
</div>
<input type="search" name="" id=""
class="bg-green-500 rounded-full align-middle hidden scale-0 px-2.5
text-white placeholder:text-white
sm:max-lg:focus:scale-100 sm:max-lg:peer-focus-within:scale-100 sm:max-lg:focus:flex sm:max-lg:peer-focus-within:flex"
placeholder="Rechercher">
<a href="login.html" class="rounded-full border bg-rose-600 text-white px-1.5 hover:bg-green-500" id="connexion">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15m3 0l3-3m0 0l-3-3m3 3H9" />
</svg>
</a>
<div id="profile-info" class="group"></div>
</div>
</header>
<main class="flex justify-center w-full h-screen">

View File

@@ -1,9 +1,14 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdn.tailwindcss.com"></script>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- JQuery integration -->
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script>
<script src="https://cdn.tailwindcss.com"></script>
<!-- Initialisation Personal Script -->
<script src="public_html/js/utils.js" defer></script>
<script src="public_html/js/login.js" defer></script>
</head>
<body>
<header class="fixed flex justify-between p-2.5 bg-lime-600 top-0 w-full">
@@ -15,19 +20,25 @@
focus:scale-100 group-focus-within:scale-100 focus:flex group-focus-within:flex"
placeholder="Rechercher">
<button class="bg-slate-400 rounded-xl px-1.5 focus:ring focus:ring-red-600">
rechercher
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
</svg>
</button>
</div>
</header>
<main class="flex justify-center w-full h-[70vh]">
<div class="bg-blue-300 self-center flex flex-col p-3.5 w-[60vw] space-y-2.5">
<main class="flex justify-center w-full h-[70vh] flex-col">
<div class="bg-amber-500 mx-80 mb-20 rounded-lg text-center text-lg hidden" id="error-zone">
Mail ou mot de passe non reconnu !
</div>
<form class="bg-blue-300 self-center flex flex-col p-3.5 w-[60vw] space-y-2.5" id ="form_login">
<h3>Connexion</h3>
<div class="items-center" id="error-zone"></div>
<div class="flex flex-col rounded-md shadow-sm">
<input type="email" name="" id="" placeholder="Mail" autocomplete="email" required
<input type="email" name="" id="email" placeholder="Mail" autocomplete="email" required
class="rounded-t-md border border-gray-500 px-2 py-1 w-full ring-0 placeholder:text-zinc-400
focus:outline-none focus:border-emerald-600 focus:ring-1 focus:ring-emerald-600 focus:z-10">
<input type="password" name="" id="" placeholder="Mot de passe" autocomplete="current-password" required
<input type="password" name="" id="pwd" placeholder="Mot de passe" autocomplete="current-password" required
class="rounded-b-md border border-gray-500 px-2 py-1 w-full ring-0 placeholder:text-zinc-400
focus:outline-none focus:border-emerald-600 focus:ring-1 focus:ring-emerald-600 focus:z-10">
</div>
@@ -41,10 +52,10 @@
<a href="" class="font-medium text-indigo-600 hover:text-indigo-500">Mot de passe oublié ?</a>
</div>
</div>
<button type="submit" class="w-full bg-slate-200 rounded-md py-1 px-2">
<button id="login_button" type="submit" class="w-full bg-slate-200 rounded-md py-1 px-2">
Connexion
</button>
</div>
</form>
</main>
<footer class="fixed bottom-0 w-full text-center bg-lime-300 py-2">
Made with ❤️ & 🍺 by Appen

View File

@@ -0,0 +1,25 @@
<button type="button" class="inline-flex items-center gap-x-1 text-sm font-semibold leading-6 text-gray-900" id="profile">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</button>
<div id="deconnexion" class="group-focus-within:visible invisible absolute right-0 mt-5 z-10 flex w-50 max-w-50 px-4">
<div class="w-sm max-w-sm flex-auto overflow-hidden rounded-3xl bg-white text-sm leading-6 shadow-lg ring-1 ring-gray-900/5">
<div class=" static p-4">
<p id="profil_name" class="relative -top-1.5 text-center"></p>
<div class="group relative flex rounded-lg gap-x-5 hover:bg-gray-50 align-middle">
<div class="mt-1 flex flex-none items-center justify-center rounded-lg group-hover:bg-gray-50 bg-white">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15M12 9l-3 3m0 0l3 3m-3-3h12.75" />
</svg>
</div>
<div>
<button type="button" class="font-semibold text-gray-900" id="deconnect">
Déconnexion
<span class="absolute inset-0"></span>
</button>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>

After

Width:  |  Height:  |  Size: 389 B

29
public_html/js/home.js Normal file
View File

@@ -0,0 +1,29 @@
function profile(){
if(getCookie('fermentardoise_session') != ""){
$("#connexion").remove();
$("#profile-info").load("public_html/html_elements/profile.html", () => {
$.ajax({
type: 'POST',
url: 'api.php/getEmail',
headers: {
Authorization: 'Bearer ' + getCookie('fermentardoise_session')
}
}).done((data) => {
$("#profil_name").append(data);
});
$("#deconnect").click(() => {
$.ajax({
type: 'POST',
url: 'api.php/logout',
headers: {
Authorization: 'Bearer ' + getCookie('fermentardoise_session')
}
}).done((data) => {
window.location.replace("index.html");
});
});
});
}
}
profile();

29
public_html/js/login.js Normal file
View File

@@ -0,0 +1,29 @@
function listener_login() {
if(getCookie('fermentardoise_session') != ""){
window.location.replace("index.html");
}
document.getElementById("form_login").addEventListener("submit", function (event) {
event.preventDefault();
event.stopImmediatePropagation();
console.log("connexion");
let mail = document.getElementById("email").value;
let upwd = document.getElementById("pwd").value;
$.ajax({
type: 'POST',
url: 'api.php/login',
data: {
email: mail,
pwd: upwd,
},
statusCode: {
400: function () {
$("#error-zone").removeClass("hidden");
}
}
}).done(() => {
window.location.replace("index.html");
});
});
}
listener_login();

42
public_html/js/utils.js Normal file
View File

@@ -0,0 +1,42 @@
function createCookie(name, value, timestamp) {
var expires;
if (timestamp) {
var date = new Date(timestamp);
expires = "; expires=" + date.toGMTString();
} else {
expires = "";
}
document.cookie = name + "=" + value + expires + "; path=/";
}
function getCookie(c_name) {
let c_start;
let c_end;
if (document.cookie.length > 0) {
c_start = document.cookie.indexOf(c_name + "=");
if (c_start != -1) {
c_start = c_start + c_name.length + 1;
c_end = document.cookie.indexOf(";", c_start);
if (c_end == -1) {
c_end = document.cookie.length;
}
return unescape(document.cookie.substring(c_start, c_end));
}
}
return "";
}
function get_cookie(name){
return document.cookie.split(';').some(c => {
return c.trim().startsWith(name + '=');
});
}
function deleteCookie( name, path, domain ) {
if( get_cookie( name ) ) {
document.cookie = name + "=" +
((path) ? ";path="+path:"")+
((domain)?";domain="+domain:"") +
";expires=Thu, 01 Jan 1970 00:00:01 GMT";
}
}

241
resources/database.php Normal file
View File

@@ -0,0 +1,241 @@
<?php
/**
* PHP Version 8.2.1
*/
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
require_once 'config.php';
require_once 'library/exceptions.php';
/**
* Collection of methods to communicate with the database.
*/
class Database {
protected $PDO;
/**
* Connect to the PostgreSQL database.
*
* @throws PDOException Error thrown if the connection
* to the database failed.
*/
public function __construct(){
$this->PDO = new PDO(
'pgsql:host=' . DB_SERVER . ';port=' . DB_PORT . ';dbname=' . DB_NAME,
DB_USER,
DB_PASSWORD
);
}
/**
* Gets the password hash of a user.
*
* @param string $eamil
*
* @return ?string if the password hash exists.
*/
public function getUserPasswordHash(string $email): ?string{
$email = strtolower($email);
$request = 'SELECT password_hash from users where email = :email';
$statement = $this->PDO->prepare($request);
$statement->bindParam(':email', $email);
$statement->execute();
$result = $statement->fetch(PDO::FETCH_OBJ);
if (!$result) {
return NULL;
}
return $result->password_hash;
}
/**
* Verifies the user credentials.
*
* @param string $email
* @param string $password
*
* @return bool
*/
public function verifyUserCredentials(string $email, string $password): bool {
$password_hash = $this->getUserPasswordHash($email);
return !empty($password_hash) && password_verify($password, $password_hash);
}
/**
* Verifies the user access token.
*
* @param string $access_token
*
* @return bool
*/
public function verifyUserAccessToken(string $access_token): bool {
$request = 'SELECT * from users where access_token = :access_token';
$statement = $this->PDO->prepare($request);
$statement->bindParam(':access_token', $access_token);
$statement->execute();
$result = $statement->fetch(PDO::FETCH_OBJ);
return !empty($result);
}
/**
* Connects the user by returning its unique id if the
* credentials are valid.
*
* @param string $email
* @param string $password
* @param int $session_expire (optional) The lifetime of the session cookie in seconds.
*
* @throws AuthenticationException If the authentication failed.
*/
public function connectUser(string $email, string $password, int $session_expire = 0): bool {
// test if the credentials are correct
if (!$this->verifyUserCredentials($email, $password)) {
throw new AuthenticationException();
}
// make email lowercase in case the user used uppercase letters
$email = strtolower($email);
// create a unique token used to identify the user
$access_token = hash('sha256', $email . $password . microtime(true));
// Set session hash on the user
$request = 'UPDATE users SET access_token = :access_token
WHERE email = :email';
$statement = $this->PDO->prepare($request);
$statement->bindParam(':email', $email);
$statement->bindParam(':access_token', $access_token);
$success = $statement->execute();
// Throw an exception if the update failed
if (!$success) {
throw new Exception('Failed to connect user.');
}
if ($session_expire > 0) {
$session_expire = time() + $session_expire;
}
// set the session cookie
return setcookie(
ACCESS_TOKEN_NAME,
$access_token,
$session_expire,
"/"
);
}
/**
* Create an access token if the credentials are valid
* the functionality is the same as connectUser but
* this function is related to AJAX request
*
* @param string $email
* @param string $password
*
* @return string the access_token
*/
public function getUserAccessToken(string $email, string $password): ?string {
if (!$this->verifyUserCredentials($email, $password)) {
return NULL;
}
$email = strtolower($email);
$access_token = hash('sha256', $email . $password . time());
//Set the access token to the user
$request = 'UPDATE users set access_token = :access_token where email = :email';
$statement = $this->PDO->prepare($request);
$statement->bindParam(':access_token', $access_token);
$statement->bindParam(':email', $email);
$statement->execute();
return $access_token;
}
/**
* Disconnects the user by deleting the access token.
*
* @param string $access_token
*
* @throws AuthenticationException If the access token is invalid.
*/
public function disconnectUser($access_token): bool{
if (!$this->verifyUserAccessToken($access_token)) {
throw new AuthenticationException();
}
// remove access token from the user
$request = 'UPDATE users SET access_token = NULL
WHERE access_token = :access_token';
$statement = $this->PDO->prepare($request);
$statement->bindParam(':access_token', $access_token);
$success = $statement->execute();
// delete the session cookie
unset($_COOKIE[ACCESS_TOKEN_NAME]);
setcookie(ACCESS_TOKEN_NAME, '', -1, '/');
return $success;
}
/**
* Gets the user email
*
* @param string $access_token
*
* @return ?string
*/
public function getEmail(string $access_token): ?string{
if (!$this->verifyUserAccessToken($access_token)) {
throw new AuthenticationException();
}
$request = 'SELECT email from users where access_token = :access_token';
$statement = $this->PDO->prepare($request);
$statement->bindParam(':access_token', $access_token);
$statement->execute();
$result = $statement->fetch(PDO::FETCH_OBJ);
return $result->email;
}
public function test(){
if (!isset($_COOKIE[ACCESS_TOKEN_NAME])) {
return false;
}
$access_token = $_COOKIE[ACCESS_TOKEN_NAME];
if (!$this->verifyUserAccessToken($access_token)) {
throw new AuthenticationException();
}
$request = 'SELECT * from users where access_token = :access_token';
$statement = $this->PDO->prepare($request);
$statement->bindParam(':access_token', $access_token);
$statement->execute();
$result = $statement->fetchAll(PDO::FETCH_ASSOC);
return $result;
}
}
?>

View File

@@ -12,8 +12,9 @@ DROP TABLE IF EXISTS payments CASCADE;
-- Table users
CREATE TABLE users (
id SERIAL PRIMARY KEY,
login VARCHAR(255) NOT NULL,
password_hash VARCHAR(50) NOT NULL
email VARCHAR(255) NOT NULL,
password_hash VARCHAR(64) NOT NULL,
access_token VARCHAR(64)
);
-- Table clients