Add base directory support

- Base directory can be changed in config.py from the default / to, for example, /taiko-web/
  - See tools/nginx_subdir.conf for an example nginx configuration with a base directory
- Custom error pages can be used, they can be set in config.py
This commit is contained in:
KatieFrogs 2022-08-21 22:48:24 +02:00
parent ba1a6ab306
commit fd32ecb004
10 changed files with 120 additions and 61 deletions

1
.gitignore vendored
View File

@ -53,3 +53,4 @@ config.py
public/assets/song_skins
.venv
public/src/js/plugin
.hidden

92
app.py
View File

@ -15,7 +15,7 @@ import os
import time
from functools import wraps
from flask import Flask, g, jsonify, render_template, request, abort, redirect, session, flash, make_response
from flask import Flask, g, jsonify, render_template, request, abort, redirect, session, flash, make_response, send_from_directory
from flask_caching import Cache
from flask_session import Session
from flask_wtf.csrf import CSRFProtect, generate_csrf, CSRFError
@ -33,6 +33,7 @@ def take_config(name, required=False):
app = Flask(__name__)
client = MongoClient(host=take_config('MONGO', required=True)['host'])
basedir = take_config('BASEDIR') or '/'
app.secret_key = take_config('SECRET_KEY') or 'change-me'
app.config['SESSION_TYPE'] = 'redis'
@ -79,8 +80,8 @@ def generate_hash(id, form):
raise HashException('Invalid response from %s (status code %s)' % (resp.url, resp.status_code))
md5.update(resp.content)
else:
if url.startswith("/"):
url = url[1:]
if url.startswith(basedir):
url = url[len(basedir):]
path = os.path.normpath(os.path.join("public", url))
if not os.path.isfile(path):
raise HashException("File not found: %s" % (os.path.abspath(path)))
@ -129,6 +130,7 @@ def before_request_func():
def get_config(credentials=False):
config_out = {
'basedir': basedir,
'songs_baseurl': take_config('SONGS_BASEURL', required=True),
'assets_baseurl': take_config('ASSETS_BASEURL', required=True),
'email': take_config('EMAIL'),
@ -138,6 +140,10 @@ def get_config(credentials=False):
'preview_type': take_config('PREVIEW_TYPE') or 'mp3',
'multiplayer_url': take_config('MULTIPLAYER_URL')
}
relative_urls = ['songs_baseurl', 'assets_baseurl']
for name in relative_urls:
if not config_out[name].startswith("/") and not config_out[name].startswith("http://") and not config_out[name].startswith("https://"):
config_out[name] = basedir + config_out[name]
if credentials:
google_credentials = take_config('GOOGLE_CREDENTIALS')
min_level = google_credentials['min_level'] or 0
@ -200,24 +206,24 @@ def is_hex(input):
return False
@app.route('/')
@app.route(basedir)
def route_index():
version = get_version()
return render_template('index.html', version=version, config=get_config())
@app.route('/api/csrftoken')
@app.route(basedir + 'api/csrftoken')
def route_csrftoken():
return jsonify({'status': 'ok', 'token': generate_csrf()})
@app.route('/admin')
@app.route(basedir + 'admin')
@admin_required(level=50)
def route_admin():
return redirect('/admin/songs')
return redirect(basedir + 'admin/songs')
@app.route('/admin/songs')
@app.route(basedir + 'admin/songs')
@admin_required(level=50)
def route_admin_songs():
songs = sorted(list(db.songs.find({})), key=lambda x: x['id'])
@ -226,7 +232,7 @@ def route_admin_songs():
return render_template('admin_songs.html', songs=songs, admin=user, categories=list(categories), config=get_config())
@app.route('/admin/songs/<int:id>')
@app.route(basedir + 'admin/songs/<int:id>')
@admin_required(level=50)
def route_admin_songs_id(id):
song = db.songs.find_one({'id': id})
@ -242,7 +248,7 @@ def route_admin_songs_id(id):
song=song, categories=categories, song_skins=song_skins, makers=makers, admin=user, config=get_config())
@app.route('/admin/songs/new')
@app.route(basedir + 'admin/songs/new')
@admin_required(level=100)
def route_admin_songs_new():
categories = list(db.categories.find({}))
@ -254,7 +260,7 @@ def route_admin_songs_new():
return render_template('admin_song_new.html', categories=categories, song_skins=song_skins, makers=makers, config=get_config(), id=seq_new)
@app.route('/admin/songs/new', methods=['POST'])
@app.route(basedir + 'admin/songs/new', methods=['POST'])
@admin_required(level=100)
def route_admin_songs_new_post():
output = {'title_lang': {}, 'subtitle_lang': {}, 'courses': {}}
@ -303,10 +309,10 @@ def route_admin_songs_new_post():
db.seq.update_one({'name': 'songs'}, {'$set': {'value': seq_new}}, upsert=True)
return redirect('/admin/songs/%s' % str(seq_new))
return redirect(basedir + 'admin/songs/%s' % str(seq_new))
@app.route('/admin/songs/<int:id>', methods=['POST'])
@app.route(basedir + 'admin/songs/<int:id>', methods=['POST'])
@admin_required(level=50)
def route_admin_songs_id_post(id):
song = db.songs.find_one({'id': id})
@ -356,10 +362,10 @@ def route_admin_songs_id_post(id):
if not hash_error:
flash('Changes saved.')
return redirect('/admin/songs/%s' % id)
return redirect(basedir + 'admin/songs/%s' % id)
@app.route('/admin/songs/<int:id>/delete', methods=['POST'])
@app.route(basedir + 'admin/songs/<int:id>/delete', methods=['POST'])
@admin_required(level=100)
def route_admin_songs_id_delete(id):
song = db.songs.find_one({'id': id})
@ -368,10 +374,10 @@ def route_admin_songs_id_delete(id):
db.songs.delete_one({'id': id})
flash('Song deleted.')
return redirect('/admin/songs')
return redirect(basedir + 'admin/songs')
@app.route('/admin/users')
@app.route(basedir + 'admin/users')
@admin_required(level=50)
def route_admin_users():
user = db.users.find_one({'username': session.get('username')})
@ -379,7 +385,7 @@ def route_admin_users():
return render_template('admin_users.html', config=get_config(), max_level=max_level, username='', level='')
@app.route('/admin/users', methods=['POST'])
@app.route(basedir + 'admin/users', methods=['POST'])
@admin_required(level=50)
def route_admin_users_post():
admin_name = session.get('username')
@ -411,7 +417,7 @@ def route_admin_users_post():
return render_template('admin_users.html', config=get_config(), max_level=max_level, username=username, level=level)
@app.route('/api/preview')
@app.route(basedir + 'api/preview')
@app.cache.cached(timeout=15, query_string=True)
def route_api_preview():
song_id = request.args.get('id', None)
@ -432,7 +438,7 @@ def route_api_preview():
return redirect(get_config()['songs_baseurl'] + '%s/preview.mp3' % song_id)
@app.route('/api/songs')
@app.route(basedir + 'api/songs')
@app.cache.cached(timeout=15)
def route_api_songs():
songs = list(db.songs.find({'enabled': True}, {'_id': False, 'enabled': False}))
@ -460,20 +466,20 @@ def route_api_songs():
return jsonify(songs)
@app.route('/api/categories')
@app.route(basedir + 'api/categories')
@app.cache.cached(timeout=15)
def route_api_categories():
categories = list(db.categories.find({},{'_id': False}))
return jsonify(categories)
@app.route('/api/config')
@app.route(basedir + 'api/config')
@app.cache.cached(timeout=15)
def route_api_config():
config = get_config(credentials=True)
return jsonify(config)
@app.route('/api/register', methods=['POST'])
@app.route(basedir + 'api/register', methods=['POST'])
def route_api_register():
data = request.get_json()
if not schema.validate(data, schema.register):
@ -514,7 +520,7 @@ def route_api_register():
return jsonify({'status': 'ok', 'username': username, 'display_name': username, 'don': don})
@app.route('/api/login', methods=['POST'])
@app.route(basedir + 'api/login', methods=['POST'])
def route_api_login():
data = request.get_json()
if not schema.validate(data, schema.login):
@ -541,14 +547,14 @@ def route_api_login():
return jsonify({'status': 'ok', 'username': result['username'], 'display_name': result['display_name'], 'don': don})
@app.route('/api/logout', methods=['POST'])
@app.route(basedir + 'api/logout', methods=['POST'])
@login_required
def route_api_logout():
session.clear()
return jsonify({'status': 'ok'})
@app.route('/api/account/display_name', methods=['POST'])
@app.route(basedir + 'api/account/display_name', methods=['POST'])
@login_required
def route_api_account_display_name():
data = request.get_json()
@ -568,7 +574,7 @@ def route_api_account_display_name():
return jsonify({'status': 'ok', 'display_name': display_name})
@app.route('/api/account/don', methods=['POST'])
@app.route(basedir + 'api/account/don', methods=['POST'])
@login_required
def route_api_account_don():
data = request.get_json()
@ -593,7 +599,7 @@ def route_api_account_don():
return jsonify({'status': 'ok', 'don': {'body_fill': don_body_fill, 'face_fill': don_face_fill}})
@app.route('/api/account/password', methods=['POST'])
@app.route(basedir + 'api/account/password', methods=['POST'])
@login_required
def route_api_account_password():
data = request.get_json()
@ -621,7 +627,7 @@ def route_api_account_password():
return jsonify({'status': 'ok'})
@app.route('/api/account/remove', methods=['POST'])
@app.route(basedir + 'api/account/remove', methods=['POST'])
@login_required
def route_api_account_remove():
data = request.get_json()
@ -640,7 +646,7 @@ def route_api_account_remove():
return jsonify({'status': 'ok'})
@app.route('/api/scores/save', methods=['POST'])
@app.route(basedir + 'api/scores/save', methods=['POST'])
@login_required
def route_api_scores_save():
data = request.get_json()
@ -663,7 +669,7 @@ def route_api_scores_save():
return jsonify({'status': 'ok'})
@app.route('/api/scores/get')
@app.route(basedir + 'api/scores/get')
@login_required
def route_api_scores_get():
username = session.get('username')
@ -680,7 +686,7 @@ def route_api_scores_get():
return jsonify({'status': 'ok', 'scores': scores, 'username': user['username'], 'display_name': user['display_name'], 'don': don})
@app.route('/privacy')
@app.route(basedir + 'privacy')
def route_api_privacy():
last_modified = time.strftime('%d %B %Y', time.gmtime(os.path.getmtime('templates/privacy.txt')))
integration = take_config('GOOGLE_CREDENTIALS')['gdrive_enabled'] if take_config('GOOGLE_CREDENTIALS') else False
@ -706,10 +712,26 @@ def make_preview(song_id, song_type, song_ext, preview):
return prev_path
error_pages = take_config('ERROR_PAGES') or {}
def create_error_page(code, url):
if url.startswith("http://") or url.startswith("https://"):
resp = requests.get(url)
if resp.status_code == 200:
app.register_error_handler(code, lambda e: (resp.content, code))
else:
if url.startswith(basedir):
url = url[len(basedir):]
path = os.path.normpath(os.path.join("public", url))
if os.path.isfile(path):
app.register_error_handler(code, lambda e: (send_from_directory(".", path), code))
for code in error_pages:
if error_pages[code]:
create_error_page(code, error_pages[code])
if __name__ == '__main__':
import argparse
from flask import send_from_directory
parser = argparse.ArgumentParser(description='Run the taiko-web development server.')
parser.add_argument('port', type=int, metavar='PORT', nargs='?', default=34801, help='Port to listen on.')
@ -717,11 +739,11 @@ if __name__ == '__main__':
parser.add_argument('-d', '--debug', action='store_true', help='Enable debug mode.')
args = parser.parse_args()
@app.route('/src/<path:path>')
@app.route(basedir + 'src/<path:path>')
def send_src(path):
return send_from_directory('public/src', path)
@app.route('/assets/<path:path>')
@app.route(basedir + 'assets/<path:path>')
def send_assets(path):
return send_from_directory('public/assets', path)

