Compare commits
No commits in common. "93be26fac8fbd7c106a1e5105c8e63ae32f3838b" and "2f0d63a90fb422454dbad28996d4c2c52929da90" have entirely different histories.
93be26fac8
...
2f0d63a90f
26 changed files with 7 additions and 5047 deletions
13
README.md
13
README.md
|
@ -17,7 +17,7 @@
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
<p align="center">A simple yet somehow overdesigned guestbook system featuring a simple control panel</p>
|
<p align="center">A simple yet somehow overdesigned guestbook system featuring a simple control panel <small>(which is a WIP so you'll have to make do with a db manager)</small></p>
|
||||||
|
|
||||||
<p align="center"> This is phase I of the personal backscratchers project.</p>
|
<p align="center"> This is phase I of the personal backscratchers project.</p>
|
||||||
|
|
||||||
|
@ -29,8 +29,6 @@
|
||||||
- [📑 Documentation ](#-documentation-)
|
- [📑 Documentation ](#-documentation-)
|
||||||
- [🏁 Getting Started ](#-getting-started-)
|
- [🏁 Getting Started ](#-getting-started-)
|
||||||
- [🕸️ Prerequisites](#️-prerequisites)
|
- [🕸️ Prerequisites](#️-prerequisites)
|
||||||
- [Backend:](#backend)
|
|
||||||
- [Admin panel:](#admin-panel)
|
|
||||||
- [🚀 Deployment ](#-deployment-)
|
- [🚀 Deployment ](#-deployment-)
|
||||||
- [⛏️ Built Using ](#️-built-using-)
|
- [⛏️ Built Using ](#️-built-using-)
|
||||||
- [✍️ Authors ](#️-authors-)
|
- [✍️ Authors ](#️-authors-)
|
||||||
|
@ -51,8 +49,6 @@ These instructions will get you a copy of the project up and running on your loc
|
||||||
|
|
||||||
## 🕸️ Prerequisites
|
## 🕸️ Prerequisites
|
||||||
|
|
||||||
### Backend:
|
|
||||||
|
|
||||||
For running it locally:
|
For running it locally:
|
||||||
- .NET 8.0
|
- .NET 8.0
|
||||||
- A running instance of MongoDB
|
- A running instance of MongoDB
|
||||||
|
@ -88,12 +84,6 @@ You will be able to see in `build/docker-compose.public.yml` that the applicatio
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
> For local usage of the backend, you can use `docker-compose.local.yml` and edit the fields you need.
|
> For local usage of the backend, you can use `docker-compose.local.yml` and edit the fields you need.
|
||||||
|
|
||||||
### Admin panel:
|
|
||||||
|
|
||||||
The admin panel is a simple React/Vite app. For development, it should be enough to run `vite` in Guestbooky-admin's `src` folder.
|
|
||||||
|
|
||||||
In order to create a live version, adjust the **API_URL** path in `Guestbooky-admin/src/environment/constants.js`, and execute `vite build`.
|
|
||||||
|
|
||||||
## 🚀 Deployment <a name = "deployment"></a>
|
## 🚀 Deployment <a name = "deployment"></a>
|
||||||
|
|
||||||
Use `docker-compose.public.yml` as a basis. it should create the image for you and start running.
|
Use `docker-compose.public.yml` as a basis. it should create the image for you and start running.
|
||||||
|
@ -102,7 +92,6 @@ Use `docker-compose.public.yml` as a basis. it should create the image for you a
|
||||||
|
|
||||||
- [MongoDB](https://www.mongodb.com/) - Database
|
- [MongoDB](https://www.mongodb.com/) - Database
|
||||||
- [.NET](https://dot.net/) - Backend
|
- [.NET](https://dot.net/) - Backend
|
||||||
- [React](https://react.dev/)/[Vite](https://vite.dev) - Admin Panel
|
|
||||||
- [Cloudflare Turnstile](https://www.cloudflare.com/pt-br/products/turnstile/) - Captcha
|
- [Cloudflare Turnstile](https://www.cloudflare.com/pt-br/products/turnstile/) - Captcha
|
||||||
|
|
||||||
## ✍️ Authors <a name = "authors"></a>
|
## ✍️ Authors <a name = "authors"></a>
|
||||||
|
|
|
@ -43,4 +43,3 @@ ___
|
||||||
- There isn't much exception handling, except in the API layer. This is on purpose. Another thing that this project could really use, but is left as an exercise, is using a `Maybe<T>/Result<T>/ErrorOr<T>` type.
|
- There isn't much exception handling, except in the API layer. This is on purpose. Another thing that this project could really use, but is left as an exercise, is using a `Maybe<T>/Result<T>/ErrorOr<T>` type.
|
||||||
- Since there is so little that can go wrong with *low-stakes CRUDding*, it is a reasonable trade-off to let the API layer catch and send an internal server error.
|
- Since there is so little that can go wrong with *low-stakes CRUDding*, it is a reasonable trade-off to let the API layer catch and send an internal server error.
|
||||||
|
|
||||||
- By default, you need to choose between good Cookie-based authentication defaults or REST-friendly authentication via the `Authorization` header. Luckily you can support both with a few small additions - it made more sense to keep *RESTy* as the main method and stick Cookie support to its tail.
|
|
24
src/Guestbooky-admin/.gitignore
vendored
24
src/Guestbooky-admin/.gitignore
vendored
|
@ -1,24 +0,0 @@
|
||||||
# Logs
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
pnpm-debug.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
|
|
||||||
node_modules
|
|
||||||
dist
|
|
||||||
dist-ssr
|
|
||||||
*.local
|
|
||||||
|
|
||||||
# Editor directories and files
|
|
||||||
.vscode/*
|
|
||||||
!.vscode/extensions.json
|
|
||||||
.idea
|
|
||||||
.DS_Store
|
|
||||||
*.suo
|
|
||||||
*.ntvs*
|
|
||||||
*.njsproj
|
|
||||||
*.sln
|
|
||||||
*.sw?
|
|
|
@ -1,38 +0,0 @@
|
||||||
import js from '@eslint/js'
|
|
||||||
import globals from 'globals'
|
|
||||||
import react from 'eslint-plugin-react'
|
|
||||||
import reactHooks from 'eslint-plugin-react-hooks'
|
|
||||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
|
||||||
|
|
||||||
export default [
|
|
||||||
{ ignores: ['dist'] },
|
|
||||||
{
|
|
||||||
files: ['**/*.{js,jsx}'],
|
|
||||||
languageOptions: {
|
|
||||||
ecmaVersion: 2020,
|
|
||||||
globals: globals.browser,
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: 'latest',
|
|
||||||
ecmaFeatures: { jsx: true },
|
|
||||||
sourceType: 'module',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
settings: { react: { version: '18.3' } },
|
|
||||||
plugins: {
|
|
||||||
react,
|
|
||||||
'react-hooks': reactHooks,
|
|
||||||
'react-refresh': reactRefresh,
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
...js.configs.recommended.rules,
|
|
||||||
...react.configs.recommended.rules,
|
|
||||||
...react.configs['jsx-runtime'].rules,
|
|
||||||
...reactHooks.configs.recommended.rules,
|
|
||||||
'react/jsx-no-target-blank': 'off',
|
|
||||||
'react-refresh/only-export-components': [
|
|
||||||
'warn',
|
|
||||||
{ allowConstantExport: true },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
|
@ -1,13 +0,0 @@
|
||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<link rel="icon" type="image/png" href="/src/assets/guestbooky.png" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Guestbooky Admin Panel</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
<script type="module" src="/src/main.jsx"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
4391
src/Guestbooky-admin/package-lock.json
generated
4391
src/Guestbooky-admin/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -1,30 +0,0 @@
|
||||||
{
|
|
||||||
"name": "guestbooky-admin",
|
|
||||||
"private": true,
|
|
||||||
"version": "0.0.0",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite",
|
|
||||||
"build": "vite build",
|
|
||||||
"lint": "eslint .",
|
|
||||||
"preview": "vite preview"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"axios": "^1.7.7",
|
|
||||||
"guestbooky-admin": "file:",
|
|
||||||
"react": "^18.3.1",
|
|
||||||
"react-dom": "^18.3.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@eslint/js": "^9.13.0",
|
|
||||||
"@types/react": "^18.3.11",
|
|
||||||
"@types/react-dom": "^18.3.1",
|
|
||||||
"@vitejs/plugin-react": "^4.3.3",
|
|
||||||
"eslint": "^9.13.0",
|
|
||||||
"eslint-plugin-react": "^7.37.1",
|
|
||||||
"eslint-plugin-react-hooks": "^5.0.0",
|
|
||||||
"eslint-plugin-react-refresh": "^0.4.13",
|
|
||||||
"globals": "^15.11.0",
|
|
||||||
"vite": "^5.4.9"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
.header{
|
|
||||||
font-size: 3em;
|
|
||||||
word-spacing: 0.3em;
|
|
||||||
letter-spacing: -0.05em;
|
|
||||||
background: linear-gradient(180deg, hsl(15, 80%, 10%) 0%, hsl(15, 50%, 60%) 100%);
|
|
||||||
width: 100%;
|
|
||||||
padding: 2ch;
|
|
||||||
border-bottom: 0.1em solid hsl(15, 80%, 20%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header h1 {
|
|
||||||
margin-inline: auto;
|
|
||||||
width: 1200px;
|
|
||||||
color: hsl(15, 100%, 90%);
|
|
||||||
text-shadow: 0.01em 0.01em 0.01em hsl(15, 0%, 20%);
|
|
||||||
font-family: "Carattere", cursive;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
width: 1200px;
|
|
||||||
max-width: 1200px;
|
|
||||||
margin-inline: auto;
|
|
||||||
padding: 2ch;
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
import {useState} from 'react'
|
|
||||||
import AdminPage from './components/AdminPage'
|
|
||||||
import LoginPage from './components/LoginPage'
|
|
||||||
|
|
||||||
import './App.css'
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
const [isLoggedIn, setIsLoggedIn] = useState(false)
|
|
||||||
|
|
||||||
const handleLoggedIn = (newValue) => {
|
|
||||||
setIsLoggedIn(newValue)
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="header"><h1>Guestbooky Admin</h1></div>
|
|
||||||
<div className="content">
|
|
||||||
{isLoggedIn ? <AdminPage className="content"/> :
|
|
||||||
<LoginPage onLoggedIn={handleLoggedIn} className="content"/>}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App
|
|
Binary file not shown.
Before Width: | Height: | Size: 11 KiB |
|
@ -1,111 +0,0 @@
|
||||||
@keyframes smallFadeIn {
|
|
||||||
0% {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(20px);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-item {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(20px);
|
|
||||||
animation: smallFadeIn 0.5s ease-out forwards;
|
|
||||||
animation-delay: calc(0.2s * var(--child-index));
|
|
||||||
}
|
|
||||||
|
|
||||||
.warning{
|
|
||||||
background-color: hsla(15, 100%, 90%, 0.75);
|
|
||||||
border: 0.2em solid hsla(15, 80%, 70%, 1);
|
|
||||||
border-radius: 0.3em;
|
|
||||||
padding: 2ch 1ch;
|
|
||||||
width: fit-content;
|
|
||||||
font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navigation{
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navigation button {
|
|
||||||
font-size: 1em;
|
|
||||||
margin: 2em;
|
|
||||||
background: hsla(15, 60%, 80%, 1);
|
|
||||||
border-radius: 0.2em;
|
|
||||||
width: fit-content;
|
|
||||||
padding: 0.8em 0.5em;
|
|
||||||
font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navigation button:hover {
|
|
||||||
background: hsla(15, 80%, 90%, 0.75);
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
list-style-type: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul li{
|
|
||||||
line-height: 1.6em;
|
|
||||||
display: grid;
|
|
||||||
grid-row-gap: 2em;
|
|
||||||
padding: 1em;
|
|
||||||
grid-template-rows: auto 1fr;
|
|
||||||
grid-template-columns: 1fr auto auto;
|
|
||||||
font-family: 'Solway', serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul li h2{
|
|
||||||
align-self: center;
|
|
||||||
grid-row: 1;
|
|
||||||
background: linear-gradient(to right, hsl(15, 40%, 85%), hsla(15, 40%, 85%, 0));
|
|
||||||
border-radius: 0.2em;
|
|
||||||
padding: 0.5em 0 0.5em 0.5em;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul li h3{
|
|
||||||
align-self: center;
|
|
||||||
grid-row: 1;
|
|
||||||
justify-self: right;
|
|
||||||
padding-right: 0.5em;
|
|
||||||
vertical-align: bottom;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
ul li button{
|
|
||||||
grid-row: 1;
|
|
||||||
justify-self: right;
|
|
||||||
width: fit-content;
|
|
||||||
border-radius: 0.5em;
|
|
||||||
padding: 0.5em;
|
|
||||||
font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
|
||||||
background: hsla(15, 60%, 80%, 0.75);
|
|
||||||
}
|
|
||||||
ul li button:hover {
|
|
||||||
background: hsla(15, 80%, 90%, 0.75);
|
|
||||||
}
|
|
||||||
.message-text {
|
|
||||||
grid-row: 2;
|
|
||||||
grid-column: 1 / 4;
|
|
||||||
background-color: hsl(15, 50%, 90%);
|
|
||||||
margin: 1em 0.5em;
|
|
||||||
padding: 1em 0.5em;
|
|
||||||
font-weight: 300;
|
|
||||||
border-radius: 0.5em;
|
|
||||||
box-shadow: 0.05em 0.05em 0.05em hsla(15, 20%, 20%, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-text p{
|
|
||||||
line-height: 1.5em;
|
|
||||||
text-align: justify;
|
|
||||||
text-justify: inter-word;
|
|
||||||
text-align-last: left;
|
|
||||||
}
|
|
|
@ -1,51 +0,0 @@
|
||||||
import useMessages from "../hooks/useMessages.js";
|
|
||||||
|
|
||||||
import './AdminPage.css'
|
|
||||||
|
|
||||||
|
|
||||||
const AdminPage = () => {
|
|
||||||
const {messages, totalMessages, loading, error, removeMessage, page, nextPage, previousPage} = useMessages();
|
|
||||||
const localeOptions = {
|
|
||||||
weekday: 'long',
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'long',
|
|
||||||
day: 'numeric',
|
|
||||||
hour: 'numeric',
|
|
||||||
minute: 'numeric',
|
|
||||||
second: 'numeric',
|
|
||||||
timeZoneName: 'short'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (<div className='warning'><p>Loading...</p></div>);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (messages.length === 0 && page > 1) {
|
|
||||||
previousPage();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (messages.length === 0) {
|
|
||||||
return (<div className='warning'><p>No messages.</p></div>);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className='warning'><p>{error ? 'Error : ' + error : 'Total messages: ' + totalMessages }</p></div>
|
|
||||||
<ul>
|
|
||||||
{messages.length > 0 ? messages.map((message, index) => (
|
|
||||||
<li key={message.id} className='list-item' style={{ '--child-index': index}} >
|
|
||||||
<h2>{message.author}</h2>
|
|
||||||
<h3>{new Date(message.timestamp).toLocaleString(navigator.language, localeOptions)}</h3>
|
|
||||||
<button onClick={() => removeMessage(message.id)}>Delete</button>
|
|
||||||
<div className='message-text'><p>{message.message}</p></div>
|
|
||||||
</li>
|
|
||||||
)) : ''}
|
|
||||||
</ul>
|
|
||||||
<div className='navigation'>
|
|
||||||
<button onClick={() => previousPage()}>Previous Page</button>
|
|
||||||
<button onClick={() => nextPage()}>Next Page</button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export default AdminPage;
|
|
|
@ -1,64 +0,0 @@
|
||||||
.login-form{
|
|
||||||
display: flex;
|
|
||||||
margin-inline: auto;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
vertical-align: center;
|
|
||||||
margin-top: 20vh;
|
|
||||||
background-color: hsla(15, 100%, 93%);
|
|
||||||
width: fit-content;
|
|
||||||
padding: 2em;
|
|
||||||
border-radius: 1em;
|
|
||||||
box-shadow: 0.05em 0.05em 0.05em hsla(15, 20%, 20%, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.fade-in{
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(10vh);
|
|
||||||
animation: fadeIn 0.5s ease-out forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeIn {
|
|
||||||
0% {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(20vh);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
form {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: auto 20ch;
|
|
||||||
grid-column-gap: 2ch;
|
|
||||||
grid-row-gap: 2ch;
|
|
||||||
align-items: center;
|
|
||||||
font-family: 'Solway', serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
form label {
|
|
||||||
font-size: 1.4em;
|
|
||||||
text-align: right;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
form input[type=text], form input[type=password] {
|
|
||||||
font-size: 1.4em;
|
|
||||||
border: 0.2em hsl(15, 100%, 50%) outset;
|
|
||||||
border-radius: 0.2em;
|
|
||||||
padding: 0.2em 0.2em;
|
|
||||||
font-weight: 300;
|
|
||||||
}
|
|
||||||
|
|
||||||
form button {
|
|
||||||
margin-top: 1em;
|
|
||||||
grid-column: 2 / 3;
|
|
||||||
padding: 1em 2em;
|
|
||||||
font-size: 1.4em;
|
|
||||||
border-radius: 0.2em;
|
|
||||||
background-color: hsl(15, 50%, 85%);
|
|
||||||
font-family: 'Solway', serif;
|
|
||||||
font-weight: 300;
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
import React, { useRef } from 'react';
|
|
||||||
import useAuth from '../hooks/useAuth.js';
|
|
||||||
|
|
||||||
import './LoginPage.css'
|
|
||||||
|
|
||||||
const LoginPage = ({onLoggedIn}) =>
|
|
||||||
{
|
|
||||||
const userInputRef = useRef(null);
|
|
||||||
const passInputRef = useRef(null);
|
|
||||||
const { authenticate, error } = useAuth();
|
|
||||||
|
|
||||||
const handleSubmit = (e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
const user = userInputRef.current.value;
|
|
||||||
const pass = passInputRef.current.value;
|
|
||||||
|
|
||||||
authenticate({user, pass})
|
|
||||||
.then(() => {
|
|
||||||
if (error === null) {
|
|
||||||
onLoggedIn(true)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return(
|
|
||||||
<>
|
|
||||||
<div className="login-form fade-in">
|
|
||||||
<form action="post" onSubmit={handleSubmit}>
|
|
||||||
<label>User: </label><input type='text' name='username' placeholder=''
|
|
||||||
ref={userInputRef}></input>
|
|
||||||
<label>Password: </label><input type='password' name='password' placeholder=''
|
|
||||||
ref={passInputRef}></input>
|
|
||||||
<button type='submit' className='login-button'>Login</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default LoginPage;
|
|
|
@ -1 +0,0 @@
|
||||||
export const API_URL = 'http://localhost:8080';
|
|
|
@ -1,19 +0,0 @@
|
||||||
import { useState } from "react";
|
|
||||||
import { post } from '../services/httpService.js'
|
|
||||||
|
|
||||||
const useAuth = () => {
|
|
||||||
const [error, setError] = useState(null);
|
|
||||||
|
|
||||||
const authenticate = async ({user, pass}) => {
|
|
||||||
try {
|
|
||||||
await post('/auth/login', {username: user, password: pass});
|
|
||||||
}
|
|
||||||
catch(err){
|
|
||||||
setError(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { authenticate, error };
|
|
||||||
}
|
|
||||||
|
|
||||||
export default useAuth;
|
|
|
@ -1,67 +0,0 @@
|
||||||
import { useState, useEffect } from "react";
|
|
||||||
import { get, del } from '../services/httpService.js'
|
|
||||||
|
|
||||||
const useMessages = () => {
|
|
||||||
const [messages, setMessages] = useState([]);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [error, setError] = useState(null);
|
|
||||||
const [totalMessages, setTotalMessages] = useState(0);
|
|
||||||
const [page, setPage] = useState(1);
|
|
||||||
const amountPerPage = 10;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchMessages = async () => {
|
|
||||||
try{
|
|
||||||
setLoading(true);
|
|
||||||
const data = await get('/message', {
|
|
||||||
headers: {
|
|
||||||
'Range': 'messages=' + ((page - 1) * amountPerPage) + '-' + (((page - 1) * amountPerPage) + amountPerPage),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const [, rangeInfo] = data.headers['content-range'].split(' ');
|
|
||||||
const [, total] = rangeInfo.split('/');
|
|
||||||
|
|
||||||
setTotalMessages(parseInt(total));
|
|
||||||
setMessages(data.data);
|
|
||||||
} catch(err) {
|
|
||||||
setError(err);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
fetchMessages();
|
|
||||||
}, [page]);
|
|
||||||
|
|
||||||
const removeMessage = async (id) => {
|
|
||||||
try{
|
|
||||||
await del('/message', {id: id});
|
|
||||||
setMessages((prevlist) => prevlist.filter(message => message.id !== id));
|
|
||||||
setTotalMessages(totalMessages - 1)
|
|
||||||
}catch(err){
|
|
||||||
setError(err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const nextPage = () => {
|
|
||||||
if(page * amountPerPage < totalMessages)
|
|
||||||
setPage((page + 1));
|
|
||||||
}
|
|
||||||
const previousPage = () => {
|
|
||||||
if (page > 1)
|
|
||||||
setPage((page - 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
messages,
|
|
||||||
totalMessages,
|
|
||||||
loading,
|
|
||||||
error,
|
|
||||||
removeMessage,
|
|
||||||
page,
|
|
||||||
nextPage,
|
|
||||||
previousPage
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default useMessages;
|
|
|
@ -1,40 +0,0 @@
|
||||||
@import url(https://fonts.bunny.net/css?family=carattere:400|solway:300,500);
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce){
|
|
||||||
html:focus-within {
|
|
||||||
scroll-behavior: auto;
|
|
||||||
}
|
|
||||||
*, *::before, *::after {
|
|
||||||
animation-duration: 0.01ms !important;
|
|
||||||
animation-iteration-count: 1 !important;
|
|
||||||
transition: none;
|
|
||||||
transition-duration: 0.01ms !important;
|
|
||||||
scroll-behavior: auto !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
*, *::before, *::after{
|
|
||||||
box-sizing: border-box;
|
|
||||||
transition: all 0.05s ease;
|
|
||||||
animation-duration: initial;
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
html:focus-within{
|
|
||||||
scroll-behavior: smooth;
|
|
||||||
}
|
|
||||||
|
|
||||||
img, picture, svg, video, canvas{
|
|
||||||
max-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background: linear-gradient(90deg, hsl(15, 80%, 90%) 0%, hsl(15, 80%, 98%) 5%, hsl(15, 80%, 98%) 95%, hsl(15, 80%, 90%) 100%);
|
|
||||||
color: hsl(15, 70%, 5%);
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
import { createRoot } from 'react-dom/client'
|
|
||||||
import './index.css'
|
|
||||||
import App from './App.jsx'
|
|
||||||
|
|
||||||
createRoot(document.getElementById('root')).render(
|
|
||||||
<App />
|
|
||||||
)
|
|
|
@ -1,21 +0,0 @@
|
||||||
import axios from 'axios';
|
|
||||||
import { API_URL } from '../environment/constants.js'
|
|
||||||
|
|
||||||
const httpClient = axios.create({
|
|
||||||
baseURL: API_URL,
|
|
||||||
withCredentials: true,
|
|
||||||
timeout: 5000,
|
|
||||||
headers: {'Content-Type': 'application/json',}
|
|
||||||
});
|
|
||||||
|
|
||||||
export const get = (endpoint, params) => {
|
|
||||||
return httpClient.get(endpoint, { headers: params.headers, data: params.data});
|
|
||||||
}
|
|
||||||
|
|
||||||
export const post = (endpoint, data) => {
|
|
||||||
return httpClient.post(endpoint, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const del = (endpoint, data) => {
|
|
||||||
return httpClient.delete(endpoint, {data: data });
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
import { defineConfig } from 'vite'
|
|
||||||
import react from '@vitejs/plugin-react'
|
|
||||||
|
|
||||||
// https://vite.dev/config/
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [react()],
|
|
||||||
})
|
|
|
@ -1,8 +0,0 @@
|
||||||
using Guestbooky.API.Enums;
|
|
||||||
|
|
||||||
namespace Guestbooky.API.Configurations;
|
|
||||||
|
|
||||||
public class APISettings
|
|
||||||
{
|
|
||||||
public required RunningEnvironment RunningEnvironment { get; set; }
|
|
||||||
}
|
|
|
@ -1,5 +1,4 @@
|
||||||
using Guestbooky.API.Configurations;
|
using Guestbooky.API.DTOs.Auth;
|
||||||
using Guestbooky.API.DTOs.Auth;
|
|
||||||
using Guestbooky.Application.UseCases.AuthenticateUser;
|
using Guestbooky.Application.UseCases.AuthenticateUser;
|
||||||
using Guestbooky.Application.UseCases.RefreshToken;
|
using Guestbooky.Application.UseCases.RefreshToken;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
@ -13,13 +12,11 @@ public class AuthController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly IMediator _mediator;
|
private readonly IMediator _mediator;
|
||||||
private readonly ILogger<AuthController> _logger;
|
private readonly ILogger<AuthController> _logger;
|
||||||
private readonly APISettings _apiSettings;
|
|
||||||
|
|
||||||
public AuthController(IMediator mediator, ILogger<AuthController> logger, APISettings apiSettings)
|
public AuthController(IMediator mediator, ILogger<AuthController> logger)
|
||||||
{
|
{
|
||||||
_mediator = mediator;
|
_mediator = mediator;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_apiSettings = apiSettings;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[ProducesResponseType(typeof(LoginResponseDto), 200)]
|
[ProducesResponseType(typeof(LoginResponseDto), 200)]
|
||||||
|
@ -38,7 +35,6 @@ public class AuthController : ControllerBase
|
||||||
if(result.IsAuthenticated)
|
if(result.IsAuthenticated)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Authentication successful. Returning LoginResponse.");
|
_logger.LogInformation("Authentication successful. Returning LoginResponse.");
|
||||||
SetJwtCookie(result.Token);
|
|
||||||
return Ok(new LoginResponseDto(result.Token, result.RefreshToken));
|
return Ok(new LoginResponseDto(result.Token, result.RefreshToken));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -84,16 +80,4 @@ public class AuthController : ControllerBase
|
||||||
return Problem($"An error occurred on the server: {e.Message}", statusCode: StatusCodes.Status500InternalServerError);
|
return Problem($"An error occurred on the server: {e.Message}", statusCode: StatusCodes.Status500InternalServerError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetJwtCookie(string token)
|
|
||||||
{
|
|
||||||
var cookieOptions = new CookieOptions
|
|
||||||
{
|
|
||||||
HttpOnly = true,
|
|
||||||
Secure = _apiSettings.RunningEnvironment == Enums.RunningEnvironment.Production,
|
|
||||||
SameSite = SameSiteMode.Strict,
|
|
||||||
Expires = DateTimeOffset.UtcNow.AddHours(2)
|
|
||||||
};
|
|
||||||
Response.Cookies.Append("token", token, cookieOptions);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
namespace Guestbooky.API.Enums;
|
|
||||||
|
|
||||||
public enum RunningEnvironment
|
|
||||||
{
|
|
||||||
Unknown = 0,
|
|
||||||
Development,
|
|
||||||
Production
|
|
||||||
}
|
|
|
@ -1,13 +1,11 @@
|
||||||
using Guestbooky.Application.DependencyInjection;
|
using Guestbooky.Application.DependencyInjection;
|
||||||
using Guestbooky.Infrastructure.DependencyInjection;
|
using Guestbooky.Infrastructure.DependencyInjection;
|
||||||
using Guestbooky.Infrastructure.Environment;
|
using Guestbooky.Infrastructure.Environment;
|
||||||
using Guestbooky.API.Configurations;
|
|
||||||
using Guestbooky.API.Enums;
|
|
||||||
using Guestbooky.API.Validations;
|
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
using Guestbooky.API.Validations;
|
||||||
|
|
||||||
namespace Guestbooky.API
|
namespace Guestbooky.API
|
||||||
{
|
{
|
||||||
|
@ -76,10 +74,9 @@ namespace Guestbooky.API
|
||||||
var corsOrigins = builder.Configuration[Constants.CORS_ORIGINS]?.Split(',') ?? Array.Empty<string>();
|
var corsOrigins = builder.Configuration[Constants.CORS_ORIGINS]?.Split(',') ?? Array.Empty<string>();
|
||||||
cfg.AddPolicy(name: "local", policy =>
|
cfg.AddPolicy(name: "local", policy =>
|
||||||
{
|
{
|
||||||
policy.WithExposedHeaders("Content-Range", "Accept-Ranges", "Set-Cookie")
|
policy.WithExposedHeaders("Content-Range", "Accept-Ranges")
|
||||||
.WithOrigins(corsOrigins)
|
.WithOrigins(corsOrigins)
|
||||||
.AllowAnyHeader()
|
.AllowAnyHeader()
|
||||||
.AllowCredentials()
|
|
||||||
.WithMethods("GET", "POST", "DELETE", "OPTIONS");
|
.WithMethods("GET", "POST", "DELETE", "OPTIONS");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -89,11 +86,7 @@ namespace Guestbooky.API
|
||||||
options.InvalidModelStateResponseFactory = InvalidModelStateResponseFactory.DefaultInvalidModelStateResponse;
|
options.InvalidModelStateResponseFactory = InvalidModelStateResponseFactory.DefaultInvalidModelStateResponse;
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.Services.AddAuthentication(o =>
|
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||||
{
|
|
||||||
o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
|
||||||
o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
|
||||||
})
|
|
||||||
.AddJwtBearer(o =>
|
.AddJwtBearer(o =>
|
||||||
{
|
{
|
||||||
o.RequireHttpsMetadata = false;
|
o.RequireHttpsMetadata = false;
|
||||||
|
@ -109,22 +102,12 @@ namespace Guestbooky.API
|
||||||
ValidateLifetime = true,
|
ValidateLifetime = true,
|
||||||
ClockSkew = TimeSpan.Zero
|
ClockSkew = TimeSpan.Zero
|
||||||
};
|
};
|
||||||
o.Events = new JwtBearerEvents
|
|
||||||
{
|
|
||||||
OnMessageReceived = context =>
|
|
||||||
{
|
|
||||||
context.Token = context.Request.Cookies["token"];
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.Services.AddInfrastructure(builder.Configuration);
|
builder.Services.AddInfrastructure(builder.Configuration);
|
||||||
builder.Services.AddApplication();
|
builder.Services.AddApplication();
|
||||||
|
|
||||||
builder.Services.AddSingleton(new APISettings(){ RunningEnvironment = GetRunningEnvironment(builder.Configuration["ASPNETCORE_ENVIRONMENT"]!) });
|
|
||||||
|
|
||||||
|
|
||||||
if (builder.Environment.IsDevelopment())
|
if (builder.Environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
|
@ -187,12 +170,5 @@ namespace Guestbooky.API
|
||||||
_ => conf.MinimumLevel.Information()
|
_ => conf.MinimumLevel.Information()
|
||||||
};
|
};
|
||||||
|
|
||||||
public static RunningEnvironment GetRunningEnvironment(string env) => env.ToUpper() switch
|
|
||||||
{
|
|
||||||
"DEVELOPMENT" => RunningEnvironment.Development,
|
|
||||||
"PRODUCTION" => RunningEnvironment.Production,
|
|
||||||
_ => RunningEnvironment.Unknown
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,7 +53,7 @@ namespace Guestbooky.Infrastructure.Persistence.Repositories
|
||||||
var messageDtos = await _messages.Find(_ => true)
|
var messageDtos = await _messages.Find(_ => true)
|
||||||
.SortBy(x => x.Timestamp)
|
.SortBy(x => x.Timestamp)
|
||||||
.Skip((int?)offset)
|
.Skip((int?)offset)
|
||||||
.Limit(10)
|
.Limit(50)
|
||||||
.ToCursorAsync(cancellationToken);
|
.ToCursorAsync(cancellationToken);
|
||||||
return messageDtos.ToEnumerable().Select(MapToDomainModel).ToArray();
|
return messageDtos.ToEnumerable().Select(MapToDomainModel).ToArray();
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue