1
Fork 0
mirror of https://github.com/pds-nest/nest.git synced 2024-11-22 13:04:19 +00:00

Merge remote-tracking branch 'origin/main' into main

This commit is contained in:
Lorenzo Balugani 2021-05-12 17:27:37 +02:00
commit c4f7d65820
28 changed files with 617 additions and 67 deletions

View file

@ -13,6 +13,7 @@
</value> </value>
</option> </option>
</inspection_tool> </inspection_tool>
<inspection_tool class="ExceptionCaughtLocallyJS" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="HtmlUnknownAttribute" enabled="true" level="WARNING" enabled_by_default="true"> <inspection_tool class="HtmlUnknownAttribute" enabled="true" level="WARNING" enabled_by_default="true">
<option name="myValues"> <option name="myValues">
<value> <value>
@ -30,7 +31,7 @@
<inspection_tool class="LessResolvedByNameOnly" enabled="true" level="WARNING" enabled_by_default="true" /> <inspection_tool class="LessResolvedByNameOnly" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="LessUnresolvedMixin" enabled="true" level="ERROR" enabled_by_default="true" /> <inspection_tool class="LessUnresolvedMixin" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="LessUnresolvedVariable" enabled="true" level="ERROR" enabled_by_default="true" /> <inspection_tool class="LessUnresolvedVariable" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="LongLine" enabled="true" level="WEAK WARNING" enabled_by_default="true" /> <inspection_tool class="LongLine" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="PoetryPackageVersion" enabled="true" level="SERVER PROBLEM" enabled_by_default="true" /> <inspection_tool class="PoetryPackageVersion" enabled="true" level="SERVER PROBLEM" enabled_by_default="true" />
<inspection_tool class="ProblematicWhitespace" enabled="true" level="WEAK WARNING" enabled_by_default="true" /> <inspection_tool class="ProblematicWhitespace" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="PyAbstractClassInspection" enabled="true" level="ERROR" enabled_by_default="true" /> <inspection_tool class="PyAbstractClassInspection" enabled="true" level="ERROR" enabled_by_default="true" />

View file

