mirror of
https://github.com/dancojocaru2000/foxbank.git
synced 2025-02-22 23:39:36 +02:00
commit
4de89f001a
22 changed files with 465 additions and 4 deletions
27
.github/workflows/build-image.yml
vendored
Normal file
27
.github/workflows/build-image.yml
vendored
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
name: Build Docker image
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- Backend
|
||||||
|
jobs:
|
||||||
|
push_to_ghcr:
|
||||||
|
name: Push to ghcr.io
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Publish
|
||||||
|
env:
|
||||||
|
tags: ${{ format('{0},{1}', format('ghcr.io/{0}:latest', github.repository), format('ghcr.io/{0}:{1}', github.repository, github.ref_name)) }}
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: ./server
|
||||||
|
tags: ${{ env.tags }}
|
||||||
|
push: true
|
6
.vscode/settings.json
vendored
Normal file
6
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"files.exclude": {
|
||||||
|
"client": true,
|
||||||
|
"server": true
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,9 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "server"
|
"path": "server"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "."
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"settings": {}
|
"settings": {}
|
||||||
|
|
2
server/.dockerignore
Normal file
2
server/.dockerignore
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
__pycache__/
|
||||||
|
data/
|
2
server/.gitignore
vendored
Normal file
2
server/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
__pycache__/
|
||||||
|
data/
|
4
server/.vscode/extensions.json
vendored
4
server/.vscode/extensions.json
vendored
|
@ -1,5 +1,3 @@
|
||||||
{
|
{
|
||||||
"recommendations": [
|
"recommendations": []
|
||||||
"ms-python.python"
|
|
||||||
]
|
|
||||||
}
|
}
|
23
server/.vscode/launch.json
vendored
Normal file
23
server/.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Python: Flask",
|
||||||
|
"type": "python",
|
||||||
|
"request": "launch",
|
||||||
|
"module": "flask",
|
||||||
|
"env": {
|
||||||
|
"FLASK_APP": "server.py",
|
||||||
|
"FLASK_ENV": "development"
|
||||||
|
},
|
||||||
|
"args": [
|
||||||
|
"run",
|
||||||
|
"--no-debugger"
|
||||||
|
],
|
||||||
|
"jinja": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
3
server/.vscode/settings.json
vendored
Normal file
3
server/.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"python.pythonPath": "/home/kbruen/.local/share/virtualenvs/server-4otskhbj/bin/python"
|
||||||
|
}
|
13
server/Dockerfile
Normal file
13
server/Dockerfile
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
FROM python:3.10-slim
|
||||||
|
|
||||||
|
RUN pip3 install pipenv
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY Pipfile Pipfile.lock ./
|
||||||
|
RUN pipenv install
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
EXPOSE 5000
|
||||||
|
CMD [ "pipenv", "run", "gunicorn", "-b", "0.0.0.0:5000", "--access-logfile", "-", "server:app" ]
|
|
@ -5,6 +5,9 @@ name = "pypi"
|
||||||
|
|
||||||
[packages]
|
[packages]
|
||||||
flask = "*"
|
flask = "*"
|
||||||
|
gunicorn = "*"
|
||||||
|
pyotp = "*"
|
||||||
|
flask-cors = "*"
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
|
|
||||||
|
|
42
server/Pipfile.lock
generated
42
server/Pipfile.lock
generated
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "295fa60b4ad3b19ec29744ec2dfafba79ad5ee9a0b9ff095ac626e3d3981f117"
|
"sha256": "2ba252d63658abd009170d14705593521c57c99f82b643fcf232eeb51be35d10"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {
|
"requires": {
|
||||||
|
@ -32,6 +32,22 @@
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==2.0.2"
|
"version": "==2.0.2"
|
||||||
},
|
},
|
||||||
|
"flask-cors": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:74efc975af1194fc7891ff5cd85b0f7478be4f7f59fe158102e91abb72bb4438",
|
||||||
|
"sha256:b60839393f3b84a0f3746f6cdca56c1ad7426aa738b70d6c61375857823181de"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==3.0.10"
|
||||||
|
},
|
||||||
|
"gunicorn": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e",
|
||||||
|
"sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==20.1.0"
|
||||||
|
},
|
||||||
"itsdangerous": {
|
"itsdangerous": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:5174094b9637652bdb841a3029700391451bd092ba3db90600dea710ba28e97c",
|
"sha256:5174094b9637652bdb841a3029700391451bd092ba3db90600dea710ba28e97c",
|
||||||
|
@ -123,6 +139,30 @@
|
||||||
"markers": "python_version >= '3.6'",
|
"markers": "python_version >= '3.6'",
|
||||||
"version": "==2.0.1"
|
"version": "==2.0.1"
|
||||||
},
|
},
|
||||||
|
"pyotp": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:9d144de0f8a601d6869abe1409f4a3f75f097c37b50a36a3bf165810a6e23f28",
|
||||||
|
"sha256:d28ddfd40e0c1b6a6b9da961c7d47a10261fb58f378cb00f05ce88b26df9c432"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==2.6.0"
|
||||||
|
},
|
||||||
|
"setuptools": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:6d10741ff20b89cd8c6a536ee9dc90d3002dec0226c78fb98605bfb9ef8a7adf",
|
||||||
|
"sha256:d144f85102f999444d06f9c0e8c737fd0194f10f2f7e5fdb77573f6e2fa4fad0"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==59.5.0"
|
||||||
|
},
|
||||||
|
"six": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
|
||||||
|
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
|
"version": "==1.16.0"
|
||||||
|
},
|
||||||
"werkzeug": {
|
"werkzeug": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:63d3dc1cf60e7b7e35e97fa9861f7397283b75d765afcaefd993d6046899de8f",
|
"sha256:63d3dc1cf60e7b7e35e97fa9861f7397283b75d765afcaefd993d6046899de8f",
|
||||||
|
|
38
server/db.py
Normal file
38
server/db.py
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
from flask import current_app, g
|
||||||
|
|
||||||
|
DB_FILE = './data/db.sqlite'
|
||||||
|
|
||||||
|
get_return = sqlite3.Connection
|
||||||
|
|
||||||
|
def get() -> get_return:
|
||||||
|
if 'db' not in g:
|
||||||
|
g.db = sqlite3.connect(
|
||||||
|
DB_FILE,
|
||||||
|
detect_types=sqlite3.PARSE_DECLTYPES,
|
||||||
|
)
|
||||||
|
g.db.row_factory = sqlite3.Row
|
||||||
|
|
||||||
|
return g.db
|
||||||
|
|
||||||
|
def close(e=None):
|
||||||
|
db = g.pop('db', None)
|
||||||
|
|
||||||
|
if db:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
def init():
|
||||||
|
db = get()
|
||||||
|
|
||||||
|
with current_app.open_resource('init.sql') as f:
|
||||||
|
db.executescript(f.read().decode('utf8'))
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
def init_app(app):
|
||||||
|
app.teardown_appcontext(close)
|
||||||
|
|
||||||
|
import os.path
|
||||||
|
if not os.path.exists(DB_FILE):
|
||||||
|
with app.app_context():
|
||||||
|
init()
|
24
server/db_utils.py
Normal file
24
server/db_utils.py
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
import db
|
||||||
|
import models
|
||||||
|
|
||||||
|
def get_db(fn):
|
||||||
|
@wraps(fn)
|
||||||
|
def wrapper(*args, **kargs):
|
||||||
|
return fn(db.get(), *args, **kargs)
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
@get_db
|
||||||
|
def get_user(db: db.get_return, username: str|None = None, user_id: int|None = None) -> models.User | None:
|
||||||
|
cur = db.cursor()
|
||||||
|
if username is not None:
|
||||||
|
cur.execute('select * from users where username=?', (username,))
|
||||||
|
elif user_id is not None:
|
||||||
|
cur.execute('select * from users where id=?', (user_id,))
|
||||||
|
else:
|
||||||
|
raise Exception('Neither username or user_id passed')
|
||||||
|
result = cur.fetchone()
|
||||||
|
if result is None:
|
||||||
|
return None
|
||||||
|
return models.User.from_query(result)
|
12
server/decorators.py
Normal file
12
server/decorators.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
from http import HTTPStatus
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
def no_content(fn):
|
||||||
|
@wraps(fn)
|
||||||
|
def wrapper(*args, **kargs):
|
||||||
|
result = fn(*args, **kargs)
|
||||||
|
if result is None:
|
||||||
|
return None, HTTPStatus.NO_CONTENT
|
||||||
|
else:
|
||||||
|
return result
|
||||||
|
return wrapper
|
9
server/docker-compose.yml
Normal file
9
server/docker-compose.yml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
version: '3.9'
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
build: .
|
||||||
|
image: foxbank-server
|
||||||
|
ports:
|
||||||
|
- ${PORT:-5000}:5000
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data
|
61
server/init.sql
Normal file
61
server/init.sql
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
drop table if exists users;
|
||||||
|
drop table if exists accounts;
|
||||||
|
drop table if exists users_accounts;
|
||||||
|
drop table if exists transactions;
|
||||||
|
drop table if exists accounts_transactions;
|
||||||
|
drop table if exists notifications;
|
||||||
|
drop table if exists users_notifications;
|
||||||
|
|
||||||
|
create table users (
|
||||||
|
id integer primary key autoincrement,
|
||||||
|
username text unique not null,
|
||||||
|
email text unique not null,
|
||||||
|
otp text not null,
|
||||||
|
fullname text not null
|
||||||
|
);
|
||||||
|
|
||||||
|
create table accounts (
|
||||||
|
id integer primary key autoincrement,
|
||||||
|
iban text unique not null, -- RO16 FOXB 0000 0000 0000 0000
|
||||||
|
currency text not null, -- EUR, RON, USD, ?
|
||||||
|
account_type text not null, -- checking, savings, ?
|
||||||
|
custom_name text -- 'Car Savings'; name set by user
|
||||||
|
);
|
||||||
|
|
||||||
|
create table users_accounts (
|
||||||
|
user_id integer not null, -- one user can have multiple accounts
|
||||||
|
account_id integer UNIQUE not null, -- one account can only have one user
|
||||||
|
foreign key (user_id) references users (id),
|
||||||
|
foreign key (account_id) references accounts (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table transactions (
|
||||||
|
id integer primary key autoincrement,
|
||||||
|
datetime text not null,
|
||||||
|
other_party text not null, -- JSON data describing sender/recipient/etc
|
||||||
|
-- depending on transaction type
|
||||||
|
status text not null, -- processed, failed, reverted, pending, etc
|
||||||
|
type text not null, -- send_transfer, receive_transfer, card_payment, fee, ...
|
||||||
|
extra text -- depending on type, JSON data describing extra info
|
||||||
|
);
|
||||||
|
|
||||||
|
create table accounts_transactions (
|
||||||
|
account_id integer not null,
|
||||||
|
transaction_id integer UNIQUE not null,
|
||||||
|
foreign key (account_id) references accounts (id),
|
||||||
|
foreign key (transaction_id) references transactions (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table notifications (
|
||||||
|
id integer primary key autoincrement,
|
||||||
|
body text not null,
|
||||||
|
datetime text not null,
|
||||||
|
read integer not null
|
||||||
|
);
|
||||||
|
|
||||||
|
create table users_notifications (
|
||||||
|
user_id integer not null,
|
||||||
|
notification_id integer UNIQUE not null,
|
||||||
|
foreign key (user_id) references users (id),
|
||||||
|
foreign key (notification_id) references notifications (id)
|
||||||
|
);
|
73
server/login.py
Normal file
73
server/login.py
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
from functools import wraps
|
||||||
|
from flask import Blueprint, request
|
||||||
|
|
||||||
|
from pyotp import TOTP
|
||||||
|
|
||||||
|
import db_utils
|
||||||
|
from decorators import no_content
|
||||||
|
import models
|
||||||
|
import ram_db
|
||||||
|
import returns
|
||||||
|
|
||||||
|
login = Blueprint('login', __name__)
|
||||||
|
|
||||||
|
@login.post('/')
|
||||||
|
def make_login():
|
||||||
|
try:
|
||||||
|
username = request.json['username']
|
||||||
|
code = request.json['code']
|
||||||
|
except (TypeError, KeyError):
|
||||||
|
return returns.INVALID_REQUEST
|
||||||
|
|
||||||
|
user: models.User | None = db_utils.get_user(username=username)
|
||||||
|
if user is None:
|
||||||
|
return returns.INVALID_DETAILS
|
||||||
|
|
||||||
|
otp = TOTP(user.otp)
|
||||||
|
if not otp.verify(code, valid_window=1):
|
||||||
|
return returns.INVALID_DETAILS
|
||||||
|
|
||||||
|
token = ram_db.login_user(user.id)
|
||||||
|
return returns.success(token=token)
|
||||||
|
|
||||||
|
def ensure_logged_in(token=False, user_id=False):
|
||||||
|
def decorator(fn):
|
||||||
|
pass_token = token
|
||||||
|
pass_user_id = user_id
|
||||||
|
@wraps(fn)
|
||||||
|
def wrapper(*args, **kargs):
|
||||||
|
token = request.headers.get('Authorization', None)
|
||||||
|
if token is None:
|
||||||
|
return returns.NO_AUTHORIZATION
|
||||||
|
if not token.startswith('Bearer '):
|
||||||
|
return returns.INVALID_AUTHORIZATION
|
||||||
|
token = token[7:]
|
||||||
|
user_id = ram_db.get_user(token)
|
||||||
|
if user_id is None:
|
||||||
|
return returns.INVALID_AUTHORIZATION
|
||||||
|
|
||||||
|
if pass_user_id and pass_token:
|
||||||
|
return fn(user_id=user_id, token=token, *args, **kargs)
|
||||||
|
elif pass_user_id:
|
||||||
|
return fn(user_id=user_id, *args, **kargs)
|
||||||
|
elif pass_token:
|
||||||
|
return fn(token=token, *args, **kargs)
|
||||||
|
else:
|
||||||
|
return fn(*args, **kargs)
|
||||||
|
return wrapper
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
@login.post('/logout')
|
||||||
|
@ensure_logged_in(token=True)
|
||||||
|
@no_content
|
||||||
|
def logout(token: str):
|
||||||
|
ram_db.logout_user(token)
|
||||||
|
|
||||||
|
@login.get('/whoami')
|
||||||
|
@ensure_logged_in(user_id=True)
|
||||||
|
def whoami(user_id: int):
|
||||||
|
user: models.User | None = db_utils.get_user(user_id=user_id)
|
||||||
|
if user is not None:
|
||||||
|
user = user.to_json()
|
||||||
|
|
||||||
|
return returns.success(user=user)
|
25
server/models.py
Normal file
25
server/models.py
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class User:
|
||||||
|
id: int
|
||||||
|
username: str
|
||||||
|
email: str
|
||||||
|
otp: str
|
||||||
|
fullname: str
|
||||||
|
|
||||||
|
def to_json(self, include_otp=False, include_id=False):
|
||||||
|
result = {
|
||||||
|
'username': self.username,
|
||||||
|
'email': self.email,
|
||||||
|
'fullname': self.fullname,
|
||||||
|
}
|
||||||
|
if include_id:
|
||||||
|
result['id'] = self.id
|
||||||
|
if include_otp:
|
||||||
|
result['otp'] = self.otp
|
||||||
|
return result
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_query(cls, query_result):
|
||||||
|
return cls(*query_result)
|
36
server/ram_db.py
Normal file
36
server/ram_db.py
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from types import TracebackType
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
USED_TOKENS = set()
|
||||||
|
LOGGED_IN_USERS: dict[str, (int, datetime)] = {}
|
||||||
|
|
||||||
|
def login_user(user_id: int) -> str:
|
||||||
|
'''
|
||||||
|
Creates token for user
|
||||||
|
'''
|
||||||
|
token = str(uuid4())
|
||||||
|
while token in USED_TOKENS:
|
||||||
|
token = str(uuid4())
|
||||||
|
if len(USED_TOKENS) > 10_000_000:
|
||||||
|
USED_TOKENS.clear()
|
||||||
|
for token in LOGGED_IN_USERS:
|
||||||
|
USED_TOKENS.add(token)
|
||||||
|
USED_TOKENS.add(token)
|
||||||
|
LOGGED_IN_USERS[token] = user_id, datetime.now()
|
||||||
|
return token
|
||||||
|
|
||||||
|
def logout_user(token: str):
|
||||||
|
if token in LOGGED_IN_USERS:
|
||||||
|
del LOGGED_IN_USERS[token]
|
||||||
|
|
||||||
|
def get_user(token: str) -> int | None:
|
||||||
|
if token not in LOGGED_IN_USERS:
|
||||||
|
return None
|
||||||
|
|
||||||
|
user_id, login_date = LOGGED_IN_USERS[token]
|
||||||
|
time_since_login: timedelta = datetime.now() - login_date
|
||||||
|
if time_since_login.total_seconds() > (60 * 30): # 30 mins
|
||||||
|
del LOGGED_IN_USERS[token]
|
||||||
|
return None
|
||||||
|
return user_id
|
46
server/returns.py
Normal file
46
server/returns.py
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
from http import HTTPStatus as _HTTPStatus
|
||||||
|
|
||||||
|
def _make_error(http_status, code: str):
|
||||||
|
try:
|
||||||
|
http_status = http_status[0]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return {
|
||||||
|
'status': 'error',
|
||||||
|
'code': code,
|
||||||
|
}, http_status
|
||||||
|
|
||||||
|
# General
|
||||||
|
|
||||||
|
INVALID_REQUEST = _make_error(
|
||||||
|
_HTTPStatus.BAD_REQUEST,
|
||||||
|
'general/invalid_request',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Login
|
||||||
|
|
||||||
|
INVALID_DETAILS = _make_error(
|
||||||
|
_HTTPStatus.UNAUTHORIZED,
|
||||||
|
'login/invalid_details',
|
||||||
|
)
|
||||||
|
|
||||||
|
NO_AUTHORIZATION = _make_error(
|
||||||
|
_HTTPStatus.UNAUTHORIZED,
|
||||||
|
'login/no_authorization',
|
||||||
|
)
|
||||||
|
|
||||||
|
INVALID_AUTHORIZATION = _make_error(
|
||||||
|
_HTTPStatus.UNAUTHORIZED,
|
||||||
|
'login/invalid_authorization',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Success
|
||||||
|
|
||||||
|
def success(http_status=_HTTPStatus.OK, /, **kargs):
|
||||||
|
try:
|
||||||
|
http_status = http_status[0]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return dict(kargs, status='success'), http_status
|
3
server/run.sh
Executable file
3
server/run.sh
Executable file
|
@ -0,0 +1,3 @@
|
||||||
|
#! /usr/bin/env sh
|
||||||
|
docker-compose stop
|
||||||
|
PORT=14000 docker-compose up -d --build
|
14
server/server.py
Normal file
14
server/server.py
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
from flask import Flask
|
||||||
|
from flask_cors import CORS
|
||||||
|
|
||||||
|
import db
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
CORS(app)
|
||||||
|
db.init_app(app)
|
||||||
|
|
||||||
|
from login import login
|
||||||
|
app.register_blueprint(login, url_prefix='/login')
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run()
|
Loading…
Add table
Reference in a new issue