Ajoute le routeur et la page d'instanciation de la phrase de passe

This commit is contained in:
Clement Desmidt 2022-03-01 14:10:13 +01:00
parent c2b60b4b6a
commit a8116aa5a2
12 changed files with 5919 additions and 171 deletions

29
.eslintrc.json Normal file
View File

@ -0,0 +1,29 @@
{
"env": {
"browser": true,
"es2021": true
},
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react-hooks/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": [
"react",
"@typescript-eslint",
"react-hooks"
],
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
}

View File

@ -19,6 +19,7 @@ class UserController extends Controller
$user = User::where('id', Auth::user()->getAuthIdentifier())->firstOrFail(); $user = User::where('id', Auth::user()->getAuthIdentifier())->firstOrFail();
$user->checkword = $validated['checkword']; $user->checkword = $validated['checkword'];
$user->save(); $user->save();
Auth::setUser($user);
return response()->json(['success' => true]); return response()->json(['success' => true]);
} }

View File

@ -2,6 +2,7 @@
namespace App\Http; namespace App\Http;
use App\Http\Middleware\CheckExistingPassphrase;
use Illuminate\Foundation\Http\Kernel as HttpKernel; use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel class Kernel extends HttpKernel
@ -54,6 +55,7 @@ class Kernel extends HttpKernel
* @var array<string, class-string|string> * @var array<string, class-string|string>
*/ */
protected $routeMiddleware = [ protected $routeMiddleware = [
'auth.passphrase' => CheckExistingPassphrase::class,
'auth' => \App\Http\Middleware\Authenticate::class, 'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,

View File

@ -0,0 +1,30 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class CheckExistingPassphrase
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
*/
public function handle(Request $request, Closure $next)
{
if (!$request->routeIs('user.*') && empty(Auth::user()->checkword)) {
return redirect(route('user.first'));
}
if (!empty(Auth::user()->checkword) && $request->routeIs('user.first')) {
return redirect(route('pages.index'));
}
return $next($request);
}
}

2387
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -13,10 +13,14 @@
"@babel/preset-react": "^7.13.13", "@babel/preset-react": "^7.13.13",
"@popperjs/core": "^2.10.2", "@popperjs/core": "^2.10.2",
"@tailwindcss/forms": "^0.4.0", "@tailwindcss/forms": "^0.4.0",
"@typescript-eslint/eslint-plugin": "^5.12.1",
"@typescript-eslint/parser": "^5.12.1",
"alpinejs": "^3.4.2", "alpinejs": "^3.4.2",
"autoprefixer": "^10.1.0", "autoprefixer": "^10.1.0",
"axios": "^0.21.4", "axios": "^0.21.4",
"bootstrap": "^5.1.3", "bootstrap": "^5.1.3",
"eslint": "^8.9.0",
"eslint-plugin-react": "^7.29.0",
"laravel-mix": "^6.0.6", "laravel-mix": "^6.0.6",
"lodash": "^4.17.19", "lodash": "^4.17.19",
"postcss": "^8.2.1", "postcss": "^8.2.1",
@ -36,8 +40,10 @@
"@mui/icons-material": "^5.4.1", "@mui/icons-material": "^5.4.1",
"@mui/material": "^5.4.1", "@mui/material": "^5.4.1",
"@uiw/react-md-editor": "^3.9.4", "@uiw/react-md-editor": "^3.9.4",
"eslint-plugin-react-hooks": "^4.3.0",
"react-crypt-gsm": "^1.0.4", "react-crypt-gsm": "^1.0.4",
"react-query": "^3.34.12", "react-query": "^3.34.12",
"react-router-dom": "^5.3.0",
"storage-encryption": "^1.0.16" "storage-encryption": "^1.0.16"
} }
} }

3395
public/js/app.js vendored

File diff suppressed because it is too large Load Diff

View File

@ -12,5 +12,5 @@ require('./bootstrap');
* or customize the JavaScript scaffolding to fit your unique needs. * or customize the JavaScript scaffolding to fit your unique needs.
*/ */
require('./components/pages/App'); require('./components/pages/AppWrapper');
require('./components/user/First'); require('./components/user/First');

