From 950db90547e7c81cf3deee0bd1fa744cdf04dd2d Mon Sep 17 00:00:00 2001 From: Ed Date: Fri, 12 May 2023 00:18:49 +0200 Subject: [PATCH] Added login via API for app --- api.py | 119 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ app.py | 17 +++++---- ext.py | 5 ++- 3 files changed, 132 insertions(+), 9 deletions(-) create mode 100644 api.py diff --git a/api.py b/api.py new file mode 100644 index 0000000..e20afd9 --- /dev/null +++ b/api.py @@ -0,0 +1,119 @@ +from sanic import response +from sanic import Blueprint, exceptions +from ext import * +from config import * +import sqlite3 +import smtplib +from email.mime.text import MIMEText +import random +import string + +bp = Blueprint("api", url_prefix="/manage/api") + +@bp.route("/members.json") +async def api_members(request): + + ret = [] + + for o in sorted(request.app.ctx.om.cache.values(), key=lambda x: len(x.room_members), reverse=True): + if o.status in ['c', 'e']: continue + + ret.append({ + 'code': o.code, + 'sponsorship': o.sponsorship, + 'is_fursuiter': o.is_fursuiter, + 'name': o.name, + 'has_early': o.has_early, + 'has_late': o.has_late, + 'propic': o.ans('propic'), + 'propic_fursuiter': o.ans('propic_fursuiter'), + 'staff_role': o.ans('staff_role'), + 'country': o.country, + 'is_checked_in': False + }) + + return response.json(ret) + +@bp.route("/events.json") +async def show_events(request): + + with sqlite3.connect('data/event.db') as db: + db.row_factory = sqlite3.Row + events = db.execute('SELECT * FROM event ORDER BY start ASC') + return response.json([dict(x) for x in events]) + +@bp.route("/achievements.json") +async def show_events(request): + + code = request.args.get("code") + + with sqlite3.connect('data/achievement.db') as db: + db.row_factory = sqlite3.Row + events = db.execute('SELECT * FROM achievement ORDER BY ' + ('random() LIMIT 5' if code else 'points')) + return response.json([{'won_at': '2023-05-05T21:00Z' if code else None, **dict(x), 'about': 'This is instructions on how to win the field.'} for x in events]) + +@bp.get("/logout") +async def logout(request): + if not request.token: + return response.json({'ok': False, 'error': 'You need to provide a token.'}, status=401) + + user = await request.app.ctx.om.get_order(code=request.token[:5]) + if not user or user.api_token != request.token[5:]: + return response.json({'ok': False, 'error': 'The token you have provided is not valid.'}, status=401) + + user.edit_answer('api_token', None) + await user.send_answers() + + return response.json({'ok': True, 'message': 'You have been logged out and this token has been destroyed.'}) + print(request.token) + +@bp.get("/get_token//") +async def get_token_from_code(request, code, login_code): + if not code in request.app.ctx.login_codes: + return response.json({'ok': False, 'error': 'You need to reauthenticate. The code has expired.'}, status=401) + + if request.app.ctx.login_codes[code][1] == 0: + del request.app.ctx.login_codes[code] + return response.json({'ok': False, 'error': 'Too many tries. Please reauthenticate again.'}, status=401) + + if request.app.ctx.login_codes[code][0] != login_code: + request.app.ctx.login_codes[code][1] -= 1 + return response.json({'ok': False, 'error': 'The login code is incorrect. Try again.'}, status=401) + + user = await request.app.ctx.om.get_order(code=code) + token = ''.join(random.choice(string.ascii_letters) for _ in range(48)) + user.edit_answer('api_token', token) + await user.send_answers() + + return response.json({'ok': True, 'token': code+token}) + +@bp.route("/get_token/") +async def get_token(request, code): + user = await request.app.ctx.om.get_order(code=code) + + if not user: + return response.json({'ok': False, 'error': 'The user you have requested does not exist.'}, status=404) + + if user.status != 'paid': + return response.json({'ok': False, 'error': 'This user is not allowed to login.'}, status=401) + + if not user.email: + return response.json({'ok': False, 'error': 'This user has not provided their email.'}, status=401) + + request.app.ctx.login_codes[code] = [''.join(random.choice(string.digits) for _ in range(6)), 3] + + try: + msg = MIMEText(f"Hello {user.name}!\n\nWe have received a request to login in the app. If you didn't do this, please ignore this email. Somebody is probably playing with you.\n\nYour login code is: {request.app.ctx.login_codes[code][0]}\n\nPlease do not tell this to anybody!") + msg['Subject'] = '[Furizon] Your login code' + msg['From'] = 'Furizon ' + msg['To'] = f"{user.name} <{user.email}>" + + s = smtplib.SMTP_SSL(SMTP_HOST) + s.login(SMTP_USER, SMTP_PASSWORD) + s.sendmail(msg['From'], msg['to'], msg.as_string()) + s.quit() + except: + return response.json({'ok': False, 'error': 'There has been an issue sending your e-mail. Please try again later or report to an admin.'}, status=500) + + return response.json({'ok': True, 'message': 'A login code has been sent to your email.'}) + diff --git a/app.py b/app.py index eef00a0..eac9b4d 100644 --- a/app.py +++ b/app.py @@ -29,18 +29,19 @@ app.blueprint([room_bp, propic_bp, export_bp, stats_bp, api_bp, carpooling_bp]) @app.exception(exceptions.SanicException) async def clear_session(request, exception): tpl = app.ctx.tpl.get_template('error.html') - response = html(tpl.render(exception=exception)) + r = html(tpl.render(exception=exception)) if exception.status_code == 403: - del response.cookies["foxo_code"] - del response.cookies["foxo_secret"] - return response + del r.cookies["foxo_code"] + del r.cookies["foxo_secret"] + return r @app.before_server_start async def main_start(*_): print(">>>>>> main_start <<<<<<") app.ctx.om = OrderManager() + app.ctx.login_codes = {} app.ctx.tpl = Environment(loader=FileSystemLoader("tpl"), autoescape=True) app.ctx.tpl.globals.update(time=time) app.ctx.tpl.globals.update(PROPIC_DEADLINE=PROPIC_DEADLINE) @@ -58,7 +59,7 @@ async def gen_barcode(request, code): @app.route("/furizon/beyond/order///open/") async def redirect_explore(request, code, secret, order: Order, secret2=None): - response = redirect(app.url_for("welcome")) + r = redirect(app.url_for("welcome")) if order and order.code != code: order = None if not order: @@ -70,9 +71,9 @@ async def redirect_explore(request, code, secret, order: Order, secret2=None): res = res.json() if secret != res['secret']: raise exceptions.Forbidden("The secret part of the url is not correct. Check your E-Mail for the correct link, or contact support!") - response.cookies['foxo_code'] = code - response.cookies['foxo_secret'] = secret - return response + r.cookies['foxo_code'] = code + r.cookies['foxo_secret'] = secret + return r @app.route("/manage/privacy") async def privacy(request): diff --git a/ext.py b/ext.py index 23b34a7..5d1a63a 100644 --- a/ext.py +++ b/ext.py @@ -21,6 +21,8 @@ class Order: self.code = data['code'] self.pending_update = False + self.email = data['email'] + self.has_card = False self.sponsorship = None self.has_early = False @@ -83,6 +85,7 @@ class Order: self.room_members = self.ans('room_members').split(',') if self.ans('room_members') else [] self.room_owner = (self.code == self.room_id) self.room_secret = self.ans('room_secret') + self.app_token = self.ans('app_token') def __getitem__(self, var): @@ -181,7 +184,7 @@ class OrderManager: async def get_order(self, request=None, code=None, secret=None, cached=False): # Fill the cache on first load - if not self.cache: + if not self.cache and FILL_CACHE: p = 0 async with httpx.AsyncClient() as client: