routes.py 21.8 KB
Newer Older
1
2
3
4
5
6
try:
    from Cryptodome.Cipher import PKCS1_OAEP, AES
    from Cryptodome.PublicKey import RSA
except:
    from Crypto.Cipher import PKCS1_OAEP, AES
    from Crypto.PublicKey import RSA
Leon Tappe's avatar
Leon Tappe committed
7
8
from dateutil.tz import tzutc, gettz
from dateutil import parser, utils
Leon Tappe's avatar
Leon Tappe committed
9
10
from time import time
from .oauth2 import authorization, require_oauth, OAuth2Client
Leon Tappe's avatar
Leon Tappe committed
11
from .models import AccessCode, Color, ColorAllocation, OAuth2AuthorizationCode, OAuth2Token, db, User, Setting
12
13
from authlib.oauth2 import OAuth2Error
from authlib.integrations.flask_oauth2 import current_token
Leon Tappe's avatar
Leon Tappe committed
14
import base64
15
import hashlib
Leon Tappe's avatar
Leon Tappe committed
16
import uuid
Leon Tappe's avatar
Leon Tappe committed
17
18
import os
from werkzeug.security import gen_salt
Leon Tappe's avatar
Leon Tappe committed
19
from datetime import date, datetime
Leon Tappe's avatar
Leon Tappe committed
20
from flask import Blueprint, request, session, send_file, jsonify, redirect
Leon Tappe's avatar
Leon Tappe committed
21
import json
Leon Tappe's avatar
Leon Tappe committed
22
23
24
25


bp = Blueprint('home', __name__)

26
# init private key and decryptor for /check
27
28
29
30
31
if os.path.isfile("output/private_key.pem"):
    file = open("output/private_key.pem", "r")
    privateKey = RSA.import_key(extern_key=file.read())
    file.close()
    decryptor = PKCS1_OAEP.new(privateKey)
Leon Tappe's avatar
Leon Tappe committed
32

Leon Tappe's avatar
Leon Tappe committed
33
34
35
36
37
38
# init key for signing seeds
if os.path.isfile("output/seed_key.aes"):
    file = open("output/seed_key.aes", "rb")
    seedKey = file.read()
    file.close()

39
# init key for signing tickets
40
41
42
43
if os.path.isfile("output/signing_key.aes"):
    file = open("output/signing_key.aes", "rb")
    signKey = file.read()
    file.close()
44

Leon Tappe's avatar
Leon Tappe committed
45
46
47
48
49
50
51
52
53
54
55
56

def current_user():
    if 'id' in session:
        uid = session['id']
        return User.query.get(uid)
    return None


def split_by_crlf(s):
    return [v for v in s.splitlines() if v]


Leon Tappe's avatar
Leon Tappe committed
57
58
59
60
61
62
63
64
65
66
@bp.route('/health')
def health():
    return 'service is running', 200


@bp.route('/')
def home():
    return redirect('https://hilfe.uni-paderborn.de/Anleitung_zur_Nutzung_des_digitalen_3G-Nachweises', code=302)


Leon Tappe's avatar
Leon Tappe committed
67
68
69
70
71
72
@bp.route('/oauth/authorize', methods=['GET', 'POST'])
def authorize():
    user = current_user()
    if request.method == 'GET':
        try:
            grant = authorization.validate_consent_request(end_user=user)
Leon Tappe's avatar
Leon Tappe committed
73
            return 'ok', 200
Leon Tappe's avatar
Leon Tappe committed
74
75
76
77
78
79
80
81
82
83
84
85
        except OAuth2Error as error:
            return error.error
    if not user and 'username' in request.form:
        username = request.form.get('username')
        user = User.query.filter_by(username=username).first()
    if request.form['confirm']:
        grant_user = user
    else:
        grant_user = None
    return authorization.create_authorization_response(grant_user=grant_user)


Leon Tappe's avatar
Leon Tappe committed
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
@bp.route('/get_client', methods=['POST'])
def get_client():
    user = User.query.filter_by(username=request.json['username']).first()
    if user == None:
        return 'user not existent', 400
    if user.scope == request.json['scope']:
        old_client = db.session.query(OAuth2Client).filter(
            OAuth2Client.user_id == user.id).first()
        if old_client != None:
            return jsonify({'client_id': old_client.client_id, 'client_secret': old_client.client_secret})

        client_id = gen_salt(24)
        client_id_issued_at = int(time())
        client = OAuth2Client(
            client_id=client_id,
            client_id_issued_at=client_id_issued_at,
            user_id=user.id,
        )

        client_metadata = {
            "client_name": user.uuid,
            "client_uri": "*",
            "grant_types": [request.json['grant_type']],
            "redirect_uris": [],
            "response_types": ["code"],
            "scope": user.scope,
            "token_endpoint_auth_method": "client_secret_basic"
        }
        client.set_client_metadata(client_metadata)

        client_secret = gen_salt(48)
        client.client_secret = client_secret

        db.session.add(client)
        db.session.commit()

        return jsonify({'client_id': client_id, 'client_secret': client_secret})
    else:
        return 'wrong scope', 400


Leon Tappe's avatar
Leon Tappe committed
127
128
@bp.route('/oauth/token', methods=['POST'])
def issue_token():
Leon Tappe's avatar
Leon Tappe committed
129
130
131
    user = User.query.filter_by(username=request.form['username']).first()
    if user == None:
        return 'user not existent', 401
132
    if user.scope == request.form['scope']:
Leon Tappe's avatar
Leon Tappe committed
133
134
135
        response = authorization.create_token_response(request=request)
        print(response.response)
        return response
Leon Tappe's avatar
Leon Tappe committed
136
137
    else:
        return 'wrong scope', 401
Leon Tappe's avatar
Leon Tappe committed
138
139
140
141
142
143
144


@bp.route('/oauth/revoke', methods=['POST'])
def revoke_token():
    return authorization.create_endpoint_response('revocation')


145
146
@bp.route("/check", methods=['POST'])
def check():
Leon Tappe's avatar
Leon Tappe committed
147
    # decode signed ticket and split into parts for AES
Leon Tappe's avatar
Leon Tappe committed
148
149
150
151
152
    decoded = base64.b64decode(str(request.json))
    nonce = decoded[:16]
    tag = decoded[16:32]
    ciphertext = decoded[32:]

Leon Tappe's avatar
Leon Tappe committed
153
    # decrypt and verify parsed AES payload
Leon Tappe's avatar
Leon Tappe committed
154
    cipher = AES.new(signKey, AES.MODE_EAX, nonce)
Leon Tappe's avatar
Leon Tappe committed
155
156
157
    try:
        data = cipher.decrypt_and_verify(ciphertext, tag)
    except:
Leon Tappe's avatar
Leon Tappe committed
158
        return 'error: signing', 400
Leon Tappe's avatar
Leon Tappe committed
159

Leon Tappe's avatar
Leon Tappe committed
160
161
162
    # decrypt the ticket
    correct_key, decrypted = decrypt_ticket(data)
    if not correct_key:
Leon Tappe's avatar
Leon Tappe committed
163
        return 'error: encryption', 400
Leon Tappe's avatar
Leon Tappe committed
164
165
166
167

    # check if valid datetime
    valid = check_ticket(decrypted)

168
    # respond with color code according to current day
169
    if valid != None and valid:
Leon Tappe's avatar
Leon Tappe committed
170
        alloc = ColorAllocation.query.filter_by(day=date.today()).first()
Leon Tappe's avatar
fix 500    
Leon Tappe committed
171
172
173
174
175
176
177
178
179
180
181
182
        if alloc != None:
            return jsonify(
                verified=True,
                color=alloc.color.hex,
                inverse=alloc.color.inverse,
            )
        else:
            return jsonify(
                verified=True,
                color=None,
                inverse=None,
            )