View File

@ -1,11 +1,17 @@
import ReactDOM from 'react-dom';
import PageForm from "./Form"; import PageForm from "./Form";
import Pages from "./List"; import Pages from "./List";
import Prompt from "./Prompt"; import Prompt from "./Prompt";
import {useState} from "react";
import * as React from 'react'; import * as React from 'react';
import {Divider, Paper} from "@mui/material"; import {Alert, AlertTitle, Divider} from "@mui/material";
import {EncryptStorage} from "storage-encryption"; import {EncryptStorage} from "storage-encryption";
import {
Switch,
Route,
Link,
Redirect,
withRouter,
useHistory
} from "react-router-dom";
interface List { interface List {
id: string; id: string;
@ -13,9 +19,9 @@ interface List {
} }
const app = document.getElementById('app'); const app = document.getElementById('app');
const word = "shikiryu"; const word = "shikiryu"; // FIXME should be in db and ≠ between users
let sessionPassphrase = sessionStorage.getItem("key"); const sessionPassphrase = sessionStorage.getItem("key");
let pages: List[] = []; let pages: List[] = [];
let getPageContentUrl, let getPageContentUrl,
postUrl, postUrl,
@ -30,42 +36,120 @@ if (app) {
removeUrl = "" + app.getAttribute('data-remove'); removeUrl = "" + app.getAttribute('data-remove');
csrf = "" + app.getAttribute('data-csrf'); csrf = "" + app.getAttribute('data-csrf');
checkword = "" + app.getAttribute('data-checkword'); checkword = "" + app.getAttribute('data-checkword');
ReactDOM.render(<App/>, app);
} }
export default function App() { function App() {
const [listPages, setListPages] = useState(pages); const [listPages, setListPages] = React.useState(pages);
const [passphrase, setPassphrase] = useState(sessionPassphrase); const [passphrase, setPassphrase] = React.useState(sessionPassphrase);
const history = useHistory();
const [user, setUser] = React.useState(false);
const [error, isError] = React.useState(false);
const updatePassphrase = function(newPassphrase) { const signin = cb => {
setPassphrase(newPassphrase); const isAuthenticated = checkPassphrase();
return result(checkPassphrase()); setUser(isAuthenticated);
if (isAuthenticated) {
isError(false);
cb();
} else {
isError(true);
}
}; };
const checkPassphrase = function() { const signout = cb => {
setUser(false);
cb();
};
function checkPassphrase() {
if (checkword === "" || checkword === null || checkword === "null") { if (checkword === "" || checkword === null || checkword === "null") {
console.error("checkword is empty !"); console.error("checkword is empty !");
// return (<Redirect to="/first" />); return false;
return false; // TODO redirect to first
} }
localStorage.setItem("checkword", checkword); localStorage.setItem("checkword", checkword);
const key = ""+sessionStorage.getItem("key"); const key = ""+sessionStorage.getItem("key");
if (key === "" || key === null || key === "null") { if (key === "" || key === null || key === "null") {
console.error("key is empty 🤔 !"); console.error("key is empty 🤔 !");
// return (<Redirect to="/first" />); return false;
return false; // TODO redirect to first ?
} }
let encryptStorage = new EncryptStorage(key); const encryptStorage = new EncryptStorage(key);
const decrypted_word = encryptStorage.decrypt("checkword"); const decrypted_word = encryptStorage.decrypt("checkword");
return decrypted_word === word; return decrypted_word === word;
}; }
const result = function(correct) { const Error = function() {
let content; if (error) {
if (correct === true) { return (
content = <div className="col-md-8"> <Alert severity="error">
<AlertTitle>Erreur</AlertTitle>
La phrase de passe ne correspond pas.
</Alert>
);
}
return (<div></div>);
}
const AuthButton = function() {
const history = useHistory();
return user ? (
<p>
Welcome!{" "}
<button
onClick={() => {
signout(() => history.push("/"));
}}
>
Sign out
</button>
</p>
) : (
<p>You are not logged in.</p>
);
}
function updatePassphrase(newPassphrase) {
setPassphrase(newPassphrase);
if (checkPassphrase()) {
isError(false);
setUser(true);
signin(() => {
history.push({ pathname: "/diary/public/pages" });
})
} else {
isError(true);
}
}
function PrivateRoute({ children, ...rest }) {
return (
<Route
{...rest}
render={({ location }) =>
user ? (
children
) : (
<Redirect
to={{
pathname: "/diary/public/pass",
state: { from: location }
}}
/>
)
}
/>
);
}
function PromptPage() {
return (<Prompt open={true} setOpen={updatePassphrase}/>);
}
function ListPage() {
return (
<div className="col-md-8">
<Pages <Pages
pages={listPages} pages={listPages}
url={getPageContentUrl} url={getPageContentUrl}
@ -76,24 +160,29 @@ export default function App() {
<Divider/> <Divider/>
<PageForm setListPages={setListPages} csrf={csrf} url={postUrl} passphrase={passphrase}/> <PageForm setListPages={setListPages} csrf={csrf} url={postUrl} passphrase={passphrase}/>
</div> </div>
} else if (correct === false) { );
content = <div className="col-md-8">
<Prompt open={true} setOpen={updatePassphrase}/>
</div>;
} else {
return correct;
} }
return ( return (
<div> <div>
<div className="container"> <AuthButton />
<div className="row justify-content-center"> <Error/>
{ content } <ul>
</div> <li>
</div> <Link to="/diary/public/pages">Voir vos pages</Link>
</li>
</ul>
<Switch>
<Route path="/diary/public/pass">
<PromptPage />
</Route>
<PrivateRoute path="/diary/public/pages">
<ListPage />
</PrivateRoute>
</Switch>
</div> </div>
); );
} }
return result(checkPassphrase()); export default withRouter(App);
}

View File

@ -0,0 +1,17 @@
import * as React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router} from "react-router-dom";
import App from "./App";
const app = document.getElementById('app');
if (app) {
ReactDOM.render(<AppWrapper/>, app);
}
export default function AppWrapper() {
return (
<Router>
<App />
</Router>
)
}

View File

@ -24,15 +24,19 @@ export default function FirstPage() {
encryptStorage = new EncryptStorage(passphrase); encryptStorage = new EncryptStorage(passphrase);
encryptStorage.encrypt("checkword", word); encryptStorage.encrypt("checkword", word);
let encryptedFormData = new FormData(); let encryptedFormData = new FormData();
encryptedFormData.append("checkword", ""+localStorage.getItem("key")); encryptedFormData.append("checkword", ""+localStorage.getItem("checkword"));
encryptedFormData.append('_token', csrf); encryptedFormData.append('_token', csrf);
let response = await fetch(url, { const response = await fetch(url, {
method: 'POST', method: 'POST',
body: encryptedFormData body: encryptedFormData
}); });
const json = await response.json(); // TODO redirect if success const json = await response.json(); // TODO redirect if success
if (json.success) {
location.href = "/diary/public/pages"
}
}; };
const updatePassphrase = (e: React.ChangeEvent<HTMLInputElement>) => { const updatePassphrase = (e: React.ChangeEvent<HTMLInputElement>) => {

View File

@ -20,10 +20,10 @@ Route::get('/', function () {
}); });
Route::resource('pages', PageController::class) Route::resource('pages', PageController::class)
->middleware(['auth']); ->middleware(['auth', 'auth.passphrase']);
Route::get('/first', [UserController::class, 'first'])->name('user.first'); Route::get('/first', [UserController::class, 'first'])->middleware(['auth', 'auth.passphrase'])->name('user.first');
Route::post('/first', [UserController::class, 'storeFirst'])->name('user.storeFirst'); Route::post('/first', [UserController::class, 'storeFirst'])->middleware(['auth', 'auth.passphrase'])->name('user.storeFirst');
Route::get('/dashboard', function () { Route::get('/dashboard', function () {
return view('dashboard'); return view('dashboard');