Compare commits
4 Commits
8ee3687c73
...
4892916e16
Author | SHA1 | Date | |
---|---|---|---|
|
4892916e16 | ||
|
7291faede5 | ||
|
653688b376 | ||
|
2e76962ca0 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -154,5 +154,4 @@ cython_debug/
|
|||||||
|
|
||||||
# Custom
|
# Custom
|
||||||
.vscode
|
.vscode
|
||||||
config.json
|
config.json
|
||||||
pages
|
|
21
classes/models.py
Normal file
21
classes/models.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
from typing import Union
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
class Photo(BaseModel):
|
||||||
|
id: str
|
||||||
|
album: str
|
||||||
|
hash: str
|
||||||
|
filename: str
|
||||||
|
|
||||||
|
class Album(BaseModel):
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
title: str
|
||||||
|
|
||||||
|
class AlbumModified(BaseModel):
|
||||||
|
name: str
|
||||||
|
title: str
|
||||||
|
|
||||||
|
class SearchResults(BaseModel):
|
||||||
|
results: list
|
||||||
|
next_page: Union[str, None] = None
|
@ -2,6 +2,7 @@ import re
|
|||||||
from os import makedirs, path, rename
|
from os import makedirs, path, rename
|
||||||
from shutil import rmtree
|
from shutil import rmtree
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
from classes.models import Album, AlbumModified, SearchResults
|
||||||
from modules.app import app
|
from modules.app import app
|
||||||
from modules.database import col_photos, col_albums
|
from modules.database import col_photos, col_albums
|
||||||
from modules.security import User, get_current_active_user
|
from modules.security import User, get_current_active_user
|
||||||
@ -12,7 +13,7 @@ from fastapi import HTTPException, Security
|
|||||||
from fastapi.responses import UJSONResponse, Response
|
from fastapi.responses import UJSONResponse, Response
|
||||||
from starlette.status import HTTP_204_NO_CONTENT, HTTP_404_NOT_FOUND, HTTP_406_NOT_ACCEPTABLE, HTTP_409_CONFLICT
|
from starlette.status import HTTP_204_NO_CONTENT, HTTP_404_NOT_FOUND, HTTP_406_NOT_ACCEPTABLE, HTTP_409_CONFLICT
|
||||||
|
|
||||||
@app.post("/albums", response_class=UJSONResponse, description="Create album with name and title")
|
@app.post("/albums", response_class=UJSONResponse, response_model=Album, description="Create album with name and title")
|
||||||
async def album_create(name: str, title: str, current_user: User = Security(get_current_active_user, scopes=["albums.write"])):
|
async def album_create(name: str, title: str, current_user: User = Security(get_current_active_user, scopes=["albums.write"])):
|
||||||
|
|
||||||
if re.search(re.compile('^[a-z,0-9,_]*$'), name) is False:
|
if re.search(re.compile('^[a-z,0-9,_]*$'), name) is False:
|
||||||
@ -39,7 +40,7 @@ async def album_create(name: str, title: str, current_user: User = Security(get_
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@app.get("/albums", description="Find album by name")
|
@app.get("/albums", response_model=SearchResults, description="Find album by name")
|
||||||
async def album_find(q: str, current_user: User = Security(get_current_active_user, scopes=["albums.list"])):
|
async def album_find(q: str, current_user: User = Security(get_current_active_user, scopes=["albums.list"])):
|
||||||
|
|
||||||
output = {"results": []}
|
output = {"results": []}
|
||||||
@ -50,7 +51,7 @@ async def album_find(q: str, current_user: User = Security(get_current_active_us
|
|||||||
|
|
||||||
return UJSONResponse(output)
|
return UJSONResponse(output)
|
||||||
|
|
||||||
@app.patch("/albums/{id}", response_class=UJSONResponse, description="Modify album's name or title by id")
|
@app.patch("/albums/{id}", response_class=UJSONResponse, response_model=AlbumModified, description="Modify album's name or title by id")
|
||||||
async def album_patch(id: str, name: Union[str, None] = None, title: Union[str, None] = None, current_user: User = Security(get_current_active_user, scopes=["albums.write"])):
|
async def album_patch(id: str, name: Union[str, None] = None, title: Union[str, None] = None, current_user: User = Security(get_current_active_user, scopes=["albums.write"])):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -88,7 +89,7 @@ async def album_patch(id: str, name: Union[str, None] = None, title: Union[str,
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@app.put("/albums/{id}", response_class=UJSONResponse, description="Modify album's name and title by id")
|
@app.put("/albums/{id}", response_class=UJSONResponse, response_model=AlbumModified, description="Modify album's name and title by id")
|
||||||
async def album_put(id: str, name: str, title: str, current_user: User = Security(get_current_active_user, scopes=["albums.write"])):
|
async def album_put(id: str, name: str, title: str, current_user: User = Security(get_current_active_user, scopes=["albums.write"])):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
27
extensions/pages.py
Normal file
27
extensions/pages.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
from os import path
|
||||||
|
from modules.app import app
|
||||||
|
from fastapi.responses import HTMLResponse, Response
|
||||||
|
|
||||||
|
@app.get("/pages/matter.css", include_in_schema=False)
|
||||||
|
async def page_matter():
|
||||||
|
with open(path.join("pages", "matter.css"), "r", encoding="utf-8") as f:
|
||||||
|
output = f.read()
|
||||||
|
return Response(content=output)
|
||||||
|
|
||||||
|
@app.get("/pages/{page}/{file}", include_in_schema=False)
|
||||||
|
async def page_assets(page:str, file: str):
|
||||||
|
with open(path.join("pages", page, file), "r", encoding="utf-8") as f:
|
||||||
|
output = f.read()
|
||||||
|
return Response(content=output)
|
||||||
|
|
||||||
|
@app.get("/", include_in_schema=False)
|
||||||
|
async def page_home():
|
||||||
|
with open(path.join("pages", "home", "index.html"), "r", encoding="utf-8") as f:
|
||||||
|
output = f.read()
|
||||||
|
return HTMLResponse(content=output)
|
||||||
|
|
||||||
|
@app.get("/register", include_in_schema=False)
|
||||||
|
async def page_register():
|
||||||
|
with open(path.join("pages", "register", "index.html"), "r", encoding="utf-8") as f:
|
||||||
|
output = f.read()
|
||||||
|
return HTMLResponse(content=output)
|
@ -2,9 +2,11 @@ import re
|
|||||||
import pickle
|
import pickle
|
||||||
from secrets import token_urlsafe
|
from secrets import token_urlsafe
|
||||||
from magic import Magic
|
from magic import Magic
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
from os import makedirs, path, remove
|
from os import makedirs, path, remove, system
|
||||||
|
from classes.models import Photo, SearchResults
|
||||||
from modules.hasher import get_phash, get_duplicates
|
from modules.hasher import get_phash, get_duplicates
|
||||||
|
from modules.scheduler import scheduler
|
||||||
from modules.security import User, get_current_active_user
|
from modules.security import User, get_current_active_user
|
||||||
from modules.app import app
|
from modules.app import app
|
||||||
from modules.database import col_photos, col_albums, col_tokens
|
from modules.database import col_photos, col_albums, col_tokens
|
||||||
@ -15,8 +17,28 @@ from fastapi import HTTPException, UploadFile, Security
|
|||||||
from fastapi.responses import UJSONResponse, Response
|
from fastapi.responses import UJSONResponse, Response
|
||||||
from starlette.status import HTTP_204_NO_CONTENT, HTTP_400_BAD_REQUEST, HTTP_401_UNAUTHORIZED, HTTP_404_NOT_FOUND, HTTP_409_CONFLICT
|
from starlette.status import HTTP_204_NO_CONTENT, HTTP_400_BAD_REQUEST, HTTP_401_UNAUTHORIZED, HTTP_404_NOT_FOUND, HTTP_409_CONFLICT
|
||||||
|
|
||||||
@app.post("/albums/{album}/photos", response_class=UJSONResponse, description="Upload a photo to album")
|
from modules.utils import logWrite
|
||||||
async def photo_upload(file: UploadFile, album: str, ignore_duplicates: bool = False, current_user: User = Security(get_current_active_user, scopes=["photos.write"])):
|
|
||||||
|
async def compress_image(image_path: str):
|
||||||
|
|
||||||
|
image_type = Magic(mime=True).from_file(image_path)
|
||||||
|
|
||||||
|
if image_type not in ["image/jpeg", "image/png"]:
|
||||||
|
logWrite(f"Not compressing {image_path} because its mime is '{image_type}'")
|
||||||
|
return
|
||||||
|
|
||||||
|
size_before = path.getsize(image_path) / 1024
|
||||||
|
|
||||||
|
if image_type == "image/jpeg":
|
||||||
|
system(f"jpegoptim {image_path} -o --max=60 --strip-all")
|
||||||
|
elif image_type == "image/png":
|
||||||
|
system(f"optipng -o3 {image_path}")
|
||||||
|
|
||||||
|
size_after = path.getsize(image_path) / 1024
|
||||||
|
logWrite(f"Compressed '{path.split(image_path)[-1]}' from {size_before} Kb to {size_after} Kb")
|
||||||
|
|
||||||
|
@app.post("/albums/{album}/photos", response_class=UJSONResponse, response_model=Photo, description="Upload a photo to album")
|
||||||
|
async def photo_upload(file: UploadFile, album: str, ignore_duplicates: bool = False, compress: bool = True, current_user: User = Security(get_current_active_user, scopes=["photos.write"])):
|
||||||
|
|
||||||
if col_albums.find_one( {"user": current_user.user, "name": album} ) is None:
|
if col_albums.find_one( {"user": current_user.user, "name": album} ) is None:
|
||||||
return HTTPException(status_code=HTTP_404_NOT_FOUND, detail=f"Provided album '{album}' does not exist.")
|
return HTTPException(status_code=HTTP_404_NOT_FOUND, detail=f"Provided album '{album}' does not exist.")
|
||||||
@ -50,6 +72,9 @@ async def photo_upload(file: UploadFile, album: str, ignore_duplicates: bool = F
|
|||||||
|
|
||||||
uploaded = col_photos.insert_one( {"user": current_user.user, "album": album, "hash": file_hash, "filename": filename} )
|
uploaded = col_photos.insert_one( {"user": current_user.user, "album": album, "hash": file_hash, "filename": filename} )
|
||||||
|
|
||||||
|
if compress is True:
|
||||||
|
scheduler.add_job(compress_image, trigger="date", run_date=datetime.now()+timedelta(seconds=1), args=[path.join("data", "users", current_user.user, "albums", album, filename)])
|
||||||
|
|
||||||
return UJSONResponse(
|
return UJSONResponse(
|
||||||
{
|
{
|
||||||
"id": uploaded.inserted_id.__str__(),
|
"id": uploaded.inserted_id.__str__(),
|
||||||
@ -91,7 +116,7 @@ async def photo_delete(id: str, current_user: User = Security(get_current_active
|
|||||||
|
|
||||||
return Response(status_code=HTTP_204_NO_CONTENT)
|
return Response(status_code=HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
@app.get("/albums/{album}/photos", response_class=UJSONResponse, description="Find a photo by filename")
|
@app.get("/albums/{album}/photos", response_class=UJSONResponse, response_model=SearchResults, description="Find a photo by filename")
|
||||||
async def photo_find(q: str, album: str, page: int = 1, page_size: int = 100, current_user: User = Security(get_current_active_user, scopes=["photos.list"])):
|
async def photo_find(q: str, album: str, page: int = 1, page_size: int = 100, current_user: User = Security(get_current_active_user, scopes=["photos.list"])):
|
||||||
|
|
||||||
if col_albums.find_one( {"user": current_user.user, "name": album} ) is None:
|
if col_albums.find_one( {"user": current_user.user, "name": album} ) is None:
|
||||||
@ -111,13 +136,12 @@ async def photo_find(q: str, album: str, page: int = 1, page_size: int = 100, cu
|
|||||||
token = str(token_urlsafe(32))
|
token = str(token_urlsafe(32))
|
||||||
col_tokens.insert_one( {"token": token, "query": q, "album": album, "page": page+1, "page_size": page_size, "user": pickle.dumps(current_user)} )
|
col_tokens.insert_one( {"token": token, "query": q, "album": album, "page": page+1, "page_size": page_size, "user": pickle.dumps(current_user)} )
|
||||||
output["next_page"] = f"/albums/{album}/photos/token?token={token}" # type: ignore
|
output["next_page"] = f"/albums/{album}/photos/token?token={token}" # type: ignore
|
||||||
|
else:
|
||||||
with open("something.txt", "w", encoding="utf-8") as f:
|
output["next_page"] = None # type: ignore
|
||||||
f.write(pickle.loads(pickle.dumps(current_user)).user)
|
|
||||||
|
|
||||||
return UJSONResponse(output)
|
return UJSONResponse(output)
|
||||||
|
|
||||||
@app.get("/albums/{album}/photos/token", response_class=UJSONResponse, description="Find a photo by token")
|
@app.get("/albums/{album}/photos/token", response_class=UJSONResponse, response_model=SearchResults, description="Find a photo by token")
|
||||||
async def photo_find_token(token: str):
|
async def photo_find_token(token: str):
|
||||||
|
|
||||||
found_record = col_tokens.find_one( {"token": token} )
|
found_record = col_tokens.find_one( {"token": token} )
|
||||||
|
3
modules/scheduler.py
Normal file
3
modules/scheduler.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||||
|
|
||||||
|
scheduler = AsyncIOScheduler()
|
24
pages/home/index.html
Normal file
24
pages/home/index.html
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" >
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>END PLAY Photos API • Home</title>
|
||||||
|
<link href="/pages/matter.css" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="/pages/home/style.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<form class="registration">
|
||||||
|
|
||||||
|
<h1>👋 Welcome!</h1>
|
||||||
|
|
||||||
|
<p>You need to register in order to use this API.</p>
|
||||||
|
<p>After registering use official docs to learn how to authentiticate and start managing photos.</p>
|
||||||
|
|
||||||
|
<center><a class="matter-button-contained" href="https://photos.end-play.xyz/register">Sign Up</a></center>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
6
pages/home/script.js
Normal file
6
pages/home/script.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// JavaScript is used for toggling loading state
|
||||||
|
var form = document.querySelector('form');
|
||||||
|
form.onsubmit = function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
form.classList.add('signed');
|
||||||
|
};
|
148
pages/home/style.css
Normal file
148
pages/home/style.css
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
/* Material Customization */
|
||||||
|
:root {
|
||||||
|
--pure-material-primary-rgb: 255, 191, 0;
|
||||||
|
--pure-material-onsurface-rgb: 0, 0, 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: url("https://res.cloudinary.com/finnhvman/image/upload/v1541930411/pattern.png");
|
||||||
|
}
|
||||||
|
|
||||||
|
.registration {
|
||||||
|
position: relative;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px 48px;
|
||||||
|
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 32px 0;
|
||||||
|
font-family: "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system;
|
||||||
|
font-weight: normal;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registration > label {
|
||||||
|
display: block;
|
||||||
|
margin: 24px 0;
|
||||||
|
width: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-family: "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system;
|
||||||
|
font-weight: normal;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.matter-button-contained {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: rgb(var(--pure-material-primary-rgb));
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
display: block !important;
|
||||||
|
margin: 32px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.done,
|
||||||
|
.progress {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: white;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.done {
|
||||||
|
transition: visibility 0s 1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signed > .done {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.done > a {
|
||||||
|
display: inline-block;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signed > .progress {
|
||||||
|
animation: loading 4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes loading {
|
||||||
|
0% {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
12.5% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
87.5% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-footer,
|
||||||
|
.right-footer {
|
||||||
|
position: fixed;
|
||||||
|
padding: 14px;
|
||||||
|
bottom: 14px;
|
||||||
|
color: #555;
|
||||||
|
background-color: #eee;
|
||||||
|
font-family: "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-footer {
|
||||||
|
left: 0;
|
||||||
|
border-radius: 0 4px 4px 0;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-footer {
|
||||||
|
right: 0;
|
||||||
|
border-radius: 4px 0 0 4px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-footer > a,
|
||||||
|
.right-footer > a {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-footer > a:hover,
|
||||||
|
.right-footer > a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
2
pages/matter.css
Normal file
2
pages/matter.css
Normal file
File diff suppressed because one or more lines are too long
50
pages/register/index.html
Normal file
50
pages/register/index.html
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" >
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>END PLAY Photos API • Sign Up</title>
|
||||||
|
<link href="/pages/matter.css" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="/pages/home/style.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<!-- partial:index.partial.html -->
|
||||||
|
<form class="registration" method="post">
|
||||||
|
<h1>👋 Welcome!</h1>
|
||||||
|
|
||||||
|
<label class="matter-textfield-outlined">
|
||||||
|
<input placeholder=" " type="text" alt="You won't be able to change it later!" required>
|
||||||
|
<span>Username</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="matter-textfield-outlined">
|
||||||
|
<input placeholder=" " type="email" required>
|
||||||
|
<span>Email</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="matter-textfield-outlined">
|
||||||
|
<input placeholder=" " type="password" required>
|
||||||
|
<span>Password</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="matter-checkbox">
|
||||||
|
<input type="checkbox" required>
|
||||||
|
<span>I agree to the <a href="https://codepen.io/collection/nZKBZe/" target="_blank" title="Actually not a Terms of Service">Terms of Service</a></span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button class="matter-button-contained" type="submit">Sign Up</button>
|
||||||
|
|
||||||
|
<div class="done">
|
||||||
|
<h1>👌 You're all set!</h1>
|
||||||
|
<a class="matter-button-text" href="javascript:window.location.reload(true)">Again</a>
|
||||||
|
</div>
|
||||||
|
<div class="progress">
|
||||||
|
<progress class="matter-progress-circular" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- partial -->
|
||||||
|
<script src="./script.js"></script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
6
pages/register/script.js
Normal file
6
pages/register/script.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// JavaScript is used for toggling loading state
|
||||||
|
var form = document.querySelector('form');
|
||||||
|
form.onsubmit = function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
form.classList.add('signed');
|
||||||
|
};
|
138
pages/register/style.css
Normal file
138
pages/register/style.css
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
/* Material Customization */
|
||||||
|
:root {
|
||||||
|
--pure-material-primary-rgb: 255, 191, 0;
|
||||||
|
--pure-material-onsurface-rgb: 0, 0, 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: url("https://res.cloudinary.com/finnhvman/image/upload/v1541930411/pattern.png");
|
||||||
|
}
|
||||||
|
|
||||||
|
.registration {
|
||||||
|
position: relative;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px 48px;
|
||||||
|
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 32px 0;
|
||||||
|
font-family: "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system;
|
||||||
|
font-weight: normal;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registration > label {
|
||||||
|
display: block;
|
||||||
|
margin: 24px 0;
|
||||||
|
width: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: rgb(var(--pure-material-primary-rgb));
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
display: block !important;
|
||||||
|
margin: 32px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.done,
|
||||||
|
.progress {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: white;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.done {
|
||||||
|
transition: visibility 0s 1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signed > .done {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.done > a {
|
||||||
|
display: inline-block;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signed > .progress {
|
||||||
|
animation: loading 4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes loading {
|
||||||
|
0% {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
12.5% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
87.5% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-footer,
|
||||||
|
.right-footer {
|
||||||
|
position: fixed;
|
||||||
|
padding: 14px;
|
||||||
|
bottom: 14px;
|
||||||
|
color: #555;
|
||||||
|
background-color: #eee;
|
||||||
|
font-family: "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-footer {
|
||||||
|
left: 0;
|
||||||
|
border-radius: 0 4px 4px 0;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-footer {
|
||||||
|
right: 0;
|
||||||
|
border-radius: 4px 0 0 4px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-footer > a,
|
||||||
|
.right-footer > a {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-footer > a:hover,
|
||||||
|
.right-footer > a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
from os import makedirs, sep
|
from os import makedirs, sep
|
||||||
from modules.app import app
|
from modules.app import app
|
||||||
from modules.utils import *
|
from modules.utils import *
|
||||||
|
from modules.scheduler import scheduler
|
||||||
from modules.extensions_loader import dynamic_import_from_src
|
from modules.extensions_loader import dynamic_import_from_src
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse
|
||||||
|
|
||||||
@ -14,4 +15,6 @@ async def favicon():
|
|||||||
|
|
||||||
#=================================================================================
|
#=================================================================================
|
||||||
dynamic_import_from_src("extensions", star_import = True)
|
dynamic_import_from_src("extensions", star_import = True)
|
||||||
#=================================================================================
|
#=================================================================================
|
||||||
|
|
||||||
|
scheduler.start()
|
@ -5,4 +5,5 @@ scipy~=1.9.3
|
|||||||
python-magic~=0.4.27
|
python-magic~=0.4.27
|
||||||
opencv-python~=4.6.0.66
|
opencv-python~=4.6.0.66
|
||||||
python-jose[cryptography]~=3.3.0
|
python-jose[cryptography]~=3.3.0
|
||||||
passlib~=1.7.4
|
passlib~=1.7.4
|
||||||
|
apscheduler~=3.9.1.post1
|
Loading…
x
Reference in New Issue
Block a user