183
184
185
186
    else:
        return jsonify(
            verified=False,
            color=None,
Leon Tappe's avatar
Leon Tappe committed
187
            inverse=None,
188
189
190
        )


Leon Tappe's avatar
Leon Tappe committed
191
def decrypt_ticket(ticket):
192
193
194
    try:
        # decode and parse request body into datetime object
        decrypted = decryptor.decrypt(ticket)
Leon Tappe's avatar
Leon Tappe committed
195
        return True, parser.isoparse(decrypted)
196
    except:
Leon Tappe's avatar
Leon Tappe committed
197
198
199
200
201
202
        return False, None


def check_ticket(decrypted):
    # compare current time with request's datetime in utc
    return decrypted > datetime.now(tzutc())
203
204


Leon Tappe's avatar
Leon Tappe committed
205
206
207
208
209
210
211
212
213
214
215
216
217
@bp.route("/genticket", methods=['POST'])
def generate_ticket():
    # decode signed ticket and split into parts for AES
    decoded = base64.b64decode(str(request.json))
    nonce = decoded[:16]
    tag = decoded[16:32]
    ciphertext = decoded[32:]

    # decrypt and verify parsed AES payload
    cipher = AES.new(seedKey, AES.MODE_EAX, nonce)
    try:
        data = cipher.decrypt_and_verify(ciphertext, tag)
    except:
Leon Tappe's avatar
Leon Tappe committed
218
        return 'error: signing', 400
Leon Tappe's avatar
Leon Tappe committed
219
220
221
222
223

    json_seed = json.loads(data.decode('utf-8'))
    epoch_timestamp = parser.isoparse(json_seed['timestamp']).timestamp()

    if datetime.now(tzutc()).timestamp() - epoch_timestamp > 60.0:
Leon Tappe's avatar
Leon Tappe committed
224
        return 'error: oldseed', 400
Leon Tappe's avatar
Leon Tappe committed
225

Leon Tappe's avatar
Leon Tappe committed
226
227
228
229
230
231
232
233
    # sign RSA ticket with AES encryption
    cipher = AES.new(signKey, AES.MODE_EAX)
    ciphertext, tag = cipher.encrypt_and_digest(
        base64.b64decode(json_seed['ticket']))
    combined = str(base64.b64encode(
        cipher.nonce + tag + ciphertext), encoding='utf-8')

    return jsonify(combined)
Leon Tappe's avatar
Leon Tappe committed
234
235


Leon Tappe's avatar
Leon Tappe committed
236
237
@bp.route('/makeseed', methods=['POST'])
@require_oauth('user')
Leon Tappe's avatar
Leon Tappe committed
238
def make_seed():
Leon Tappe's avatar
Leon Tappe committed
239
240
241
    # this is the same process as in the (soon to be deprecated) sign with some added security features
    # decode body to get raw RSA encrypted data
    body = base64.b64decode(str(request.json))
242

Leon Tappe's avatar
Leon Tappe committed
243
    # check validity of ticket to sign before proceeding
244
245
246
    # decrypt the ticket
    correct_key, decrypted = decrypt_ticket(body)
    if not correct_key:
Leon Tappe's avatar
Leon Tappe committed
247
        return 'error: encryption', 400
248
249
250
251

    # check if valid datetime
    valid = check_ticket(decrypted)

Leon Tappe's avatar
Leon Tappe committed
252
253
254
    if valid != None and valid:
        # save current timestamp
        timestamp = datetime.now(tzutc())
Leon Tappe's avatar
Leon Tappe committed
255
256
        seed = jsonify(timestamp=timestamp.isoformat(),
                       ticket=str(request.json)).data
Leon Tappe's avatar
Leon Tappe committed
257
258
259
260
261
262
263
        # sign seed with AES encryption
        cipher = AES.new(seedKey, AES.MODE_EAX)
        ciphertext, tag = cipher.encrypt_and_digest(seed)
        combined = str(base64.b64encode(
            cipher.nonce + tag + ciphertext), encoding='utf-8')
        return combined
    else:
