diff --git a/bazarr/api.py b/bazarr/api.py index 8e7957064..d794c20a8 100644 --- a/bazarr/api.py +++ b/bazarr/api.py @@ -31,7 +31,7 @@ from list_subtitles import store_subtitles, store_subtitles_movie, series_scan_s list_missing_subtitles, list_missing_subtitles_movies from utils import history_log, history_log_movie, blacklist_log, blacklist_delete, blacklist_delete_all, \ blacklist_log_movie, blacklist_delete_movie, blacklist_delete_all_movie, get_sonarr_version, get_radarr_version, \ - delete_subtitles, subtitles_apply_mods, translate_subtitles_file + delete_subtitles, subtitles_apply_mods, translate_subtitles_file, check_credentials from get_providers import get_providers, get_providers_auth, list_throttled_providers, reset_throttled_providers, \ get_throttled_providers, set_throttled_providers from event_handler import event_stream @@ -52,27 +52,9 @@ api = Api(api_bp) None_Keys = ['null', 'undefined', ''] -def check_credentials(user, pw): - username = settings.auth.username - password = settings.auth.password - if hashlib.md5(pw.encode('utf-8')).hexdigest() == password and user == username: - return True - return False - - def authenticate(actual_method): @wraps(actual_method) def wrapper(*args, **kwargs): - if settings.auth.type == 'basic': - auth = request.authorization - if not (auth and check_credentials(request.authorization.username, request.authorization.password)): - return ('Unauthorized', 401, { - 'WWW-Authenticate': 'Basic realm="Login Required"' - }) - elif settings.auth.type == 'form': - if 'logged_in' not in session: - return abort(401, message="Unauthorized") - apikey_settings = settings.auth.apikey apikey_get = request.args.get('apikey') apikey_post = request.form.get('apikey') @@ -314,12 +296,9 @@ class SystemAccount(Resource): session['logged_in'] = True return '', 204 elif action == 'logout': - if settings.auth.type == 'basic': - return abort(401) - elif settings.auth.type == 'form': - session.clear() - gc.collect() - return '', 204 + session.clear() + gc.collect() + return '', 204 return '', 401 diff --git a/bazarr/main.py b/bazarr/main.py index bd23d6058..b1b147f3c 100644 --- a/bazarr/main.py +++ b/bazarr/main.py @@ -39,6 +39,7 @@ from get_movies import * from check_update import apply_update, check_if_new_update, check_releases from server import app, webserver from functools import wraps +from utils import check_credentials # Install downloaded update if bazarr_version != '': @@ -57,44 +58,68 @@ login_auth = settings.auth.type update_notifier() + +def check_login(actual_method): + @wraps(actual_method) + def wrapper(*args, **kwargs): + if settings.auth.type == 'basic': + auth = request.authorization + if not (auth and check_credentials(request.authorization.username, request.authorization.password)): + return ('Unauthorized', 401, { + 'WWW-Authenticate': 'Basic realm="Login Required"' + }) + elif settings.auth.type == 'form': + if 'logged_in' not in session: + return abort(401, message="Unauthorized") + actual_method(*args, **kwargs) + + @app.errorhandler(404) def page_not_found(e): return redirect(base_url, code=302) + @app.route('/', defaults={'path': ''}) @app.route('/') def catch_all(path): - return render_template("index.html") + auth = True + if settings.auth.type == 'basic': + auth = request.authorization + if not (auth and check_credentials(request.authorization.username, request.authorization.password)): + return ('Unauthorized', 401, { + 'WWW-Authenticate': 'Basic realm="Login Required"' + }) + elif settings.auth.type == 'form': + if 'logged_in' not in session: + auth = False - -@app.context_processor -def template_variable_processor(): - updated = False try: updated = database.execute("SELECT updated FROM system", only_one=True)['updated'] except: - pass + updated = False inject = dict() - inject["apiKey"] = settings.auth.apikey inject["baseUrl"] = base_url inject["canUpdate"] = not args.no_update inject["hasUpdate"] = updated != '0' + if auth: + inject["apiKey"] = settings.auth.apikey + template_url = base_url if not template_url.endswith("/"): template_url += "/" - return dict(BAZARR_SERVER_INJECT=inject, baseUrl=template_url) - + return render_template("index.html", BAZARR_SERVER_INJECT=inject, baseUrl=template_url) +@check_login @app.route('/bazarr.log') def download_log(): - return send_file(os.path.join(args.config_dir, 'log', 'bazarr.log'), cache_timeout=0, as_attachment=True) +@check_login @app.route('/images/series/', methods=['GET']) def series_images(url): url = url.strip("/") @@ -109,6 +134,7 @@ def series_images(url): return Response(stream_with_context(req.iter_content(2048)), content_type=req.headers['content-type']) +@check_login @app.route('/images/movies/', methods=['GET']) def movies_images(url): apikey = settings.radarr.apikey @@ -135,6 +161,7 @@ def configured(): database.execute("UPDATE system SET configured = 1") +@check_login @app.route('/test', methods=['GET']) @app.route('/test//', methods=['GET']) def proxy(protocol, url): diff --git a/bazarr/utils.py b/bazarr/utils.py index 9dc15b11e..ede3cc2d1 100644 --- a/bazarr/utils.py +++ b/bazarr/utils.py @@ -398,3 +398,8 @@ def translate_subtitles_file(video_path, source_srt_file, to_lang, forced, hi): subs.save(dest_srt_file) return dest_srt_file + +def check_credentials(user, pw): + username = settings.auth.username + password = settings.auth.password + return hashlib.md5(pw.encode('utf-8')).hexdigest() == password and user == username \ No newline at end of file diff --git a/frontend/src/@redux/actions/site.ts b/frontend/src/@redux/actions/site.ts index b0f6a0938..9ea9e6af5 100644 --- a/frontend/src/@redux/actions/site.ts +++ b/frontend/src/@redux/actions/site.ts @@ -1,7 +1,6 @@ import { createAction } from "redux-actions"; import { BadgesApi } from "../../apis"; import { - SITE_AUTH_SUCCESS, SITE_BADGE_UPDATE, SITE_INITIALIZED, SITE_INITIALIZE_FAILED, @@ -28,8 +27,6 @@ const siteInitialized = createAction(SITE_INITIALIZED); export const siteRedirectToAuth = createAction(SITE_NEED_AUTH); -export const siteAuthSuccess = createAction(SITE_AUTH_SUCCESS); - export const badgeUpdateAll = createAsyncAction(SITE_BADGE_UPDATE, () => BadgesApi.all() ); diff --git a/frontend/src/@redux/constants/index.ts b/frontend/src/@redux/constants/index.ts index 494841f5e..d4be20f04 100644 --- a/frontend/src/@redux/constants/index.ts +++ b/frontend/src/@redux/constants/index.ts @@ -31,7 +31,6 @@ export const MOVIES_UPDATE_RANGE = "MOVIES_UPDATE_RANGE"; export const MOVIES_UPDATE_BLACKLIST = "UPDATE_MOVIES_BLACKLIST"; // Site Action -export const SITE_AUTH_SUCCESS = "SITE_AUTH_SUCCESS"; export const SITE_NEED_AUTH = "SITE_NEED_AUTH"; export const SITE_INITIALIZED = "SITE_SYSTEM_INITIALIZED"; export const SITE_INITIALIZE_FAILED = "SITE_INITIALIZE_FAILED"; diff --git a/frontend/src/@redux/reducers/site.ts b/frontend/src/@redux/reducers/site.ts index 0d6f761dd..78191908a 100644 --- a/frontend/src/@redux/reducers/site.ts +++ b/frontend/src/@redux/reducers/site.ts @@ -1,7 +1,7 @@ import { Action, handleActions } from "redux-actions"; import { storage } from "../../@storage/local"; +import apis from "../../apis"; import { - SITE_AUTH_SUCCESS, SITE_BADGE_UPDATE, SITE_INITIALIZED, SITE_INITIALIZE_FAILED, @@ -23,14 +23,15 @@ function updateLocalStorage(): Partial { const reducer = handleActions( { - [SITE_NEED_AUTH]: (state) => ({ - ...state, - auth: false, - }), - [SITE_AUTH_SUCCESS]: (state) => ({ - ...state, - auth: true, - }), + [SITE_NEED_AUTH]: (state) => { + if (process.env.NODE_ENV !== "development") { + apis.danger_resetApi("NEED_AUTH"); + } + return { + ...state, + auth: false, + }; + }, [SITE_INITIALIZED]: (state) => ({ ...state, initialized: true, diff --git a/frontend/src/@types/window.d.ts b/frontend/src/@types/window.d.ts index 234052902..ab5f39761 100644 --- a/frontend/src/@types/window.d.ts +++ b/frontend/src/@types/window.d.ts @@ -6,7 +6,7 @@ declare global { export interface BazarrServer { baseUrl: string; - apiKey: string; + apiKey?: string; canUpdate: boolean; hasUpdate: boolean; } diff --git a/frontend/src/App/Header.tsx b/frontend/src/App/Header.tsx index 0786cd35d..27a6c5c1f 100644 --- a/frontend/src/App/Header.tsx +++ b/frontend/src/App/Header.tsx @@ -58,7 +58,7 @@ const Header: FunctionComponent = () => { const [settings] = useSystemSettings(); - const canLogout = (settings.data?.auth.type ?? "none") !== "none"; + const canLogout = (settings.data?.auth.type ?? "none") === "form"; const toggleSidebar = useContext(SidebarToggleContext); diff --git a/frontend/src/Auth/index.tsx b/frontend/src/Auth/index.tsx index 50b756daa..0293b1fa2 100644 --- a/frontend/src/Auth/index.tsx +++ b/frontend/src/Auth/index.tsx @@ -9,8 +9,7 @@ import { Spinner, } from "react-bootstrap"; import { Redirect } from "react-router-dom"; -import { siteAuthSuccess } from "../@redux/actions"; -import { useReduxAction, useReduxStore } from "../@redux/hooks/base"; +import { useReduxStore } from "../@redux/hooks/base"; import logo from "../@static/logo128.png"; import { SystemApi } from "../apis"; import "./style.scss"; @@ -29,7 +28,7 @@ const AuthPage: FunctionComponent = () => { setTimeout(() => setError(""), 2000); }, []); - const onSuccess = useReduxAction(siteAuthSuccess); + const onSuccess = useCallback(() => window.location.reload(), []); const authState = useReduxStore((s) => s.site.auth); diff --git a/frontend/src/apis/index.ts b/frontend/src/apis/index.ts index 3707b9bab..b583d3a84 100644 --- a/frontend/src/apis/index.ts +++ b/frontend/src/apis/index.ts @@ -17,13 +17,13 @@ class Api { } } - initialize(url: string, apikey: string) { + initialize(url: string, apikey?: string) { this.axios = Axios.create({ baseURL: url, }); this.axios.defaults.headers.post["Content-Type"] = "application/json"; - this.axios.defaults.headers.common["X-API-KEY"] = apikey; + this.axios.defaults.headers.common["X-API-KEY"] = apikey ?? "AUTH_NEEDED"; this.source = Axios.CancelToken.source(); @@ -56,6 +56,10 @@ class Api { ); } + danger_resetApi(apikey: string) { + this.axios.defaults.headers.common["X-API-KEY"] = apikey; + } + onOnline() { const offline = reduxStore.getState().site.offline; if (offline) { diff --git a/frontend/src/apis/utils.ts b/frontend/src/apis/utils.ts index 2ea911de6..9601a692b 100644 --- a/frontend/src/apis/utils.ts +++ b/frontend/src/apis/utils.ts @@ -1,4 +1,4 @@ -import Axios, { AxiosInstance } from "axios"; +import apis from "."; type UrlTestResponse = | { @@ -11,35 +11,14 @@ type UrlTestResponse = }; class RequestUtils { - private axios!: AxiosInstance; - - constructor() { - if (process.env.NODE_ENV === "development") { - this.recreateAxios("/", process.env.REACT_APP_APIKEY!); - } else { - const baseUrl = - window.Bazarr.baseUrl === "/" ? "/" : `${window.Bazarr.baseUrl}/`; - this.recreateAxios(baseUrl, window.Bazarr.apiKey); - } - } - - private recreateAxios(url: string, apikey: string) { - this.axios = Axios.create({ - baseURL: url, - }); - - this.axios.defaults.headers.post["Content-Type"] = "application/json"; - this.axios.defaults.headers.common["x-api-key"] = apikey; - } - urlTest( protocol: string, url: string, params?: any ): Promise { return new Promise((resolve, reject) => { - this.axios - .get(`test/${protocol}/${url}api/system/status`, { params }) + apis.axios + .get(`../test/${protocol}/${url}api/system/status`, { params }) .then((result) => resolve(result.data)) .catch(reject); });