Compare commits
4 commits
2f0d63a90f
...
93be26fac8
Author | SHA1 | Date | |
---|---|---|---|
93be26fac8 | |||
e36952fddd | |||
6f1cc7e2a6 | |||
eaecace48d |
26 changed files with 5047 additions and 7 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 <small>(which is a WIP so you'll have to make do with a db manager)</small></p>
|
||||
<p align="center">A simple yet somehow overdesigned guestbook system featuring a simple control panel</p>
|
||||
|
||||
<p align="center"> This is phase I of the personal backscratchers project.</p>
|
||||
|
||||
|
@ -29,6 +29,8 @@
|
|||
- [📑 Documentation ](#-documentation-)
|
||||
- [🏁 Getting Started ](#-getting-started-)
|
||||
- [🕸️ Prerequisites](#️-prerequisites)
|
||||
- [Backend:](#backend)
|
||||
- [Admin panel:](#admin-panel)
|
||||
- [🚀 Deployment ](#-deployment-)
|
||||
- [⛏️ Built Using ](#️-built-using-)
|
||||
- [✍️ Authors ](#️-authors-)
|
||||
|
@ -49,6 +51,8 @@ These instructions will get you a copy of the project up and running on your loc
|
|||
|
||||
## 🕸️ Prerequisites
|
||||
|
||||
### Backend:
|
||||
|
||||
For running it locally:
|
||||
- .NET 8.0
|
||||
- A running instance of MongoDB
|
||||
|
@ -84,6 +88,12 @@ You will be able to see in `build/docker-compose.public.yml` that the applicatio
|
|||
> [!TIP]
|
||||
> 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>
|
||||
|
||||
Use `docker-compose.public.yml` as a basis. it should create the image for you and start running.
|
||||
|
@ -92,6 +102,7 @@ Use `docker-compose.public.yml` as a basis. it should create the image for you a
|
|||
|
||||
- [MongoDB](https://www.mongodb.com/) - Database
|
||||
- [.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
|
||||
|
||||
## ✍️ Authors <a name = "authors"></a>
|
||||
|
|
|
@ -43,3 +43,4 @@ ___
|
|||
- 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.
|
||||
|
||||
- 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
Normal file
24
src/Guestbooky-admin/.gitignore
vendored
Normal file
|
@ -0,0 +1,24 @@
|
|||
# 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?
|
38
src/Guestbooky-admin/eslint.config.js
Normal file
38
src/Guestbooky-admin/eslint.config.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
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 },
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
13
src/Guestbooky-admin/index.html
Normal file
13
src/Guestbooky-admin/index.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
<!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
Normal file
4391
src/Guestbooky-admin/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
30
src/Guestbooky-admin/package.json
Normal file
30
src/Guestbooky-admin/package.json
Normal file
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
24
src/Guestbooky-admin/src/App.css
Normal file
24
src/Guestbooky-admin/src/App.css
Normal file
|
@ -0,0 +1,24 @@
|
|||
.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;
|
||||
}
|
24
src/Guestbooky-admin/src/App.jsx
Normal file
24
src/Guestbooky-admin/src/App.jsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
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
|
BIN
src/Guestbooky-admin/src/assets/guestbooky.png
Normal file
BIN
src/Guestbooky-admin/src/assets/guestbooky.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
111
src/Guestbooky-admin/src/components/AdminPage.css
Normal file
111
src/Guestbooky-admin/src/components/AdminPage.css
Normal file
|
@ -0,0 +1,111 @@
|
|||
@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;
|
||||
}
|
51
src/Guestbooky-admin/src/components/AdminPage.jsx
Normal file
51
src/Guestbooky-admin/src/components/AdminPage.jsx
Normal file
|
@ -0,0 +1,51 @@
|
|||
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;
|
64
src/Guestbooky-admin/src/components/LoginPage.css
Normal file
64
src/Guestbooky-admin/src/components/LoginPage.css
Normal file
|
@ -0,0 +1,64 @@
|
|||
.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;
|
||||
}
|
40
src/Guestbooky-admin/src/components/LoginPage.jsx
Normal file
40
src/Guestbooky-admin/src/components/LoginPage.jsx
Normal file
|
@ -0,0 +1,40 @@
|
|||
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
src/Guestbooky-admin/src/environment/constants.js
Normal file
1
src/Guestbooky-admin/src/environment/constants.js
Normal file
|
@ -0,0 +1 @@
|
|||
export const API_URL = 'http://localhost:8080';
|
19
src/Guestbooky-admin/src/hooks/useAuth.js
Normal file
19
src/Guestbooky-admin/src/hooks/useAuth.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
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;
|
67
src/Guestbooky-admin/src/hooks/useMessages.js
Normal file
67
src/Guestbooky-admin/src/hooks/useMessages.js
Normal file
|
@ -0,0 +1,67 @@
|
|||
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;
|
40
src/Guestbooky-admin/src/index.css
Normal file
40
src/Guestbooky-admin/src/index.css
Normal file
|
@ -0,0 +1,40 @@
|
|||
@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%);
|
||||
}
|
7
src/Guestbooky-admin/src/main.jsx
Normal file
7
src/Guestbooky-admin/src/main.jsx
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.jsx'
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<App />
|
||||
)
|
21
src/Guestbooky-admin/src/services/httpService.js
Normal file
21
src/Guestbooky-admin/src/services/httpService.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
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 });
|
||||
}
|
7
src/Guestbooky-admin/vite.config.js
Normal file
7
src/Guestbooky-admin/vite.config.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
|
@ -0,0 +1,8 @@
|
|||
using Guestbooky.API.Enums;
|
||||
|
||||
namespace Guestbooky.API.Configurations;
|
||||
|
||||
public class APISettings
|
||||
{
|
||||
public required RunningEnvironment RunningEnvironment { get; set; }
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
using Guestbooky.API.DTOs.Auth;
|
||||
using Guestbooky.API.Configurations;
|
||||
using Guestbooky.API.DTOs.Auth;
|
||||
using Guestbooky.Application.UseCases.AuthenticateUser;
|
||||
using Guestbooky.Application.UseCases.RefreshToken;
|
||||
using MediatR;
|
||||
|
@ -12,11 +13,13 @@ public class AuthController : ControllerBase
|
|||
{
|
||||
private readonly IMediator _mediator;
|
||||
private readonly ILogger<AuthController> _logger;
|
||||
private readonly APISettings _apiSettings;
|
||||
|
||||
public AuthController(IMediator mediator, ILogger<AuthController> logger)
|
||||
public AuthController(IMediator mediator, ILogger<AuthController> logger, APISettings apiSettings)
|
||||
{
|
||||
_mediator = mediator;
|
||||
_logger = logger;
|
||||
_apiSettings = apiSettings;
|
||||
}
|
||||
|
||||
[ProducesResponseType(typeof(LoginResponseDto), 200)]
|
||||
|
@ -35,6 +38,7 @@ public class AuthController : ControllerBase
|
|||
if(result.IsAuthenticated)
|
||||
{
|
||||
_logger.LogInformation("Authentication successful. Returning LoginResponse.");
|
||||
SetJwtCookie(result.Token);
|
||||
return Ok(new LoginResponseDto(result.Token, result.RefreshToken));
|
||||
}
|
||||
else
|
||||
|
@ -80,4 +84,16 @@ public class AuthController : ControllerBase
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
namespace Guestbooky.API.Enums;
|
||||
|
||||
public enum RunningEnvironment
|
||||
{
|
||||
Unknown = 0,
|
||||
Development,
|
||||
Production
|
||||
}
|
|
@ -1,11 +1,13 @@
|
|||
using Guestbooky.Application.DependencyInjection;
|
||||
using Guestbooky.Infrastructure.DependencyInjection;
|
||||
using Guestbooky.Infrastructure.Environment;
|
||||
using Guestbooky.API.Configurations;
|
||||
using Guestbooky.API.Enums;
|
||||
using Guestbooky.API.Validations;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.Text;
|
||||
using Serilog;
|
||||
using Guestbooky.API.Validations;
|
||||
|
||||
namespace Guestbooky.API
|
||||
{
|
||||
|
@ -74,9 +76,10 @@ namespace Guestbooky.API
|
|||
var corsOrigins = builder.Configuration[Constants.CORS_ORIGINS]?.Split(',') ?? Array.Empty<string>();
|
||||
cfg.AddPolicy(name: "local", policy =>
|
||||
{
|
||||
policy.WithExposedHeaders("Content-Range", "Accept-Ranges")
|
||||
policy.WithExposedHeaders("Content-Range", "Accept-Ranges", "Set-Cookie")
|
||||
.WithOrigins(corsOrigins)
|
||||
.AllowAnyHeader()
|
||||
.AllowCredentials()
|
||||
.WithMethods("GET", "POST", "DELETE", "OPTIONS");
|
||||
});
|
||||
});
|
||||
|
@ -86,7 +89,11 @@ namespace Guestbooky.API
|
|||
options.InvalidModelStateResponseFactory = InvalidModelStateResponseFactory.DefaultInvalidModelStateResponse;
|
||||
});
|
||||
|
||||
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
builder.Services.AddAuthentication(o =>
|
||||
{
|
||||
o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
})
|
||||
.AddJwtBearer(o =>
|
||||
{
|
||||
o.RequireHttpsMetadata = false;
|
||||
|
@ -102,12 +109,22 @@ namespace Guestbooky.API
|
|||
ValidateLifetime = true,
|
||||
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.AddApplication();
|
||||
|
||||
builder.Services.AddSingleton(new APISettings(){ RunningEnvironment = GetRunningEnvironment(builder.Configuration["ASPNETCORE_ENVIRONMENT"]!) });
|
||||
|
||||
|
||||
if (builder.Environment.IsDevelopment())
|
||||
{
|
||||
|
@ -170,5 +187,12 @@ namespace Guestbooky.API
|
|||
_ => 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)
|
||||
.SortBy(x => x.Timestamp)
|
||||
.Skip((int?)offset)
|
||||
.Limit(50)
|
||||
.Limit(10)
|
||||
.ToCursorAsync(cancellationToken);
|
||||
return messageDtos.ToEnumerable().Select(MapToDomainModel).ToArray();
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue