|
1 |
| -import base64 |
2 | 1 | import logging
|
3 |
| -import os |
4 | 2 |
|
5 | 3 | from aiohttp import web
|
6 |
| -from aiohttp_session import SimpleCookieStorage, get_session, setup |
7 |
| -import asyncpg |
8 |
| -import bcrypt |
9 |
| -from dotenv import load_dotenv |
10 |
| -import pandas as pd |
11 |
| -import yaml |
12 | 4 |
|
13 |
| -from .config import Config |
| 5 | +from ioibot.storage import Storage |
14 | 6 |
|
15 | 7 | logger = logging.getLogger(__name__)
|
16 | 8 | logger.setLevel(logging.DEBUG)
|
17 | 9 |
|
18 |
| -load_dotenv() |
19 |
| - |
20 |
| -uname = os.getenv('VOTING_USERNAME').strip() |
21 |
| -passw_hash = bytes(os.getenv('VOTING_PASSWORD').strip(), "utf-8") |
22 |
| - |
23 |
| -# Middleware function to perform Basic Authentication |
24 |
| -async def basic_auth_middleware(app, handler): |
25 |
| - async def middleware(request): |
26 |
| - # Extract the Authorization header from the request |
27 |
| - auth_header = request.headers.get("Authorization") |
28 |
| - session = await get_session(request) |
29 |
| - |
30 |
| - if auth_header: |
31 |
| - try: |
32 |
| - auth_type, encoded_credentials = auth_header.split(" ") |
33 |
| - if auth_type.lower() == "basic": |
34 |
| - decoded_credentials = base64.b64decode(encoded_credentials).decode("utf-8") |
35 |
| - username, password = decoded_credentials.split(":") |
36 |
| - username = username.strip() |
37 |
| - password = password.strip() |
38 |
| - password = bytes(password, "utf-8") |
39 |
| - if username == uname and bcrypt.hashpw(password, passw_hash) == passw_hash: |
40 |
| - logger.info(f"Authentication successful for {request.remote}") |
41 |
| - |
42 |
| - session["authenticated"] = True |
43 |
| - return await handler(request) |
44 |
| - |
45 |
| - except Exception as e: |
46 |
| - print(f"Authentication error: {e}") |
47 |
| - |
48 |
| - # Authentication failed or no credentials provided, check session for authentication |
49 |
| - if session.get("authenticated"): |
50 |
| - logger.info(f"Authentication successful for {request.remote}") |
51 |
| - |
52 |
| - return await handler(request) |
| 10 | +routes = web.RouteTableDef() |
| 11 | +store_key = web.AppKey("store", Storage) |
| 12 | +static_root = 'webpage' # TODO make this configurable |
| 13 | + |
| 14 | +# return currently active poll result |
| 15 | +@routes.get("/api/polls") |
| 16 | +async def polls_active(req: web.Request): |
| 17 | + store = req.app[store_key] |
| 18 | + conn = store.conn |
| 19 | + teams = store.teams |
| 20 | + |
| 21 | + poll_details = await conn.fetchrow( |
| 22 | + "SELECT poll_id, question, status, anonymous, multiple_choice FROM polls WHERE display") |
| 23 | + if poll_details is None: |
| 24 | + return web.json_response({}) |
| 25 | + |
| 26 | + votes = [] |
| 27 | + [poll_id, question, status, anonymous, multiple_choice] = poll_details |
| 28 | + |
| 29 | + poll_choices = await conn.fetch( |
| 30 | + "SELECT poll_choice_id, choice, marker FROM poll_choices WHERE poll_id = $1", poll_id) |
| 31 | + if not poll_choices: |
| 32 | + return web.HTTPInternalServerError() |
| 33 | + |
| 34 | + choices = [{'choice_id': choice_id, 'choice': choice, 'marker': marker} for (choice_id, choice, marker) in poll_choices] |
| 35 | + |
| 36 | + if anonymous: |
| 37 | + if status == 1: |
| 38 | + results = dict() |
| 39 | + for (poll_choice_id, _, _) in poll_choices: |
| 40 | + results[poll_choice_id] = 0 |
| 41 | + |
| 42 | + anonym_votes = await conn.fetch("SELECT poll_choice_id FROM poll_anonym_active_votes") |
| 43 | + for (vote,) in anonym_votes: |
| 44 | + results[vote] += 1 |
| 45 | + vote_items = results.items() |
| 46 | + elif status == 2: |
| 47 | + vote_items = await conn.fetch("SELECT poll_choice_id, count FROM poll_anonym_votes WHERE poll_id = $1", poll_id) |
53 | 48 | else:
|
54 |
| - logger.info(f"Authentication failed for {request.remote}") |
55 |
| - |
56 |
| - # Authentication failed, return a 401 Unauthorized response |
57 |
| - response = web.Response(status=401) |
58 |
| - response.headers["WWW-Authenticate"] = 'Basic realm="Secure Area"' |
59 |
| - return response |
60 |
| - |
61 |
| - return middleware |
62 |
| - |
63 |
| - |
64 |
| -async def create_app(config: Config, conn: asyncpg.Pool): |
| 49 | + vote_items = [] |
| 50 | + votes = [{'count': count, 'choice_id': choice} for (choice, count) in vote_items] |
| 51 | + else: # not anonymous |
| 52 | + vote_items = await conn.fetch("SELECT poll_choice_id, team_code, voted_by, voted_at FROM poll_votes WHERE poll_id = $1", poll_id) |
| 53 | + votes = [{'team_code': f"({team_code}) {teams.loc[teams['Code'] == team_code, ['Name']].values[0][0]}", 'voted_by': voted_by, 'voted_at': voted_at, 'choice_id': choice} for (choice, team_code, voted_by, voted_at) in vote_items] |
| 54 | + missing_teams = teams.loc[~teams['Code'].isin([team_code for (_, team_code, _, _) in vote_items]), ['Code', 'Name']] |
| 55 | + for _, row in missing_teams.iterrows(): |
| 56 | + votes.append({'team_code': f"({row['Code']}) {row['Name']}", 'voted_by': None, 'voted_at': None, 'choice_id': None}) |
| 57 | + |
| 58 | + response = { |
| 59 | + 'question': question, |
| 60 | + 'choices': choices, |
| 61 | + 'anonymous': anonymous, |
| 62 | + 'multiple_choice': multiple_choice, |
| 63 | + 'status': status, |
| 64 | + 'votes': votes, |
| 65 | + } |
| 66 | + |
| 67 | + return web.json_response(response) |
| 68 | + |
| 69 | +# for development, should be handled by nginx |
| 70 | +@routes.get('/') |
| 71 | +async def get_index(req: web.Request): |
| 72 | + return web.FileResponse(f'{static_root}/index.html') |
| 73 | +routes.static('/', static_root) |
| 74 | + |
| 75 | +async def run_webapp(store: Storage): |
65 | 76 | app = web.Application()
|
66 |
| - setup(app, SimpleCookieStorage(max_age=3600)) |
67 |
| - app.middlewares.append(basic_auth_middleware) |
68 |
| - routes = web.RouteTableDef() |
69 |
| - teams_all = pd.read_csv(config.team_url) |
70 |
| - teams = teams_all[teams_all['Voting'] == 1] |
71 |
| - |
72 |
| - |
73 |
| - # website |
74 |
| - @routes.get("/polls") |
75 |
| - async def polls(request): |
76 |
| - return web.FileResponse("./webpage/index.html") |
77 |
| - |
78 |
| - # return currently active poll result |
79 |
| - @routes.get("/polls/display") |
80 |
| - async def polls_active(request): |
81 |
| - poll_details = await conn.fetchrow( |
82 |
| - "SELECT poll_id, question, status, anonymous, multiple_choice FROM polls WHERE display = 1") |
83 |
| - if poll_details is None: |
84 |
| - return web.json_response({}) |
85 |
| - |
86 |
| - votes = [] |
87 |
| - [poll_id, question, status, anonymous, multiple_choice] = poll_details |
88 |
| - |
89 |
| - poll_choices = await conn.fetch( |
90 |
| - "SELECT poll_choice_id, choice, marker FROM poll_choices WHERE poll_id = $1", poll_id) |
91 |
| - if not poll_choices: |
92 |
| - return web.HTTPInternalServerError() |
93 |
| - |
94 |
| - choices = [{'choice_id': choice_id, 'choice': choice, 'marker': marker} for (choice_id, choice, marker) in poll_choices] |
95 |
| - |
96 |
| - if anonymous: |
97 |
| - if status == 1: |
98 |
| - results = dict() |
99 |
| - for (poll_choice_id, _, _) in poll_choices: |
100 |
| - results[poll_choice_id] = 0 |
101 |
| - |
102 |
| - anonym_votes = await conn.fetch("SELECT poll_choice_id FROM poll_anonym_active_votes") |
103 |
| - for (vote,) in anonym_votes: |
104 |
| - results[vote] += 1 |
105 |
| - vote_items = results.items() |
106 |
| - elif status == 2: |
107 |
| - vote_items = await conn.fetch("SELECT poll_choice_id, count FROM poll_anonym_votes WHERE poll_id = $1", poll_id) |
108 |
| - else: |
109 |
| - vote_items = [] |
110 |
| - votes = [{'count': count, 'choice_id': choice} for (choice, count) in vote_items] |
111 |
| - else: # not anonymous |
112 |
| - vote_items = await conn.fetch("SELECT poll_choice_id, team_code, voted_by, voted_at FROM poll_votes WHERE poll_id = $1", poll_id) |
113 |
| - votes = [{'team_code': f"({team_code}) {teams.loc[teams['Code'] == team_code, ['Name']].values[0][0]}", 'voted_by': voted_by, 'voted_at': voted_at, 'choice_id': choice} for (choice, team_code, voted_by, voted_at) in vote_items] |
114 |
| - missing_teams = teams.loc[~teams['Code'].isin([team_code for (_, team_code, _, _) in vote_items]), ['Code', 'Name']] |
115 |
| - for _, row in missing_teams.iterrows(): |
116 |
| - votes.append({'team_code': f"({row['Code']}) {row['Name']}", 'voted_by': None, 'voted_at': None, 'choice_id': None}) |
117 |
| - |
118 |
| - response = { |
119 |
| - 'question': question, |
120 |
| - 'choices': choices, |
121 |
| - 'anonymous': anonymous, |
122 |
| - 'multiple_choice': multiple_choice, |
123 |
| - 'status': status, |
124 |
| - 'votes': votes, |
125 |
| - } |
126 |
| - |
127 |
| - return web.json_response(response) |
128 |
| - |
129 |
| - |
130 |
| - app.router.add_routes(routes) |
131 |
| - app.router.add_static("/", "./") |
132 |
| - return app |
133 |
| - |
134 |
| -async def run_webapp(config: Config, conn: asyncpg.Pool): |
135 |
| - app = await create_app(config, conn) |
| 77 | + app[store_key] = store |
| 78 | + app.add_routes(routes) |
| 79 | + |
136 | 80 | runner = web.AppRunner(app)
|
137 | 81 | await runner.setup()
|
138 |
| - site = web.TCPSite(runner, "0.0.0.0", 9000) |
| 82 | + site = web.TCPSite(runner, '127.0.0.1', 9000) # TODO make this configurable |
139 | 83 | await site.start()
|
0 commit comments