moving website to fastapi as well

dev
Anton Franzluebbers 2022-06-23 20:27:59 -04:00
parent 7dd25b2bc1
commit 39e840b62a
31 changed files with 389 additions and 233 deletions

1
.gitignore vendored
View File

@ -9,3 +9,4 @@ spectre/
velconnect_backup.sql velconnect_backup.sql
discord_bot/graph.png discord_bot/graph.png
discord_bot/config.py discord_bot/config.py
env_win/

View File

@ -1,6 +0,0 @@
#!/bin/bash
export FLASK_APP="velconnect"
export FLASK_ENV=development
source env/bin/activate
flask run

File diff suppressed because one or more lines are too long

View File

@ -1,210 +0,0 @@
<html>
<head>
<link rel="apple-touch-icon" sizes="180x180" href="/favicons/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicons/favicon-16x16.png">
<link rel="manifest" href="/favicons/site.webmanifest">
<link rel="mask-icon" href="/favicons/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#b91d47">
<meta name="theme-color" content="#ffffff">
<title>VEL Connect</title>
<link rel="stylesheet" href="/css/spectre.min.css">
<script src="/js/util.js"></script>
<style>
.container {
max-width: 30em;
}
.card {
margin: 1em;
box-shadow: 0 0 2em #0003;
}
input.btn {
cursor: auto;
user-select: auto;
}
.centered {
margin: auto;
}
</style>
</head>
<body>
<div class="container">
<div id="loading" class="loading loading-lg"></div>
<div id="failure" style="display: none;">☹️</div>
<div id="headset_details" style="display: none;">
<div class="panel card">
<div class="panel-header text-center">
<figure class="avatar avatar-lg"><img src="/favicons/android-chrome-192x192.png"
alt="Avatar"></figure>
<div class="panel-title h5 mt-10">Headset Info</div>
<div class="panel-subtitle hw_id">---</div>
</div>
<div class="panel-body">
<div class="tile tile-centered">
<div class="tile-content">
<div class="tile-title text-bold">Current Room</div>
<input class="btn current_room" type="text" id="current_room" placeholder="----">
</div>
<div class="tile-action">
<button class="btn btn-primary btn-lg tooltip tooltip-left" id="set_room_id"
data-tooltip="Set Room ID">Set</button>
</div>
</div>
<br>
<div class="tile tile-centered">
<div class="tile-content">
<div class="tile-title text-bold">First Added</div>
<div class="tile-subtitle date_created">---</div>
</div>
</div>
<br>
<div class="tile tile-centered">
<div class="tile-content">
<div class="tile-title text-bold">Last Used</div>
<div class="tile-subtitle last_used">---</div>
</div>
</div>
<br>
<div class="tile tile-centered">
<div class="tile-content">
<div class="tile-title text-bold">User Name</div>
<input class="btn user_name" type="text" id="user_name" placeholder="----">
</div>
<div class="tile-action">
<button class="btn btn-primary btn-lg tooltip tooltip-left" id="set_user_name"
data-tooltip="">Set</button>
</div>
</div>
<br>
<div class="tile tile-centered">
<div class="tile-content">
<div class="tile-title text-bold">TV URL</div>
<input class="btn tv_url" type="text" id="tv_url" placeholder="----">
</div>
<div class="tile-action">
<button class="btn btn-primary btn-lg tooltip tooltip-left" id="set_tv_url"
data-tooltip="">Set</button>
</div>
</div>
<br>
<div class="tile tile-centered">
<div class="tile-content">
<div class="tile-title text-bold">User Color</div>
<input class="btn user_color" type="color" id="user_color" placeholder="#ffffff">
</div>
<div class="tile-action">
<button class="btn btn-primary btn-lg tooltip tooltip-left" id="set_user_color"
data-tooltip="Set User Color">Set</button>
</div>
</div>
<br>
<div class="tile tile-centered">
<div class="tile-content">
<div class="tile-title text-bold">Carpet Color</div>
<input class="btn carpet_color" type="color" id="carpet_color" placeholder="#ffffff">
</div>
<div class="tile-action">
<button class="btn btn-primary btn-lg tooltip tooltip-left" id="set_carpet_color"
data-tooltip="Set Carpet Color">Set</button>
</div>
</div>
<br>
<br>
</div>
</div>
</div>
</div>
<script>
let submit_button = document.getElementById('submit_pairing_code');
let pair_code_input = document.getElementById('pair_code');
let loading = document.getElementById('loading');
let enter_pairing_id = document.getElementById('enter_pairing_id');
let headset_details = document.getElementById('headset_details');
let hw_id_field = document.getElementById('hw_id');
let failure = document.getElementById('failure');
let set_room_id = document.getElementById('set_room_id');
let current_room = document.getElementById('current_room');
// check cookie
let hw_id = getCookie('hw_id');
loading.style.display = "none";
if (hw_id != "") {
httpGetAsync('https://connect.vel.workers.dev/api/get_state/' + hw_id, (resp) => {
console.log(resp);
let respData = JSON.parse(resp);
writeClass('hw_id', respData['user']['hw_id']);
writeValue('current_room', respData['user']['current_room']);
writeClass('date_created', respData['user']['date_created']);
writeClass('last_used', respData['user']['last_used']);
writeValue('user_color', respData['user']['user_color']);
writeValue('user_name', respData['user']['user_name']);
if (respData['room']) {
writeValue('tv_url', respData['room']['tv_url']);
writeValue('carpet_color', respData['room']['carpet_color']);
}
headset_details.style.display = "block";
}, (status) => {
failure.style.display = "block";
});
function setUserData(endpoint, data) {
httpPostAsync('https://connect.vel.workers.dev/api/set_headset_details/' + hw_id + '/' + endpoint,
data,
(resp) => {console.log('success');},
(status) => {console.log('fail');}
);
}
function setRoomData(endpoint, data) {
httpPostAsync('https://connect.vel.workers.dev/api/set_room_details/' + current_room.value + '/' + endpoint,
data,
(resp) => {console.log('success');},
(status) => {console.log('fail');}
);
}
set_room_id.addEventListener('click', () => {
setUserData('current_room', {"current_room": current_room.value});
});
document.getElementById('set_user_color').addEventListener('click', () => {
setUserData('user_color', {"user_color": document.getElementById('user_color').value});
});
document.getElementById('set_user_name').addEventListener('click', () => {
setUserData('user_name', {"user_name": document.getElementById('user_name').value});
});
document.getElementById('set_tv_url').addEventListener('click', () => {
setRoomData('tv_url', {"tv_url": document.getElementById('tv_url').value});
});
document.getElementById('set_carpet_color').addEventListener('click', () => {
setRoomData('carpet_color', {"carpet_color": document.getElementById('carpet_color').value});
});
} else {
window.location.href = "/pair";
}
</script>
</body>
</html>

View File

@ -34,6 +34,8 @@ CREATE TABLE `Headset` (
`avatar_url` VARCHAR(128), `avatar_url` VARCHAR(128),
-- Stuff like player color, nickname, whiteboard state -- Stuff like player color, nickname, whiteboard state
`user_details` JSON, `user_details` JSON,
`streamer_stream_id` VARCHAR(64),
`streamer_control_id` VARCHAR(64),
CHECK (JSON_VALID(`user_details`)) CHECK (JSON_VALID(`user_details`))
); );
DROP TABLE IF EXISTS `APIKey`; DROP TABLE IF EXISTS `APIKey`;

View File

@ -8,6 +8,8 @@ from fastapi.security import OAuth2PasswordBearer
from db import query, insert from db import query, insert
from pydantic import BaseModel from pydantic import BaseModel
from typing import Union from typing import Union
from pyppeteer import launch
from enum import Enum
# APIRouter creates path operations for user module # APIRouter creates path operations for user module
@ -219,3 +221,43 @@ def get_user_count(hours: float = 24):
WHERE TIMESTAMP > DATE_SUB(NOW(), INTERVAL """ + str(hours) + """ HOUR); WHERE TIMESTAMP > DATE_SUB(NOW(), INTERVAL """ + str(hours) + """ HOUR);
""") """)
return values return values
class QuestRift(str, Enum):
quest = "quest"
rift = "rift"
@router.get('/get_store_details/{quest_rift}/{app_id}', tags=["Oculus API"])
async def get_version_nums(quest_rift: QuestRift, app_id: int):
browser = await launch(headless=True, options={'args': ['--no-sandbox']})
page = await browser.newPage()
await page.goto(f'https://www.oculus.com/experiences/{quest_rift}/{app_id}')
ret = {}
# title
title = await page.querySelector(".app-description__title")
ret["title"] = await page.evaluate("e => e.textContent", title)
# description
desc = await page.querySelector(".clamped-description__content")
ret["description"] = await page.evaluate("e => e.textContent", desc)
# versions
await page.evaluate("document.querySelector('.app-details-version-info-row__version').nextElementSibling.firstChild.click();")
elements = await page.querySelectorAll('.sky-dropdown__link.link.link--clickable')
versions = []
for e in elements:
v = await page.evaluate('(element) => element.textContent', e)
versions.append({
'channel': v.split(':')[0],
'version': v.split(':')[1]
})
ret["versions"] = versions
await browser.close()
return ret

View File

@ -3,6 +3,8 @@ import uvicorn
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi import FastAPI from fastapi import FastAPI
from api import router as api_router from api import router as api_router
from website import router as website_router
from fastapi.staticfiles import StaticFiles
app = FastAPI() app = FastAPI()
@ -21,7 +23,10 @@ app.add_middleware(
allow_headers=["*"], allow_headers=["*"],
) )
app.mount("/static", StaticFiles(directory="static"), name="static")
app.include_router(api_router) app.include_router(api_router)
app.include_router(website_router)
if __name__ == '__main__': if __name__ == '__main__':
uvicorn.run("main:app", host='127.0.0.1', port=8005, uvicorn.run("main:app", host='127.0.0.1', port=8005,

View File

@ -2,3 +2,5 @@ fastapi
autopep8 autopep8
uvicorn uvicorn
pymysql pymysql
pyppeteer
jinja2

1
velconnect/static/css/coloris.min.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
velconnect/static/css/spectre.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View File

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 815 B

After

Width:  |  Height:  |  Size: 815 B

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 977 B

After

Width:  |  Height:  |  Size: 977 B

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

6
velconnect/static/js/coloris.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -96,3 +96,60 @@ function writeSrc(className, data) {
e.src = data; e.src = data;
}); });
} }
function timeSince(date) {
let seconds = Math.floor((new Date() - date) / 1000);
let interval = seconds / 31536000;
if (interval > 1) {
return Math.floor(interval) + " years";
}
interval = seconds / 2592000;
if (interval > 1) {
return Math.floor(interval) + " months";
}
interval = seconds / 86400;
if (interval > 1) {
return Math.floor(interval) + " days";
}
interval = seconds / 3600;
if (interval > 1) {
return Math.floor(interval) + " hours";
}
interval = seconds / 60;
if (interval > 1) {
return Math.floor(interval) + " minutes";
}
return Math.floor(seconds) + " seconds";
}
function timeSinceString(date) {
date = Date.parse(date);
let seconds = Math.floor((new Date() - date) / 1000);
let interval = seconds / 31536000;
if (interval > 1) {
return Math.floor(interval) + " years";
}
interval = seconds / 2592000;
if (interval > 1) {
return Math.floor(interval) + " months";
}
interval = seconds / 86400;
if (interval > 1) {
return Math.floor(interval) + " days";
}
interval = seconds / 3600;
if (interval > 1) {
return Math.floor(interval) + " hours";
}
interval = seconds / 60;
if (interval > 1) {
return Math.floor(interval) + " minutes";
}
return Math.floor(seconds) + " seconds";
}

View File

@ -17,7 +17,7 @@
<div slot="nav-logo" style="display: flex; align-items: center; justify-content: center;"> <div slot="nav-logo" style="display: flex; align-items: center; justify-content: center;">
<img src = "/favicons/android-chrome-256x256.png" style="width:10em; margin: auto;" /> <img src = "/static/favicons/android-chrome-256x256.png" style="width:10em; margin: auto;" />
</div> </div>
</rapi-doc> </rapi-doc>
</body> </body>

View File

@ -2,11 +2,11 @@
<head> <head>
<link rel="apple-touch-icon" sizes="180x180" href="/favicons/apple-touch-icon.png"> <link rel="apple-touch-icon" sizes="180x180" href="/static/favicons/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicons/favicon-32x32.png"> <link rel="icon" type="image/png" sizes="32x32" href="/static/favicons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicons/favicon-16x16.png"> <link rel="icon" type="image/png" sizes="16x16" href="/static/favicons/favicon-16x16.png">
<link rel="manifest" href="/favicons/site.webmanifest"> <link rel="manifest" href="/static/favicons/site.webmanifest">
<link rel="mask-icon" href="/favicons/safari-pinned-tab.svg" color="#5bbad5"> <link rel="mask-icon" href="/static/favicons/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#b91d47"> <meta name="msapplication-TileColor" content="#b91d47">
<meta name="theme-color" content="#ffffff"> <meta name="theme-color" content="#ffffff">

View File

@ -0,0 +1,225 @@
<html>
<head>
<link rel="apple-touch-icon" sizes="180x180" href="/static/favicons/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicons/favicon-16x16.png">
<link rel="manifest" href="/static/favicons/site.webmanifest">
<link rel="mask-icon" href="/static/favicons/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#b91d47">
<meta name="theme-color" content="#ffffff">
<title>VEL Connect</title>
<link rel="stylesheet" href="/static/css/spectre.min.css">
<script src="/static/js/util.js"></script>
<style>
.container {
max-width: 30em;
}
.card {
margin: 1em;
box-shadow: 0 0 2em #0003;
}
input.btn {
cursor: auto;
user-select: auto;
}
.centered {
margin: auto;
}
</style>
</head>
<body>
<div class="container">
<div id="loading"><br><br><div class="loading loading-lg"></div></div>
<div id="failure" style="display: none;"><br><br><br>☹️</div>
<div id="headset_details" style="display: none;">
<div class="panel card">
<div class="panel-header text-center">
<figure class="avatar avatar-lg"><img src="/static/favicons/android-chrome-192x192.png" alt="Avatar"></figure>
<div class="panel-title h5 mt-10">Headset Info</div>
<div class="panel-subtitle hw_id">---</div>
</div>
<div class="panel-body">
<div class="tile tile-centered">
<div class="tile-content">
<div class="tile-title text-bold">Current Room</div>
<input class="btn current_room" type="text" id="current_room" placeholder="----">
</div>
<div class="tile-action">
<button class="btn btn-primary btn-lg tooltip tooltip-left" id="set_room_id"
data-tooltip="Set Room ID">Set</button>
</div>
</div>
<br>
<div class="tile tile-centered">
<div class="tile-content">
<div class="tile-title text-bold">First Seen</div>
<div class="tile-subtitle date_created">---</div>
</div>
</div>
<br>
<div class="tile tile-centered">
<div class="tile-content">
<div class="tile-title text-bold">Last Login</div>
<div class="tile-subtitle last_used">---</div>
</div>
</div>
<br>
<div class="tile tile-centered">
<div class="tile-content">
<div class="tile-title text-bold">User Name</div>
<input class="btn user_name" type="text" id="user_name" placeholder="----">
<button class="btn btn-primary btn-lg tooltip tooltip-left" id="set_user_name" data-tooltip="">Set</button>
</div>
<div class="tile-action">
</div>
</div>
<br>
<div class="tile tile-centered">
<div class="tile-content">
<div class="tile-title text-bold">TV URL</div>
<input class="btn tv_url" type="text" id="tv_url" placeholder="----">
<button class="btn btn-primary btn-lg tooltip tooltip-left" id="set_tv_url" data-tooltip="">Set</button>
</div>
<div class="tile-action">
</div>
</div>
<br>
<div class="tile tile-centered">
<div class="tile-content">
<div class="tile-title text-bold">User Color</div>
<input class="btn user_color coloris" type="text" id="user_color" placeholder="#ffffff">
<button class="btn btn-primary btn-lg tooltip tooltip-left" id="set_user_color" data-tooltip="Set User Color">Set</button>
</div>
<div class="tile-action">
</div>
</div>
<br>
<div class="tile tile-centered">
<div class="tile-content">
<div class="tile-title text-bold">Carpet Color</div>
<input class="btn carpet_color coloris" type="text" id="carpet_color" placeholder="#ffffff">
<button class="btn btn-primary btn-lg tooltip tooltip-left" id="set_carpet_color" data-tooltip="Set Carpet Color">Set</button>
</div>
<div class="tile-action">
</div>
</div>
<br>
<br>
</div>
</div>
</div>
<script type="text/javascript" src="/static/js/coloris.min.js"></script>
<script>
let submit_button = document.getElementById('submit_pairing_code');
let pair_code_input = document.getElementById('pair_code');
let loading = document.getElementById('loading');
let enter_pairing_id = document.getElementById('enter_pairing_id');
let headset_details = document.getElementById('headset_details');
let hw_id_field = document.getElementById('hw_id');
let failure = document.getElementById('failure');
let set_room_id = document.getElementById('set_room_id');
let current_room = document.getElementById('current_room');
// check cookie
let hw_id = getCookie('hw_id');
if (hw_id != "") {
httpGetAsync('/api/get_state/' + hw_id, (resp) => {
console.log(resp);
let respData = JSON.parse(resp);
writeClass('hw_id', respData['user']['hw_id']);
writeValue('current_room', respData['user']['current_room']);
writeClass('date_created', respData['user']['date_created'] + "<br>" + timeSinceString(respData['user']['date_created']) + " ago");
writeClass('last_used', respData['user']['last_used'] + "<br>" + timeSinceString(respData['user']['last_used']) + " ago");
writeValue('user_color', respData['user']['user_color']);
writeValue('user_name', respData['user']['user_name']);
if (respData['room']) {
writeValue('tv_url', respData['room']['tv_url']);
writeValue('carpet_color', respData['room']['carpet_color']);
}
loading.style.display = "none";
headset_details.style.display = "block";
}, (status) => {
loading.style.display = "none";
failure.style.display = "block";
});
function setUserData(endpoint, data) {
httpPostAsync('/api/set_headset_details/' + hw_id + '/' + endpoint,
data,
(resp) => { console.log('success'); },
(status) => { console.log('fail'); }
);
}
function setRoomData(endpoint, data) {
httpPostAsync('/api/set_room_details/' + current_room.value + '/' + endpoint,
data,
(resp) => { console.log('success'); },
(status) => { console.log('fail'); }
);
}
set_room_id.addEventListener('click', () => {
setUserData('current_room', { "current_room": current_room.value });
});
document.getElementById('set_user_color').addEventListener('click', () => {
setUserData('user_color', { "user_color": document.getElementById('user_color').value });
});
document.getElementById('set_user_name').addEventListener('click', () => {
setUserData('user_name', { "user_name": document.getElementById('user_name').value });
});
document.getElementById('set_tv_url').addEventListener('click', () => {
setRoomData('tv_url', { "tv_url": document.getElementById('tv_url').value });
});
document.getElementById('set_carpet_color').addEventListener('click', () => {
setRoomData('carpet_color', { "carpet_color": document.getElementById('carpet_color').value });
});
} else {
window.location.href = "/pair";
}
Coloris({
el: '.coloris',
swatches: [
'#264653',
'#2a9d8f',
'#e9c46a',
'#f4a261',
'#e76f51',
'#d62828',
'#023e8a',
'#0077b6',
'#0096c7',
'#00b4d8',
'#48cae4',
]
});
</script>
</div>
</body>
</html>