@ -1,7 +1,6 @@
from flask import Flask from flask import Flask
from flask_cors import CORS as FlaskCORS from flask_cors import CORS as FlaskCORS
from flask_jwt_extended import JWTManager as FlaskJWTManager from flask_jwt_extended import JWTManager as FlaskJWTManager
from flask_sqlalchemy import SQLAlchemy as FlaskSQLAlchemy
from werkzeug.middleware.proxy_fix import ProxyFix as MiddlewareProxyFix from werkzeug.middleware.proxy_fix import ProxyFix as MiddlewareProxyFix
from . import database, routes, gestione, swagger from . import database, routes, gestione, swagger
@ -107,5 +106,4 @@ app.register_error_handler(
# --- REVERSE PROXY --- # --- REVERSE PROXY ---
if not __debug__: rp_app = MiddlewareProxyFix(app=app, x_for=1, x_proto=0, x_host=1, x_port=0, x_prefix=0)
app = MiddlewareProxyFix(app=app, x_for=1, x_proto=0, x_host=1, x_port=0, x_prefix=0)

View file

@ -133,7 +133,7 @@ def page_user(email):
except Exception: except Exception:
ext.session.rollback() ext.session.rollback()
return json_error("Could not delete the user."), 500 return json_error("Could not delete the user."), 500
return json_success("The user has been deleted."), 204 return json_success(""), 204 # "The user has been deleted."
elif request.method == "PATCH": elif request.method == "PATCH":
if not email == user.email and not user.isAdmin: if not email == user.email and not user.isAdmin:
return json_error("Thou art not authorized."), 403 return json_error("Thou art not authorized."), 403
@ -143,4 +143,4 @@ def page_user(email):
if request.json.get("password"): if request.json.get("password"):
target.password = gen_password(request.json.get("password")) target.password = gen_password(request.json.get("password"))
ext.session.commit() ext.session.commit()
return json_success(target.to_json()), 204 return json_success(target.to_json()), 200 # 204

View file

@ -4,4 +4,7 @@ Pytest configuration file.
Import global fixtures here. Import global fixtures here.
""" """
from fixtures.flask_client import flask_client, admin_access_token, admin_headers, user_access_token, user_headers, user_exists from fixtures.flask_client import flask_client, \
admin_access_token, admin_headers, \
user_access_token, user_headers, user_exists, \
repository_exists

View file

@ -64,6 +64,13 @@ def user_exists(admin_headers, flask_client):
}) })
@pytest.fixture(scope="package")
def repository_exists(admin_headers, flask_client):
flask_client.post(f'/api/v1/repository/', headers=admin_headers, json={
'name': 'repo1'
})
@pytest.fixture(scope="package") @pytest.fixture(scope="package")
def user_access_token(flask_client, user_exists): def user_access_token(flask_client, user_exists):
response = flask_client.post("/api/v1/login", json={ response = flask_client.post("/api/v1/login", json={

View file

@ -16,44 +16,45 @@ class TestUserGet:
assert r.json["result"] == "failure" assert r.json["result"] == "failure"
assert r.json["msg"] == "Could not locate the user." assert r.json["msg"] == "Could not locate the user."
# ritorna i dati di tutti gli utenti registrati # ritorna i dati di tutti gli utenti registrati
class TestUserGetAll: class TestUserGetAll:
def test_for_success(self, flask_client: Client, admin_headers): def test_for_success(self, flask_client: Client, admin_headers):
r = flask_client.get(f'/api/v1/users/', headers=admin_headers) r = flask_client.get(f'/api/v1/users/', headers=admin_headers)
assert b'success' in r.data assert r.json["result"] == "success"
def test_for_failure(self, flask_client: Client, user_headers): def test_for_failure(self, flask_client: Client, user_headers):
r = flask_client.get(f'/api/v1/users/', headers=user_headers) r = flask_client.get(f'/api/v1/users/', headers=user_headers)
assert b'failure' in r.data assert r.json["result"] == "failure"
class TestUserAdd: class TestUserAdd:
def test_for_success(self, flask_client: Client, admin_headers): def test_valid_user(self, flask_client: Client, admin_headers):
r = flask_client.post(f'/api/v1/users/', headers=admin_headers, json={ r = flask_client.post(f'/api/v1/users/', headers=admin_headers, json={
'email': 'utente1_test@nest.com', 'email': 'utente1_test@nest.com',
'password': 'password', 'password': 'password',
'username': 'utente_test' 'username': 'utente_test'
}) })
assert b'success' in r.data assert r.json["result"] == "success"
def test_for_failure(self, flask_client: Client, user_headers): def test_existing_user(self, flask_client: Client, user_headers):
r = flask_client.post(f'/api/v1/users/', headers=user_headers, json={ r = flask_client.post(f'/api/v1/users/', headers=user_headers, json={
'email': 'utente_test@nest.com', 'email': 'utente_test@nest.com',
'password': 'password', 'password': 'password',
'username': 'utente_test' 'username': 'utente_test'
}) })
assert b'failure' in r.data assert r.json["result"] == "failure"
class TestUserDelete: class TestUserDelete:
def test_for_success(self, flask_client: Client, admin_headers): def test_valid_user(self, flask_client: Client, admin_headers):
r = flask_client.delete(f'/api/v1/users/utente_test@nest.com', headers=admin_headers) r = flask_client.delete(f'/api/v1/users/utente1_test@nest.com', headers=admin_headers)
assert b'success' in r.data assert r.status_code == 204
# the admin tries to commit suicide # the admin tries to commit suicide
def test_for_failure(self, flask_client: Client, admin_headers): def test_himself(self, flask_client: Client, admin_headers):
r = flask_client.delete(f'/api/v1/users/admin@admin.com', headers=admin_headers) r = flask_client.delete(f'/api/v1/users/admin@admin.com', headers=admin_headers)
assert b'failure' in r.data assert r.json["result"] == "failure"
class TestUserPatch: class TestUserPatch:
@ -61,11 +62,10 @@ class TestUserPatch:
r = flask_client.patch(f'/api/v1/users/admin@admin.com', headers=admin_headers, json={ r = flask_client.patch(f'/api/v1/users/admin@admin.com', headers=admin_headers, json={
'username': 'admin_patched' 'username': 'admin_patched'
}) })
assert b'success' in r.data assert r.json["result"] == "success"
# FIXME AssertionError in flask_client at line 63. Il test non riesce ad andare a buon fine def test_not_authorized(self, flask_client: Client, user_headers):
def test_for_failure(self, flask_client: Client, user_headers):
r = flask_client.patch(f'/api/v1/users/admin@admin.com', headers=user_headers, json={ r = flask_client.patch(f'/api/v1/users/admin@admin.com', headers=user_headers, json={
'username': 'admin_patched' 'username': 'admin_patched'
}) })
assert b'failure' in r.data assert r.json["result"] == "failure"

View file

@ -0,0 +1,71 @@
from flask.testing import Client
'''A file that contains tests of classes and methods for all the requests concerning an user.'''
class TestRepositoryGetAll:
def test_get_all_user_repositories(self, flask_client: Client, user_headers):
r = flask_client.get(f'/api/v1/repositories/', headers=user_headers,
json={'owner_id': 'utente_test@nest.com', 'isActive': False})
assert r.json["result"] == "success"
# assert r.json["data"]["email"] == "admin@admin.com"
# assert r.json["data"]["isAdmin"] is True
def test_get_all_admin_repositories(self, flask_client: Client, admin_headers):
r = flask_client.get(f'/api/v1/repositories/', headers=admin_headers,
json={'owner_id': 'admin@admin.com', 'isActive': False})
assert r.json["result"] == "success"
# assert r.json["data"]["email"] == "admin@admin.com"
# assert r.json["data"]["isAdmin"] is True
'''
def test_non_existing_user(self, flask_client: Client, admin_headers):
r = flask_client.get(f'/api/v1/users/ciccio@dev.com', headers=admin_headers)
assert r.json["result"] == "failure"
assert r.json["msg"] == "Could not locate the user."
class TestUserAdd:
def test_for_success(self, flask_client: Client, admin_headers):
r = flask_client.post(f'/api/v1/users/', headers=admin_headers, json={
'email': 'utente1_test@nest.com',
'password': 'password',
'username': 'utente_test'
})
assert b'success' in r.data
def test_for_failure(self, flask_client: Client, user_headers):
r = flask_client.post(f'/api/v1/users/', headers=user_headers, json={
'email': 'utente_test@nest.com',
'password': 'password',
'username': 'utente_test'
})
assert b'failure' in r.data
class TestUserDelete:
def test_for_success(self, flask_client: Client, admin_headers):
r = flask_client.delete(f'/api/v1/users/utente_test@nest.com', headers=admin_headers)
assert b'success' in r.data
# the admin tries to commit suicide
def test_for_failure(self, flask_client: Client, admin_headers):
r = flask_client.delete(f'/api/v1/users/admin@admin.com', headers=admin_headers)
assert b'failure' in r.data
class TestUserPatch:
def test_for_success(self, flask_client: Client, admin_headers):
r = flask_client.patch(f'/api/v1/users/admin@admin.com', headers=admin_headers, json={
'username': 'admin_patched'
})
assert b'success' in r.data
# FIXME AssertionError in flask_client at line 63. Il test non riesce ad andare a buon fine
def test_for_failure(self, flask_client: Client, user_headers):
r = flask_client.patch(f'/api/v1/users/admin@admin.com', headers=user_headers, json={
'username': 'admin_patched'
})
assert b'failure' in r.data
'''

View file

@ -222,6 +222,20 @@ python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*"
[package.extras] [package.extras]
docs = ["sphinx"] docs = ["sphinx"]
[[package]]
name = "gunicorn"
version = "20.1.0"
description = "WSGI HTTP Server for UNIX"
category = "main"
optional = false
python-versions = ">=3.5"
[package.extras]
eventlet = ["eventlet (>=0.24.1)"]
gevent = ["gevent (>=1.4.0)"]
setproctitle = ["setproctitle"]
tornado = ["tornado (>=0.2)"]
[[package]] [[package]]
name = "idna" name = "idna"
version = "2.10" version = "2.10"
@ -798,7 +812,7 @@ watchdog = ["watchdog"]
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.8.5" python-versions = "^3.8.5"
content-hash = "9c4f3517887f1d12fc4196119a7d3398fa61a5214809fcfd1a324d87ca8baeaf" content-hash = "1ac47344a839aae5a929d7ce9914f62c76c1a591bc88dabb819fe15814f40031"
[metadata.files] [metadata.files]
alabaster = [ alabaster = [
@ -963,6 +977,10 @@ greenlet = [
{ file = "greenlet-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:aa4230234d02e6f32f189fd40b59d5a968fe77e80f59c9c933384fe8ba535535" }, { file = "greenlet-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:aa4230234d02e6f32f189fd40b59d5a968fe77e80f59c9c933384fe8ba535535" },
{ file = "greenlet-1.1.0.tar.gz", hash = "sha256:c87df8ae3f01ffb4483c796fe1b15232ce2b219f0b18126948616224d3f658ee" }, { file = "greenlet-1.1.0.tar.gz", hash = "sha256:c87df8ae3f01ffb4483c796fe1b15232ce2b219f0b18126948616224d3f658ee" },
] ]
gunicorn = [
{ file = "gunicorn-20.1.0-py3-none-any.whl", hash = "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e" },
{ file = "gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8" },
]
idna = [ idna = [
{ file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" }, { file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" },
{ file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6" }, { file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6" },

View file

@ -28,6 +28,7 @@ openapi-spec-validator = "^0.3.0"
flask-swagger-ui = "^3.36.0" flask-swagger-ui = "^3.36.0"
tweepy = "^3.10.0" tweepy = "^3.10.0"
nltk = "^3.6.2" nltk = "^3.6.2"
gunicorn = "^20.1.0"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
pytest = "^6.2.3" pytest = "^6.2.3"

View file

@ -0,0 +1,16 @@
[Unit]
Description=N.E.S.T. Backend Webserver
Wants=network-online.target postgresql.service
After=network-online.target nss-lookup.target postgresql.service
[Service]
Type=exec
User=nest
Group=nest
# Cambia questa riga alla tua directory di installazione
WorkingDirectory=/srv/nest
# Cambia questa riga al tuo venv
ExecStart=/srv/nest/venv/bin/gunicorn -b 127.0.0.1:5000 --env "FLASK_CONFIG=/srv/nest/config.py" nest_backend.app:rp_app
[Install]
WantedBy=multi-user.target

View file

@ -8,16 +8,20 @@ import PageSandbox from "./routes/PageSandbox"
import PageDashboard from "./routes/PageDashboard" import PageDashboard from "./routes/PageDashboard"
import PageRoot from "./routes/PageRoot" import PageRoot from "./routes/PageRoot"
import PageEdit from "./routes/PageEdit" import PageEdit from "./routes/PageEdit"
import PageUsers from "./routes/PageUsers"
export default function PageSwitcher({ ...props }) { export default function PageSwitcher({ ...props }) {
return ( return (
<Switch {...props}> <Switch {...props}>
<Route path={"/repositories/:id/edit"} exact={true}>
<PageEdit/>
</Route>
<Route path={"/login"} exact={true}> <Route path={"/login"} exact={true}>
<PageLogin/> <PageLogin/>
</Route> </Route>
<Route path={"/repositories/:id/edit"} exact={true}> <Route path={"/users"} exact={true}>
<PageEdit/> <PageUsers/>
</Route> </Route>
<Route path={"/repositories"} exact={true}> <Route path={"/repositories"} exact={true}>
<PageRepositories/> <PageRepositories/>

View file

@ -11,6 +11,10 @@
cursor: pointer; cursor: pointer;
} }
.Button[disabled] {
opacity: 0.5;
}
.Button:focus-visible { .Button:focus-visible {
outline: 4px solid var(--outline); outline: 4px solid var(--outline);
} }

View file

@ -10,6 +10,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
* @param icon - The icon of the summary. * @param icon - The icon of the summary.
* @param title - The title of the summary. * @param title - The title of the summary.
* @param subtitle - The subtitle of the summary. * @param subtitle - The subtitle of the summary.
* @param onClick - A function to call when the summary is clicked.
* @param upperLabel - The label for the upper value. * @param upperLabel - The label for the upper value.
* @param upperValue - The upper value. * @param upperValue - The upper value.
* @param lowerLabel - The label for the lower value. * @param lowerLabel - The label for the lower value.
@ -21,11 +22,11 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
* @constructor * @constructor
*/ */
export default function Summary( export default function Summary(
{ icon, title, subtitle, upperLabel, upperValue, lowerLabel, lowerValue, buttons, className, ...props }, { icon, title, subtitle, onClick, upperLabel, upperValue, lowerLabel, lowerValue, buttons, className, ...props },
) { ) {
return ( return (
<div className={classNames(Style.Summary, className)} {...props}> <div className={classNames(Style.Summary, className)} {...props}>
<div className={Style.Left}> <div className={classNames(Style.Left, onClick ? Style.Clickable : null)} onClick={onClick}>
<div className={Style.IconContainer}> <div className={Style.IconContainer}>
<FontAwesomeIcon icon={icon}/> <FontAwesomeIcon icon={icon}/>
</div> </div>

View file

@ -7,6 +7,13 @@
display: flex; display: flex;
} }
.Clickable {
cursor: pointer;
}
.Clickable:hover {
filter: brightness(110%);
}
.Left { .Left {
width: 250px; width: 250px;

View file

@ -1,7 +1,7 @@
import React, { useContext } from "react" import React, { useContext } from "react"
import BoxFull from "../base/BoxFull" import BoxFull from "../base/BoxFull"
import SummaryRepository from "./SummaryRepository" import SummaryRepository from "./SummaryRepository"
import { faSearch } from "@fortawesome/free-solid-svg-icons" import { faFolderOpen } from "@fortawesome/free-solid-svg-icons"
import ContextUser from "../../contexts/ContextUser" import ContextUser from "../../contexts/ContextUser"
@ -23,7 +23,7 @@ export default function BoxRepositoriesActive({ repositories, refresh, ...props
<SummaryRepository <SummaryRepository
key={repo["id"]} key={repo["id"]}
repo={repo} repo={repo}
icon={faSearch} icon={faFolderOpen}
refresh={refresh} refresh={refresh}
canArchive={true} canArchive={true}
canEdit={true} canEdit={true}

View file

@ -2,7 +2,7 @@ import React, { useContext } from "react"
import BoxFull from "../base/BoxFull" import BoxFull from "../base/BoxFull"
import ContextUser from "../../contexts/ContextUser" import ContextUser from "../../contexts/ContextUser"
import SummaryRepository from "./SummaryRepository" import SummaryRepository from "./SummaryRepository"
import { faSearch } from "@fortawesome/free-solid-svg-icons" import { faFolder } from "@fortawesome/free-solid-svg-icons"
/** /**
@ -23,9 +23,9 @@ export default function BoxRepositoriesArchived({ repositories, refresh, ...prop
<SummaryRepository <SummaryRepository
key={repo["id"]} key={repo["id"]}
repo={repo} repo={repo}
icon={faSearch} icon={faFolder}
refresh={refresh} refresh={refresh}
canArchive={true} canArchive={false}
canEdit={false} canEdit={false}
canDelete={repo["owner"]["username"] === user["username"]} canDelete={repo["owner"]["username"] === user["username"]}
/> />

View file

@ -0,0 +1,72 @@
import React, { useCallback, useState } from "react"
import FormLabelled from "../base/FormLabelled"
import FormLabel from "../base/formparts/FormLabel"
import InputWithIcon from "../base/InputWithIcon"
import { faEnvelope, faKey, faPlus, faUser } from "@fortawesome/free-solid-svg-icons"
import FormButton from "../base/formparts/FormButton"
import BoxFull from "../base/BoxFull"
import FormAlert from "../base/formparts/FormAlert"
export default function BoxUserCreate({ createUser, running, ...props }) {
const [username, setUsername] = useState("")
const [email, setEmail] = useState("")
const [password, setPassword] = useState("")
const [error, setError] = useState(undefined)
const onButtonClick = useCallback(
async () => {
const result = await createUser({
"email": email,
"username": username,
"password": password,
})
setError(result.error)
},
[createUser, email, username, password],
)
return (
<BoxFull header={"Crea utente"} {...props}>
<FormLabelled>
<FormLabel text={"Username"}>
<InputWithIcon
icon={faUser}
type={"text"}
value={username}
onChange={event => setUsername(event.target.value)}
/>
</FormLabel>
<FormLabel text={"Email"}>
<InputWithIcon
icon={faEnvelope}
type={"text"}
value={email}
onChange={event => setEmail(event.target.value)}
/>
</FormLabel>
<FormLabel text={"Password"}>
<InputWithIcon
icon={faKey}
type={"password"}
value={password}
onChange={event => setPassword(event.target.value)}
/>
</FormLabel>
{error ?
<FormAlert color={"Red"}>
{error.toString()}
</FormAlert>
: null}
<FormButton
color={"Green"}
icon={faPlus}
onClick={onButtonClick}
disabled={running}
>
Create
</FormButton>
</FormLabelled>
</BoxFull>
)
}

View file

@ -0,0 +1,22 @@
import React from "react"
import Loading from "../base/Loading"
import BoxFullScrollable from "../base/BoxFullScrollable"
import SummaryUser from "./SummaryUser"
export default function BoxUserList({ users, destroyUser, running, ...props }) {
let contents
if(users === null) {
contents = <Loading/>
}
else {
contents = users.map(user =>
<SummaryUser key={user["email"]} destroyUser={destroyUser} running={running} user={user}/>)
}
return (
<BoxFullScrollable header={"Elenco utenti"} {...props}>
{contents}
</BoxFullScrollable>
)
}

View file

@ -1,9 +1,17 @@
import React, { Fragment, useContext } from "react" import React, { useContext } from "react"
import Style from "./Sidebar.module.css" import Style from "./Sidebar.module.css"
import classNames from "classnames" import classNames from "classnames"
import Logo from "../interactive/Logo" import Logo from "../interactive/Logo"
import ButtonSidebar from "../base/ButtonSidebar" import ButtonSidebar from "../base/ButtonSidebar"
import { faCog, faExclamationTriangle, faFolder, faHome, faKey, faWrench } from "@fortawesome/free-solid-svg-icons" import {
faCog,
faExclamationTriangle,
faFolder,
faHome,
faKey,
faUserCog,
faWrench,
} from "@fortawesome/free-solid-svg-icons"
import ContextUser from "../../contexts/ContextUser" import ContextUser from "../../contexts/ContextUser"
@ -24,16 +32,24 @@ export default function Sidebar({ className, ...props }) {
<Logo/> <Logo/>
{ {
user ? user ?
<Fragment> <>
<ButtonSidebar to={"/dashboard"} icon={faHome}>Dashboard</ButtonSidebar> <ButtonSidebar to={"/dashboard"} icon={faHome}>Dashboard</ButtonSidebar>
<ButtonSidebar to={"/repositories"} icon={faFolder}>Repositories</ButtonSidebar> <ButtonSidebar to={"/repositories"} icon={faFolder}>Repositories</ButtonSidebar>
<ButtonSidebar to={"/alerts"} icon={faExclamationTriangle}>Alerts</ButtonSidebar> <ButtonSidebar to={"/alerts"} icon={faExclamationTriangle}>Alerts</ButtonSidebar>
<ButtonSidebar to={"/settings"} icon={faCog}>Settings</ButtonSidebar> <ButtonSidebar to={"/settings"} icon={faCog}>Settings</ButtonSidebar>
</Fragment> </>
: :
<Fragment> <>
<ButtonSidebar to={"/login"} icon={faKey}>Login</ButtonSidebar> <ButtonSidebar to={"/login"} icon={faKey}>Login</ButtonSidebar>
</Fragment> </>
}
{
user && user.isAdmin ?
<>
<ButtonSidebar to={"/users"} icon={faUserCog}>Utenti</ButtonSidebar>
</>
:
null
} }
{ {
process.env.NODE_ENV === "development" ? process.env.NODE_ENV === "development" ?

View file

@ -26,9 +26,12 @@ export default function SummaryRepository(
const { fetchDataAuth } = useContext(ContextUser) const { fetchDataAuth } = useContext(ContextUser)
const history = useHistory() const history = useHistory()
const { fetchNow: archiveThis } = useBackend(fetchDataAuth, "PATCH", `/api/v1/repositories/${repo.id}`, { "close": true }) const { fetchNow: archiveThis } = useBackend(fetchDataAuth, "PATCH", `/api/v1/repositories/${repo.id}`, { "close": true })
const { fetchNow: unarchiveThis } = useBackend(fetchDataAuth, "PATCH", `/api/v1/repositories/${repo.id}`, { "open": true })
const { fetchNow: deletThis } = useBackend(fetchDataAuth, "DELETE", `/api/v1/repositories/${repo.id}`) const { fetchNow: deletThis } = useBackend(fetchDataAuth, "DELETE", `/api/v1/repositories/${repo.id}`)
const onRepoClick = () => {
history.push(`/repositories/${repo.id}`)
}
const onEditClick = () => { const onEditClick = () => {
history.push(`/repositories/${repo.id}/edit`) history.push(`/repositories/${repo.id}/edit`)
} }
@ -38,11 +41,6 @@ export default function SummaryRepository(
await refresh() await refresh()
} }
const onUnarchiveClick = async () => {
await unarchiveThis()
await refresh()
}
const onDeleteClick = async () => { const onDeleteClick = async () => {
await deletThis() await deletThis()
await refresh() await refresh()
@ -71,9 +69,9 @@ export default function SummaryRepository(
<Button <Button
color={"Grey"} color={"Grey"}
icon={faArchive} icon={faArchive}
onClick={repo.is_active ? onArchiveClick : onUnarchiveClick} onClick={onArchiveClick}
> >
{repo.is_active ? "Archive" : "Unarchive"} {"Archive"}
</Button> </Button>
: null} : null}
</> </>
@ -82,10 +80,11 @@ export default function SummaryRepository(
<Summary <Summary
icon={repo.is_active ? faFolderOpen : faFolder} icon={repo.is_active ? faFolderOpen : faFolder}
title={repo.name} title={repo.name}
subtitle={repo.author} subtitle={repo.owner ? repo.owner.username : null}
upperLabel={"Start"} onClick={onRepoClick}
upperLabel={"Created"}
upperValue={repo.start ? new Date(repo.start).toLocaleString() : null} upperValue={repo.start ? new Date(repo.start).toLocaleString() : null}
lowerLabel={"End"} lowerLabel={"Archived"}
lowerValue={repo.end ? new Date(repo.end).toLocaleString() : null} lowerValue={repo.end ? new Date(repo.end).toLocaleString() : null}
buttons={buttons} buttons={buttons}
{...props} {...props}

View file

@ -0,0 +1,39 @@
import React, { useContext } from "react"
import Summary from "../base/Summary"
import { faStar, faTrash, faUser } from "@fortawesome/free-solid-svg-icons"
import Button from "../base/Button"
import ContextUser from "../../contexts/ContextUser"
export default function SummaryUser({ user, destroyUser, running, ...props }) {
const { user: loggedUser } = useContext(ContextUser)
const buttons = <>
{loggedUser.email !== user.email ?
<Button
color={"Red"}
icon={faTrash}
onClick={async event => {
event.stopPropagation()
// TODO: Errors are not caught here. Where should they be displayed?
await destroyUser(user["email"])
}}
disabled={running}
>
Delete
</Button>
: null}
</>
return (
<Summary
icon={user.isAdmin ? faStar : faUser}
title={user.username}
subtitle={user.email}
upperLabel={"Tipo"}
upperValue={user.isAdmin ? "Amministratore" : "Utente"}
buttons={buttons}
{...props}
/>
)
}

View file

@ -55,28 +55,28 @@ export default function RepositoryEditor({
() => { () => {
return { return {
"conditions": _conditions, "conditions": _conditions,
"end": _end, "end": null,
"evaluation_mode": _evaluationMode, "evaluation_mode": _evaluationMode,
"id": id, "id": id,
"is_active": true, "is_active": true,
"name": _name, "name": _name,
"owner": user, "owner": user,
"start": _start, "start": null,
} }
}, },
[_conditions, _end, _evaluationMode, id, _name, user, _start], [_conditions, _evaluationMode, id, _name, user],
) )
const { error, loading, fetchNow } = useBackend(fetchDataAuth, method, path, body) const { error, loading, fetchNow } = useBackend(fetchDataAuth, method, path, body)
const save = useCallback( const save = useCallback(
() => { async () => {
if(!id) { if(!id) {
console.info("Creating new repository with body: ", body) console.info("Creating new repository with body: ", body)
} }
else { else {
console.info("Editing repository ", id, " with body: ", body) console.info("Editing repository ", id, " with body: ", body)
} }
fetchNow() await fetchNow()
}, },
[id, body, fetchNow], [id, body, fetchNow],
) )

View file

@ -25,9 +25,8 @@ export default function useArrayState(def) {
console.debug("Splicing ", position, " from ArrayState") console.debug("Splicing ", position, " from ArrayState")
setValue( setValue(
oldArray => { oldArray => {
// TODO: Hope this doesn't break anything...
oldArray.splice(position, 1) oldArray.splice(position, 1)
return oldArray return [...oldArray]
}, },
) )
}, },

View file

@ -0,0 +1,204 @@
import { useCallback, useContext, useEffect, useState } from "react"
import ContextServer from "../contexts/ContextServer"
import ContextUser from "../contexts/ContextUser"
import makeURLSearchParams from "../utils/makeURLSearchParams"
export default function useBackendViewset(resourcesPath, pkName) {
const { server } = useContext(ContextServer)
const configured = server !== null
const { user } = useContext(ContextUser)
const loggedIn = user !== null
const [abort, setAbort] = useState(null)
const running = abort !== null
const [resources, setResources] = useState(null)
const loaded = resources !== null
const apiRequest = useCallback(
async (method, path, body, init = {}) => {
// Check if server is configured
if(!configured) {
throw new Error(`Backend server not configured.`)
}
// Check if something is not already being loaded
if(running) {
throw new Error(`A request is already running.`)
}
// Ensure init has certain sub-objects
if(!init["headers"]) {
init["headers"] = {}
}
// If the user is logged in, add the Authorization headers
if(loggedIn) {
init["headers"]["Authorization"] = `Bearer ${user.token}`
}
// Set the Content-Type header
init["headers"]["Content-Type"] = "application/json"
// Use the body param as either search parameter or request body
if(body) {
if(["GET", "HEAD"].includes(method.toUpperCase())) {
path += makeURLSearchParams(body).toString()
}
else {
init["body"] = JSON.stringify(body)
}
}
// Set the method
init["method"] = method
// Create a new abort handler in case the request needs to be aborted
const thisAbort = new AbortController()
init["signal"] = thisAbort.signal
setAbort(thisAbort)
// Fetch the resource
const response = await fetch(`${server}${path}`, init)
// Clear the abort handler
setAbort(null)
// Check if the request was successful
if(!response.ok) {
throw new Error(`${method} ${path} failed with status code ${response.status} ${response.statusText}`)
}
// If the response is 204 NO CONTENT, return null
if(response.status === 204) {
return null
}
// Otherwise, try parsing the response as JSON
const json = await response.json()
// Check if the JSON contains a success
if(json["result"] !== "success") {
throw new Error(`${method} ${path} failed with message ${json["msg"]}`)
}
return json["data"]
},
[server, configured, running, loggedIn, user, setAbort],
)
const apiList = useCallback(
async (init) => await apiRequest("GET", `${resourcesPath}`, undefined, init),
[apiRequest, resourcesPath],
)
const apiRetrieve = useCallback(
async (id, init) => await apiRequest("GET", `${resourcesPath}${id}`, undefined, init),
[apiRequest, resourcesPath],
)
const apiCreate = useCallback(
async (data, init) => await apiRequest("POST", `${resourcesPath}`, data, init),
[apiRequest, resourcesPath],
)
const apiEdit = useCallback(
async (id, data, init) => await apiRequest("PUT", `${resourcesPath}${id}`, data, init),
[apiRequest, resourcesPath],
)
const apiDestroy = useCallback(
async (id, init) => await apiRequest("DELETE", `${resourcesPath}${id}`, undefined, init),
[apiRequest, resourcesPath],
)
const refreshResources = useCallback(
async () => {
try {
setResources(await apiList())
}
catch(e) {
return { error: e }
}
return {}
},
[apiList],
)
const createResource = useCallback(
async (data) => {
try {
const newResource = await apiCreate(data)
setResources(resources => [...resources, newResource])
}
catch(e) {
return { error: e }
}
return {}
},
[apiCreate],
)
const editResource = useCallback(
async (pk, data) => {
try {
const editedResource = await apiEdit(pk, data)
setResources(resources => resources.map(resource => {
if(resource[pkName] === pk) {
return editedResource
}
return resource
}))
}
catch(e) {
return { error: e }
}
return {}
},
[apiEdit, pkName],
)
const destroyResource = useCallback(
async (pk) => {
try {
await apiDestroy(pk)
setResources(resources => resources.filter(resource => resource[pkName] !== pk))
}
catch(e) {
return { error: e }
}
return {}
},
[apiDestroy, pkName],
)
useEffect(
() => {
if(!(
loaded || running
)) {
// noinspection JSIgnoredPromiseFromCall
refreshResources()
}
},
[loaded, refreshResources, running],
)
return {
abort,
resources,
running,
loaded,
apiList,
apiRetrieve,
apiCreate,
apiEdit,
apiDestroy,
refreshResources,
createResource,
editResource,
destroyResource,
}
}

View file

@ -0,0 +1,22 @@
import React from "react"
import Style from "./PageUsers.module.css"
import classNames from "classnames"
import BoxHeader from "../components/base/BoxHeader"
import BoxUserCreate from "../components/interactive/BoxUserCreate"
import useBackendViewset from "../hooks/useBackendViewset"
import BoxUserList from "../components/interactive/BoxUserList"
export default function PageUsers({ children, className, ...props }) {
const bv = useBackendViewset("/api/v1/users/", "email")
return (
<div className={classNames(Style.PageUsers, className)} {...props}>
<BoxHeader className={Style.Header}>
Gestisci utenti
</BoxHeader>
<BoxUserCreate className={Style.CreateUser} createUser={bv.createResource} running={bv.running}/>
<BoxUserList className={Style.UserList} users={bv.resources} destroyUser={bv.destroyResource} running={bv.running}/>
</div>
)
}

View file

@ -0,0 +1,27 @@
.PageUsers {
display: grid;
grid-template-areas:
"a"
"b"
"c";
grid-template-rows: auto 1fr auto;
grid-template-columns: 1fr;
grid-gap: 10px;
width: 100%;
height: 100%;
}
.Header {
grid-area: a;
}
.UserList {
grid-area: b;
}
.CreateUser {
grid-area: c;
}

View file

@ -0,0 +1,19 @@
import isString from "is-string"
export default function makeURLSearchParams(obj) {
let usp = new URLSearchParams()
for(const key in obj) {
if(!obj.hasOwnProperty(key)) {
return
}
const value = obj[key]
if(isString(value)) {
usp.set(key, value)
}
else {
usp.set(key, JSON.stringify(value))
}
}
return usp
}