View File

@ -1,3 +1,6 @@
# The base URL for Taiko Web, with trailing slash.
BASEDIR = '/'
# The full URL base asset URL, with trailing slash.
ASSETS_BASEURL = '/assets/'
@ -7,6 +10,11 @@ SONGS_BASEURL = '/songs/'
# Multiplayer websocket URL. Defaults to /p2 if blank.
MULTIPLAYER_URL = ''
# Send static files for custom error pages
ERROR_PAGES = {
404: ''
}
# The email address to display in the "About Simulator" menu.
EMAIL = None

View File

@ -300,7 +300,7 @@ class CustomSongs{
this.loading(true)
var importSongs = new ImportSongs(true)
if(!gpicker){
var gpickerPromise = loader.loadScript("/src/js/gpicker.js").then(() => {
var gpickerPromise = loader.loadScript("src/js/gpicker.js").then(() => {
gpicker = new Gpicker()
})
}else{

View File

@ -13,11 +13,11 @@ class Loader{
var promises = []
promises.push(this.ajax("/src/views/loader.html").then(page => {
promises.push(this.ajax("src/views/loader.html").then(page => {
this.screen.innerHTML = page
}))
promises.push(this.ajax("/api/config").then(conf => {
promises.push(this.ajax("api/config").then(conf => {
gameConfig = JSON.parse(conf)
}))
@ -39,7 +39,7 @@ class Loader{
assets.js.push("lib/oggmented-wasm.js")
}
assets.js.forEach(name => {
this.addPromise(this.loadScript("/src/js/" + name), "/src/js/" + name)
this.addPromise(this.loadScript("src/js/" + name), "src/js/" + name)
})
var pageVersion = versionLink.href
@ -59,7 +59,7 @@ class Loader{
assets.css.forEach(name => {
var stylesheet = document.createElement("link")
stylesheet.rel = "stylesheet"
stylesheet.href = "/src/css/" + name + this.queryString
stylesheet.href = "src/css/" + name + this.queryString
document.head.appendChild(stylesheet)
})
var checkStyles = () => {
@ -124,13 +124,13 @@ class Loader{
assets.views.forEach(name => {
var id = this.getFilename(name)
var url = "/src/views/" + name + this.queryString
var url = "src/views/" + name + this.queryString
this.addPromise(this.ajax(url).then(page => {
assets.pages[id] = page
}), url)
})
this.addPromise(this.ajax("/api/categories").then(cats => {
this.addPromise(this.ajax("api/categories").then(cats => {
assets.categories = JSON.parse(cats)
assets.categories.forEach(cat => {
if(cat.song_skin){
@ -150,7 +150,7 @@ class Loader{
infoFill: "#656565"
}
})
}), "/api/categories")
}), "api/categories")
var url = gameConfig.assets_baseurl + "img/vectors.json" + this.queryString
this.addPromise(this.ajax(url).then(response => {
@ -159,7 +159,7 @@ class Loader{
this.afterJSCount =
[
"/api/songs",
"api/songs",
"blurPerformance",
"categories"
].length +
@ -178,7 +178,7 @@ class Loader{
style.appendChild(document.createTextNode(css.join("\n")))
document.head.appendChild(style)
this.addPromise(this.ajax("/api/songs").then(songs => {
this.addPromise(this.ajax("api/songs").then(songs => {
songs = JSON.parse(songs)
songs.forEach(song => {
var directory = gameConfig.songs_baseurl + song.id + "/"
@ -203,7 +203,7 @@ class Loader{
})
assets.songsDefault = songs
assets.songs = assets.songsDefault
}), "/api/songs")
}), "api/songs")
var categoryPromises = []
assets.categories //load category backgrounds to DOM
@ -276,7 +276,7 @@ class Loader{
}), "blurPerformance")
if(gameConfig.accounts){
this.addPromise(this.ajax("/api/scores/get").then(response => {
this.addPromise(this.ajax("api/scores/get").then(response => {
response = JSON.parse(response)
if(response.status === "ok"){
account.loggedIn = true
@ -286,7 +286,7 @@ class Loader{
scoreStorage.load(response.scores)
pageEvents.send("login", account.username)
}
}), "/api/scores/get")
}), "api/scores/get")
}
settings = new Settings()

View File

@ -32,7 +32,7 @@ class P2Connection{
if(this.closed && !this.disabled){
this.closed = false
var wsProtocol = location.protocol == "https:" ? "wss:" : "ws:"
this.socket = new WebSocket(gameConfig.multiplayer_url ? gameConfig.multiplayer_url : wsProtocol + "//" + location.host + "/p2")
this.socket = new WebSocket(gameConfig.multiplayer_url ? gameConfig.multiplayer_url : wsProtocol + "//" + location.host + location.pathname + "p2")
pageEvents.race(this.socket, "open", "close").then(response => {
if(response.type === "open"){
return this.openEvent()

View File

@ -7,13 +7,13 @@
<meta name="viewport" content="width=device-width, user-scalable=no">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;700&display=swap" rel="stylesheet">
<link href="/src/css/admin.css" rel="stylesheet">
<link href="{{config.basedir}}src/css/admin.css" rel="stylesheet">
</head>
<body>
<header>
<div class="nav">
<a href="/admin/songs">Songs</a>
<a href="/admin/users">Users</a>
<a href="{{config.basedir}}admin/songs">Songs</a>
<a href="{{config.basedir}}admin/users">Users</a>
</div>
</header>

View File

@ -1,14 +1,14 @@
{% extends 'admin.html' %}
{% block content %}
{% if admin.user_level >= 100 %}
<a href="/admin/songs/new" class="side-button">New song</a>
<a href="{{config.basedir}}admin/songs/new" class="side-button">New song</a>
{% endif %}
<h1>Songs</h1>
{% for message in get_flashed_messages() %}
<div class="message">{{ message }}</div>
{% endfor %}
{% for song in songs %}
<a href="/admin/songs/{{ song.id }}" class="song-link">
<a href="{{config.basedir}}admin/songs/{{ song.id }}" class="song-link">
<div class="song">
{% if song.title_lang.en and song.title_lang.en != song.title %}
<p><span class="song-id">{{ song.id }}.</span>

View File

@ -11,12 +11,12 @@
<meta name="robots" content="noimageindex">
<meta name="color-scheme" content="only light">
<link rel="stylesheet" href="/src/css/loader.css?{{version.commit_short}}">
<link rel="stylesheet" href="src/css/loader.css?{{version.commit_short}}">
<script src="/src/js/assets.js?{{version.commit_short}}"></script>
<script src="/src/js/strings.js?{{version.commit_short}}"></script>
<script src="/src/js/pageevents.js?{{version.commit_short}}"></script>
<script src="/src/js/loader.js?{{version.commit_short}}"></script>
<script src="src/js/assets.js?{{version.commit_short}}"></script>
<script src="src/js/strings.js?{{version.commit_short}}"></script>
<script src="src/js/pageevents.js?{{version.commit_short}}"></script>
<script src="src/js/loader.js?{{version.commit_short}}"></script>
</head>
<body>
@ -29,8 +29,8 @@
<a href="{{version.url}}" target="_blank" rel="noopener" id="version-link" class="stroke-sub" alt="taiko-web (unknown version)">taiko-web (unknown version)</a>
{% endif %}
</div>
<script src="/src/js/browsersupport.js?{{version.commit_short}}"></script>
<script src="/src/js/main.js?{{version.commit_short}}"></script>
<script src="src/js/browsersupport.js?{{version.commit_short}}"></script>
<script src="src/js/main.js?{{version.commit_short}}"></script>
<noscript>
<div data-nosnippet id="unsupportedBrowser">
<div id="unsupportedWarn">!</div>

28
tools/nginx_subdir.conf Normal file
View File

@ -0,0 +1,28 @@
server {
listen 80;
#server_name taiko.example.com;
location /taiko-web/ {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $server_name;
proxy_pass http://127.0.0.1:34801;
}
location ~ ^/taiko-web/(assets|songs|src)/ {
rewrite ^/taiko-web/(.*) /$1 break;
root /srv/taiko-web/public;
location ~ ^/taiko-web/songs/([0-9]+)/preview\.mp3$ {
set $id $1;
rewrite ^/taiko-web/(.*) /$1 break;
try_files $uri /taiko-web/api/preview?id=$id;
}
}
location /taiko-web/p2 {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_pass http://127.0.0.1:34802;
}
}