View File

@ -2,19 +2,19 @@
<head> <head>
<link rel="apple-touch-icon" sizes="180x180" href="/favicons/apple-touch-icon.png"> <link rel="apple-touch-icon" sizes="180x180" href="/static/favicons/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicons/favicon-32x32.png"> <link rel="icon" type="image/png" sizes="32x32" href="/static/favicons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicons/favicon-16x16.png"> <link rel="icon" type="image/png" sizes="16x16" href="/static/favicons/favicon-16x16.png">
<link rel="manifest" href="/favicons/site.webmanifest"> <link rel="manifest" href="/static/favicons/site.webmanifest">
<link rel="mask-icon" href="/favicons/safari-pinned-tab.svg" color="#5bbad5"> <link rel="mask-icon" href="/static/favicons/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#b91d47"> <meta name="msapplication-TileColor" content="#b91d47">
<meta name="theme-color" content="#ffffff"> <meta name="theme-color" content="#ffffff">
<title>VEL Connect | Pair</title> <title>VEL Connect | Pair</title>
<link rel="stylesheet" href="/css/spectre.min.css"> <link rel="stylesheet" href="/static/css/spectre.min.css">
<script src="/js/util.js"></script> <script src="/static/js/util.js"></script>
<style> <style>
:root { :root {
--primary-color: #bc1f2d; --primary-color: #bc1f2d;
@ -54,7 +54,7 @@
<div class="card"> <div class="card">
<div class="card-image"> <div class="card-image">
<img src="/img/pair_code_screenshot.png" class="img-responsive"> <img src="/static/img/pair_code_screenshot.png" class="img-responsive">
</div> </div>
<div class="card-header"> <div class="card-header">
<div class="card-title h5">Enter Pairing Code</div> <div class="card-title h5">Enter Pairing Code</div>
@ -75,7 +75,7 @@
let submit_button = document.getElementById('submit_pairing_code'); let submit_button = document.getElementById('submit_pairing_code');
let pair_code_input = document.getElementById('pair_code'); let pair_code_input = document.getElementById('pair_code');
submit_button.addEventListener('click', () => { submit_button.addEventListener('click', () => {
httpGetAsync('https://connect.vel.workers.dev/api/pair_headset/' + pair_code_input.value, (resp) => { httpGetAsync('/api/pair_headset/' + pair_code_input.value, (resp) => {
console.log(resp); console.log(resp);
let respData = JSON.parse(resp); let respData = JSON.parse(resp);
if (respData['hw_id'] != '') { if (respData['hw_id'] != '') {

29
velconnect/website.py Normal file
View File

@ -0,0 +1,29 @@
from fastapi import APIRouter
from fastapi.responses import FileResponse
# APIRouter creates path operations for user module
router = APIRouter(
prefix="",
tags=["Website"],
)
@router.get('/')
def index():
return FileResponse("templates/index.html")
@router.get('/pair')
def pair():
return FileResponse("templates/pair.html")
@router.get('/success')
def success():
return FileResponse("templates/success.html")
@router.get('/failure')
def failure():
return FileResponse("templates/failure.html")