Leon Tappe's avatar
Leon Tappe committed
264
        return 'error: expired', 400
Leon Tappe's avatar
Leon Tappe committed
265
266


Leon Tappe's avatar
Leon Tappe committed
267
@DeprecationWarning
Leon Tappe's avatar
Leon Tappe committed
268
@bp.route('/sign', methods=['POST'])
269
@require_oauth('user')
270
def sign_ticket():
Leon Tappe's avatar
Leon Tappe committed
271
    # decode body to get raw RSA encrypted data
Leon Tappe's avatar
Leon Tappe committed
272
    body = base64.b64decode(str(request.json))
273

Leon Tappe's avatar
Leon Tappe committed
274
    # check validity of ticket to sign before proceeding
275
276
277
    # decrypt the ticket
    correct_key, decrypted = decrypt_ticket(body)
    if not correct_key:
Leon Tappe's avatar
Leon Tappe committed
278
        return 'error: encryption', 400
279
280
281
282

    # check if valid datetime
    valid = check_ticket(decrypted)

283
    if valid != None and valid:
Leon Tappe's avatar
Leon Tappe committed
284
        # sign RSA ticket with AES encryption
Leon Tappe's avatar
Leon Tappe committed
285
        cipher = AES.new(signKey, AES.MODE_EAX)
286
        ciphertext, tag = cipher.encrypt_and_digest(body)
Leon Tappe's avatar
Leon Tappe committed
287
288
289
        combined = str(base64.b64encode(
            cipher.nonce + tag + ciphertext), encoding='utf-8')
        return jsonify(combined=combined)
290
    else:
Leon Tappe's avatar
Leon Tappe committed
291
        return 'error: expired', 400
292
293


Leon Tappe's avatar
Leon Tappe committed
294
@bp.route('/pubkey', methods=['GET'])
Leon Tappe's avatar
Leon Tappe committed
295
@require_oauth('user')
Leon Tappe's avatar
Leon Tappe committed
296
297
298
299
300
301
302
def public_key():
    try:
        return send_file('../output/public_key.pem', attachment_filename='public_key.pem')
    except Exception as e:
        return str(e)


Leon Tappe's avatar
Leon Tappe committed
303
@bp.route('/api/me')
Leon Tappe's avatar
Leon Tappe committed
304
@require_oauth(['admin', 'user'])
Leon Tappe's avatar
Leon Tappe committed
305
306
307
def api_me():
    user = current_token.user
    return jsonify(id=user.id, username=user.username)
Leon Tappe's avatar
Leon Tappe committed
308
309
310


@bp.route('/users', methods=['GET', 'POST'])
Leon Tappe's avatar
Leon Tappe committed
311
@require_oauth('admin')
Leon Tappe's avatar
Leon Tappe committed
312
313
314
315
316
317
def get_users():
    if request.method == 'GET':
        users = User.query.all()
        users_map = []
        for user in users:
            users_map.append({'id': user.id, 'username': user.username,
318
                              'name': user.name, 'surname': user.surname, 'uuid': user.uuid, 'scope': user.scope})
Leon Tappe's avatar
Leon Tappe committed
319
320
321
322
323
324
        return jsonify(users_map)
    elif request.method == 'POST':
        # create user object and insert to db
        password = request.json['password']
        if len(password) < 5:
            return 'minimum password length is 5 characters', 400
325
        salt = gen_salt(32).encode('utf-8')
Leon Tappe's avatar
Leon Tappe committed
326
        key = hashlib.pbkdf2_hmac(
327
328
329
            'sha256', request.json['password'].encode('utf-8'), salt, 100000)
        user = User(username=request.json['username'], password=key,
                    salt=salt, uuid=uuid.uuid4().urn[9:], scope=request.json['scope'], name=request.json['name'] if 'name' in request.json else None, surname=request.json['surname'] if 'surname' in request.json else None)
Leon Tappe's avatar
Leon Tappe committed
330
331
332
333
334
        db.session.add(user)
        db.session.commit()

        # read new user from db to validate and return with user id
        new_user = User.query.filter_by(username=user.username).first()
Leon Tappe's avatar
Leon Tappe committed
335
        return make_user_response(new_user)
Leon Tappe's avatar
Leon Tappe committed
336

Leon Tappe's avatar
Leon Tappe committed
337
338
339

def make_user_response(user):
    return jsonify(id=user.id, username=user.username,
340
                   name=user.name, surname=user.surname, uuid=user.uuid, scope=user.scope)
Leon Tappe's avatar
Leon Tappe committed
341
342
343


@bp.route('/users/<int:user_id>', methods=['GET', 'PUT', 'DELETE'])
Leon Tappe's avatar
Leon Tappe committed
344
@require_oauth('admin')
Leon Tappe's avatar
Leon Tappe committed
345
346
347
def get_user(user_id):
    if request.method == 'GET':
        user = User.query.filter_by(id=user_id).first()
Leon Tappe's avatar
Leon Tappe committed
348
349
        if (user == None):
            return 'not found', 404
Leon Tappe's avatar
Leon Tappe committed
350
351
        return make_user_response(user)
    elif request.method == 'PUT':
Leon Tappe's avatar
Leon Tappe committed
352
353
        if db.session.query(User).filter(User.id == user_id).update(request.json) < 1:
            return 'not found', 404
Leon Tappe's avatar
Leon Tappe committed
354
355
356
357
        db.session.commit()
        new_user = User.query.filter_by(id=user_id).first()
        return make_user_response(new_user)
    elif request.method == 'DELETE':
Leon Tappe's avatar
Leon Tappe committed
358
        # delete all corresponding entries in other tables
Leon Tappe's avatar
Leon Tappe committed
359
360
361
362
363
364
365
366
        db.session.query(AccessCode).filter(
            AccessCode.user_id == user_id).delete()
        db.session.query(OAuth2Client).filter(
            OAuth2Client.user_id == user_id).delete()
        db.session.query(OAuth2Token).filter(
            OAuth2Token.user_id == user_id).delete()
        db.session.query(OAuth2AuthorizationCode).filter(
            OAuth2AuthorizationCode.user_id == user_id).delete()
Leon Tappe's avatar
Leon Tappe committed
367
        # delete user
Leon Tappe's avatar
Leon Tappe committed
368
369
        if db.session.query(User).filter(User.id == user_id).delete() < 1:
            return 'not found', 404
Leon Tappe's avatar
Leon Tappe committed
370
371
        db.session.commit()
        return 'success', 200
Leon Tappe's avatar
Leon Tappe committed
372
373
374


def make_code_response(code):
375
    return jsonify(id=code.id, code=code.code, scope=code.scope, user_id=code.user_id)
Leon Tappe's avatar
Leon Tappe committed
376
377
378


@bp.route('/access_codes', methods=['GET', 'POST'])
Leon Tappe's avatar
Leon Tappe committed
379
@require_oauth('admin')
Leon Tappe's avatar
Leon Tappe committed
380
381
382
383
384
385
def get_access_codes():
    if request.method == 'GET':
        codes = AccessCode.query.all()
        codes_map = []
        for code in codes:
            codes_map.append({'id': code.id, 'code': code.code,
386
                              'scope': code.scope, 'user_id': code.user_id})
Leon Tappe's avatar
Leon Tappe committed
387
388
389
390
391
392
393
394
        return jsonify(codes_map)
    elif request.method == 'POST':
        if len(request.json['code']) >= 16:
            if AccessCode.query.filter_by(
                    code=request.json['code']).first() != None:
                return 'code exists already', 400

            db.session.add(AccessCode(
395
                code=request.json['code'], scope=request.json['scope']))
Leon Tappe's avatar
Leon Tappe committed
396
397
398
            db.session.commit()
            new_code = AccessCode.query.filter_by(
                code=request.json['code']).first()
399
            return make_code_response(new_code)
Leon Tappe's avatar
Leon Tappe committed
400
401
402
403
404
        else:
            return 'code not long enough', 400


@bp.route('/access_codes/<int:code_id>', methods=['GET', 'DELETE'])
Leon Tappe's avatar
Leon Tappe committed
405
406
@require_oauth('admin')
def get_code(code_id):
Leon Tappe's avatar
Leon Tappe committed
407
408
    if request.method == 'GET':
        code = AccessCode.query.filter_by(id=code_id).first()
Leon Tappe's avatar
Leon Tappe committed
409
410
        if (code == None):
            return 'not found', 404
Leon Tappe's avatar
Leon Tappe committed
411
412
        return make_code_response(code)
    elif request.method == 'DELETE':
Leon Tappe's avatar
Leon Tappe committed
413
414
        if db.session.query(AccessCode).filter(AccessCode.id == code_id).delete() < 1:
            return 'not found', 404
Leon Tappe's avatar
Leon Tappe committed
415
416
        db.session.commit()
        return 'success', 200
Leon Tappe's avatar
Leon Tappe committed
417
418
419
420
421
422
423
424
425
426
427
428


@bp.route('/register', methods=['POST'])
def register():
    code = AccessCode.query.filter_by(code=request.json['code']).first()
    if (code == None):
        return 'code does not exist', 404
    if (code.user_id != None):
        return 'code already consumed', 400
    if 'username' in request.json and 'password' in request.json:
        if User.query.filter_by(username=request.json['username']).first() != None:
            return 'username taken', 400
Leon Tappe's avatar
Leon Tappe committed
429
        salt = gen_salt(32).encode('utf-8')
Leon Tappe's avatar
Leon Tappe committed
430
431
432
        key = hashlib.pbkdf2_hmac(
            'sha256', request.json['password'].encode('utf-8'), salt, 100000)
        user = User(username=request.json['username'], password=key,
433
                    salt=salt, uuid=uuid.uuid4().urn[9:], scope=code.scope, name=request.json['name'] if 'name' in request.json else None, surname=request.json['surname'] if 'surname' in request.json else None)
Leon Tappe's avatar
Leon Tappe committed
434
435
436
437
438
439
440
441
442
        db.session.add(user)
        db.session.commit()
        new_user = User.query.filter_by(username=user.username).first()
        db.session.query(AccessCode).filter(
            AccessCode.id == code.id).update({AccessCode.user_id: new_user.id})
        db.session.commit()
        return make_user_response(new_user)
    else:
        return 'incomplete json', 400
Leon Tappe's avatar
Leon Tappe committed
443
444
445


def make_color_response(color):
Leon Tappe's avatar
Leon Tappe committed
446
    return jsonify(id=color.id, hex=color.hex, inverse=color.inverse, name=color.name)
Leon Tappe's avatar
Leon Tappe committed
447
448
449


@bp.route('/colors', methods=['GET', 'POST'])
450
@require_oauth('admin')
Leon Tappe's avatar
Leon Tappe committed
451
452
453
454
455
def get_colors():
    if request.method == 'GET':
        colors = Color.query.all()
        colors_map = []
        for color in colors:
456
            colors_map.append(
Leon Tappe's avatar
Leon Tappe committed
457
                {'id': color.id, 'hex': color.hex, 'inverse': color.inverse, 'name': color.name})
Leon Tappe's avatar
Leon Tappe committed
458
459
        return jsonify(colors_map)
    elif request.method == 'POST':
Leon Tappe's avatar
Leon Tappe committed
460
461
462
463
464
        if len(request.json['hex']) >= 6 and 'inverse' in request.json:
            test = db.session.query(Color).filter(
                Color.hex == request.json['hex']).first()
            if test != None and test.inverse == request.json['inverse']:
                print(str(test.hex), '\t', str(test.inverse))
Leon Tappe's avatar
Leon Tappe committed
465
                return 'color exists already', 400
466
            db.session.add(Color(
Leon Tappe's avatar
Leon Tappe committed
467
                hex=request.json['hex'], name=request.json['name'] if 'name' in request.json else None, inverse=request.json['inverse'] if 'inverse' in request.json else False))
Leon Tappe's avatar
Leon Tappe committed
468
            db.session.commit()
Leon Tappe's avatar
Leon Tappe committed
469
470
            new_color = db.session.query(Color).filter(
                Color.hex == request.json['hex'], Color.inverse == request.json['inverse']).first()
Leon Tappe's avatar
Leon Tappe committed
471
472
473
474
475
476
            return make_color_response(new_color)
        else:
            return 'no valid color', 400


@bp.route('/colors/<int:color_id>', methods=['GET', 'DELETE', 'PUT'])
477
@require_oauth('admin')
Leon Tappe's avatar
Leon Tappe committed
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
def get_color(color_id):
    if request.method == 'GET':
        color = AccessCode.query.filter_by(id=color_id).first()
        if (color == None):
            return 'not found', 404
        return make_color_response(color)
    elif request.method == 'PUT':
        if db.session.query(Color).filter(Color.id == color_id).update(request.json) < 1:
            return 'not found', 404
        db.session.commit()
        new_color = Color.query.filter_by(id=color_id).first()
        return make_color_response(new_color)
    elif request.method == 'DELETE':
        if db.session.query(Color).filter(Color.id == color_id).delete() < 1:
            return 'not found', 404
493
494
        db.session.query(ColorAllocation).filter(
            ColorAllocation.color_id == color_id).delete()
Leon Tappe's avatar
Leon Tappe committed
495
496
        db.session.commit()
        return 'success', 200
497
498
499


def make_color_allocation_response(alloc):
500
    return jsonify(id=alloc.id, day=alloc.day.isoformat(), color_id=alloc.color_id, color_hex=alloc.color.hex, color_name=alloc.color.name, color_inverse=alloc.color.inverse)
501
502
503
504
505
506
507
508
509
510


@bp.route('/color_allocations', methods=['GET', 'POST'])
@require_oauth('admin')
def get_color_allocations():
    if request.method == 'GET':
        allocs = ColorAllocation.query.all()
        allocs_map = []
        for alloc in allocs:
            allocs_map.append(
511
                {'id': alloc.id, 'day': alloc.day.isoformat(), 'color_id': alloc.color_id, 'color_hex': alloc.color.hex, 'color_name': alloc.color.name, 'color_inverse': alloc.color.inverse})
512
513
514
515
516
517
518
        return jsonify(allocs_map)
    elif request.method == 'POST':
        if Color.query.filter_by(
                id=request.json['color_id']).first() != None:
            if ColorAllocation.query.filter_by(
                    day=request.json['day']).first() != None:
                return 'color allocation for this day exists already', 400
519
520
521
            alloc = ColorAllocation(
                day=parser.parse(request.json['day']), color_id=request.json['color_id'])
            db.session.add(alloc)
522
            db.session.commit()
523
            new_alloc = ColorAllocation.query.filter_by(day=alloc.day).first()
524
525
526
527
528
529
530
531
532
533
534
535
536
537
            return make_color_allocation_response(new_alloc)
        else:
            return 'color does not exist', 400


@bp.route('/color_allocations/<int:alloc_id>', methods=['GET', 'DELETE', 'PUT'])
@require_oauth('admin')
def get_color_allocation(alloc_id):
    if request.method == 'GET':
        alloc = ColorAllocation.query.filter_by(id=alloc_id).first()
        if (alloc == None):
            return 'not found', 404
        return make_color_allocation_response(alloc)
    elif request.method == 'PUT':
538
        request.json['day'] = parser.parse(request.json['day'])
539
540
541
542
543
544
545
546
547
548
549
550
        if db.session.query(ColorAllocation).filter(ColorAllocation.id == alloc_id).update(request.json) < 1:
            return 'not found', 404
        db.session.commit()
        new_alloc = ColorAllocation.query.filter_by(id=alloc_id).first()
        return make_color_allocation_response(new_alloc)
    elif request.method == 'DELETE':
        if db.session.query(ColorAllocation).filter(ColorAllocation.id == alloc_id).delete() < 1:
            return 'not found', 404
        db.session.commit()
        return 'success', 200


Leon Tappe's avatar
Leon Tappe committed
551
552
553
554
555
556
557
558
559
560
@bp.route('/current_color_admin', methods=['GET'])
@require_oauth('admin')
def current_color_admin():
    alloc = ColorAllocation.query.filter_by(
        day=utils.today(tzinfo=gettz()).date()).first()
    if (alloc == None):
        return 'no color found for today', 404
    return make_color_allocation_response(alloc)


561
@bp.route('/current_color', methods=['GET'])
Leon Tappe's avatar
Leon Tappe committed
562
@require_oauth('user')
563
def current_color():
Leon Tappe's avatar
Leon Tappe committed
564
565
    alloc = ColorAllocation.query.filter_by(
        day=utils.today(tzinfo=gettz()).date()).first()
566
567
    if (alloc == None):
        return 'no color found for today', 404
568
    return jsonify(color=alloc.color.hex, inverse=alloc.color.inverse)
Leon Tappe's avatar
Leon Tappe committed
569
570
571
572


def get_settings():
    settings = Setting.query.all()
Leon Tappe's avatar
Leon Tappe committed
573
    settings_map = {}
Leon Tappe's avatar
Leon Tappe committed
574
    for setting in settings:
Leon Tappe's avatar
Leon Tappe committed
575
        settings_map[setting.key] = setting.value
Leon Tappe's avatar
Leon Tappe committed
576
577
578
579
580
581
582
583
584
585
    return jsonify(settings_map)


@bp.route('/admin/settings', methods=['GET', 'POST'])
@require_oauth('admin')
def admin_settings():
    if request.method == 'GET':
        return get_settings()
    if request.method == 'POST':
        setting = Setting(
Leon Tappe's avatar
Leon Tappe committed
586
            key=request.json['key'], value=request.json['value'])
Leon Tappe's avatar
Leon Tappe committed
587
588
589
590
        try:
            db.session.add(setting)
            db.session.commit()
            new_setting = Setting.query.filter_by(key=setting.key).first()
591
            return jsonify({new_setting.key: new_setting.value})
Leon Tappe's avatar
Leon Tappe committed
592
593
594
595
596
597
598
599
600
        except:
            return 'failed to add setting, check for duplicate key', 400


@bp.route('/admin/settings/<string:key>', methods=['GET', 'PUT', 'DELETE'])
@require_oauth('admin')
def single_setting(key):
    if request.method == 'GET':
        setting = Setting.query.filter_by(key=key).first()
601
        return jsonify({setting.key: setting.value})
Leon Tappe's avatar
Leon Tappe committed
602
603
604
605
606
    if request.method == 'PUT':
        if db.session.query(Setting).filter(Setting.key == key).update({Setting.value: request.json['value']}) < 1:
            return 'not found', 404
        db.session.commit()
        new_setting = Setting.query.filter_by(key=key).first()
607
        return jsonify({new_setting.key: new_setting.value})
Leon Tappe's avatar
Leon Tappe committed
608
609
610
611
612
613
614
615
616
617
618
    if request.method == 'DELETE':
        if db.session.query(Setting).filter(Setting.key == key).delete() < 1:
            return 'not found', 404
        db.session.commit()
        return 'success', 200


@bp.route('/settings', methods=['GET'])
@require_oauth('user')
def user_settings():
    return get_settings()
619
620
621
622
623


@bp.route('/motd', methods=['GET'])
def motd():
    setting = Setting.query.filter_by(key='motd_public').first()
Leon Tappe's avatar
Leon Tappe committed
624
    return setting.value