├── data └── .gitkeep ├── front ├── .gitkeep └── dist │ ├── main.504f24a1ab60373303ef.css │ ├── favicon.ico │ ├── assets │ ├── favicon.ico │ └── logo.svg │ └── index.html ├── models ├── __init__.py ├── user_group.py ├── chat.py ├── role.py ├── message.py ├── user.py └── chat_permission.py ├── templates ├── .gitkeep ├── app.html └── index.html ├── helpers ├── __init__.py ├── validator_schema.py ├── acl.py ├── irc.py ├── log.py └── db_helper.py ├── middleware ├── __init__.py ├── police.py └── errors.py ├── public ├── media │ ├── .gitkeep │ └── avatars │ │ ├── default.png │ │ └── default_group.png └── static │ ├── favicon.ico │ ├── html_part │ └── facebook.html │ └── js │ ├── google.js │ ├── custom_auth.js │ └── fb.js ├── migrations ├── versions │ ├── .gitkeep │ ├── da550eab4fba_added_default_image_for_group_chat.py │ ├── f5113818dbf4_seed_roles.py │ ├── a6338e77be80_changed_email_column_is_bulleble_now.py │ ├── 61280f3791c1_add_cloumn_facebook_id_and_google_id.py │ ├── 9cab8e7b6419_seeds.py │ ├── aed52717ac26_move_chat_name_to_chat_permission.py │ ├── 014e2778ec23_create_message_and_chat_tables.py │ ├── 5a9d2b2aa480_create_chat_permission_tables.py │ └── 2e8df848f6cf_create_tables.py ├── script.py.mako └── env.py ├── socket_io ├── routes │ ├── __init__.py │ ├── user.py │ └── chat.py ├── config.py ├── helper.py └── main.py ├── config ├── __init__.py ├── jinja_init.py ├── gunicorn.py ├── config_example.py ├── connect_redis.py └── db.py ├── Dockerfile ├── requirements.txt ├── .env_example ├── routes ├── index.py ├── __init__.py └── user.py ├── docker-compose.dev.yml ├── .gitignore ├── README.md ├── nginx └── dev │ ├── nginx.conf │ └── conf.d │ └── web.conf ├── app.py ├── alembic.ini ├── create_admin.py └── aiohttp_sqlalchemy_alembic.postman_collection.json /data/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /front/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /models/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /helpers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /middleware/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/media/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /migrations/versions/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /socket_io/routes/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /front/dist/main.504f24a1ab60373303ef.css: -------------------------------------------------------------------------------- 1 | html,html body{height:100%;margin:0;padding:0} -------------------------------------------------------------------------------- /front/dist/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sicksick/aiohttp_sqlalchemy_alembic/HEAD/front/dist/favicon.ico -------------------------------------------------------------------------------- /public/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sicksick/aiohttp_sqlalchemy_alembic/HEAD/public/static/favicon.ico -------------------------------------------------------------------------------- /config/__init__.py: -------------------------------------------------------------------------------- 1 | from config.config import config 2 | 3 | 4 | def setup_config(app): 5 | setattr(app, 'config', config) 6 | -------------------------------------------------------------------------------- /front/dist/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sicksick/aiohttp_sqlalchemy_alembic/HEAD/front/dist/assets/favicon.ico -------------------------------------------------------------------------------- /public/media/avatars/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sicksick/aiohttp_sqlalchemy_alembic/HEAD/public/media/avatars/default.png -------------------------------------------------------------------------------- /public/media/avatars/default_group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sicksick/aiohttp_sqlalchemy_alembic/HEAD/public/media/avatars/default_group.png -------------------------------------------------------------------------------- /templates/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | App 5 | 6 | 7 |

APP

8 | 9 | -------------------------------------------------------------------------------- /public/static/html_part/facebook.html: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /config/jinja_init.py: -------------------------------------------------------------------------------- 1 | import aiohttp_jinja2 2 | import jinja2 3 | 4 | 5 | def jinja_init(app): 6 | path = app.config['root_path'] + "/templates" 7 | aiohttp_jinja2.setup(app, 8 | loader=jinja2.FileSystemLoader(path)) 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7 2 | ENV PYTHONUNBUFFERED 1 3 | RUN mkdir /code 4 | WORKDIR /code 5 | COPY requirements.txt /code 6 | RUN apt-get install libffi-dev libssl-dev 7 | RUN pip3 install --upgrade pip 8 | RUN pip3 install -r requirements.txt 9 | ADD . /code/ -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp 2 | aiopg 3 | alembic 4 | cchardet 5 | aiodns 6 | aiojobs 7 | aiohttp-jinja2 8 | aiohttp_session 9 | aioredis 10 | psycopg2-binary 11 | SQLAlchemy 12 | pyjwt 13 | bcrypt 14 | jinja2 15 | python_socketio 16 | uvloop 17 | gunicorn 18 | pytz 19 | cerberus -------------------------------------------------------------------------------- /socket_io/routes/user.py: -------------------------------------------------------------------------------- 1 | from socket_io.config import ROUTES 2 | 3 | 4 | def get_user_routes(sio, app): 5 | 6 | @sio.on(ROUTES['BACK']['USER']['INVITE']) 7 | async def user_invite(sid): 8 | print(ROUTES['BACK']['USER']['INVITE']) 9 | 10 | @sio.on(ROUTES['BACK']['USER']['EXCLUDE']) 11 | async def user_exclude(sid): 12 | print(ROUTES['BACK']['USER']['EXCLUDE']) 13 | -------------------------------------------------------------------------------- /helpers/validator_schema.py: -------------------------------------------------------------------------------- 1 | from aiohttp.http_exceptions import HttpProcessingError 2 | from cerberus import Validator 3 | 4 | from middleware.errors import CustomHTTPException 5 | 6 | 7 | async def validate(data, schema): 8 | v = Validator() 9 | 10 | if v.validate(data, schema) is False: 11 | print(v.errors) 12 | raise CustomHTTPException(v.errors, 422) 13 | 14 | return True 15 | -------------------------------------------------------------------------------- /config/gunicorn.py: -------------------------------------------------------------------------------- 1 | bind = '0.0.0.0:8080' 2 | 3 | backlog = 1024 4 | 5 | workers = 1 6 | # uvloop Ultra fast asyncio event loop https://github.com/MagicStack/uvloop 7 | worker_class = 'aiohttp.GunicornUVLoopWebWorker' 8 | worker_connections = 1000 9 | timeout = 60 * 60 10 | 11 | 12 | errorlog = '-' 13 | loglevel = 'info' 14 | accesslog = '-' 15 | access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"' 16 | -------------------------------------------------------------------------------- /config/config_example.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pathlib 3 | 4 | config = dict() 5 | 6 | config['root_path'] = str(pathlib.Path(__file__).parent.parent) 7 | config['log_path'] = 'logs' 8 | config['sleep_sec'] = 3 9 | config['secret'] = os.getenv('JWT_HASH_SECRET') 10 | config['studio_secret'] = 'a6u)_f*@yhpjdnarneamz+q--gce_)5k=i6^3xq$u4re=$p%6-' 11 | config['db'] = None 12 | config['base_url'] = 'http://0.0.0.0' 13 | config['timezone'] = 'UTC' 14 | -------------------------------------------------------------------------------- /.env_example: -------------------------------------------------------------------------------- 1 | #WEB 2 | HOST=0.0.0.0 3 | PORT=8080 4 | DEBUG=True 5 | JWT_HASH_SECRET=indus_app_mega_secret 6 | URL_REDIRECT_AFTER_LOGIN=/app/ 7 | 8 | #DB 9 | POSTGRES_USER=postgres 10 | POSTGRES_DB=app 11 | POSTGRES_PASSWORD=postgres 12 | POSTGRES_HOST=postgres 13 | POSTGRES_PORT=5432 14 | POSTGRES_TYPE=postgresql 15 | DB_EXTENSION=pg_trgm 16 | 17 | #REDIS 18 | REDIS_HOST=redis 19 | REDIS_PORT=6379 20 | 21 | #FACEBOOK AUTH 22 | #FACEBOOK_ID=99999999999 23 | 24 | #GOOGLE AUTH 25 | #GOOGLE_SIGNIN_CLIENT_ID=saaaaasasasasasasasasas.apps.googleusercontent.com -------------------------------------------------------------------------------- /config/connect_redis.py: -------------------------------------------------------------------------------- 1 | import aioredis 2 | import asyncio 3 | 4 | import os 5 | from aiohttp_session.redis_storage import RedisStorage 6 | 7 | 8 | def redis_connect(app): 9 | async def make_redis_pool(): 10 | redis_address = (os.getenv('REDIS_HOST', 'redis'), os.getenv('REDIS_PORT', '6379')) 11 | return await aioredis.create_redis_pool(redis_address, timeout=1) 12 | 13 | loop = asyncio.get_event_loop() 14 | redis_pool = loop.run_until_complete(make_redis_pool()) 15 | storage = RedisStorage(redis_pool) 16 | return storage, redis_pool 17 | 18 | -------------------------------------------------------------------------------- /migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /config/db.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import aiopg.sa 4 | 5 | from config import config 6 | 7 | 8 | async def init_pg(app): 9 | engine = await aiopg.sa.create_engine( 10 | database=os.getenv('POSTGRES_DB'), 11 | user=os.getenv('POSTGRES_USER'), 12 | password=os.getenv('POSTGRES_PASSWORD'), 13 | host=os.getenv('POSTGRES_HOST'), 14 | port=os.getenv('POSTGRES_PORT'), 15 | minsize=1, 16 | maxsize=5, 17 | loop=app.loop) 18 | config['db'] = engine 19 | setattr(app, 'db', engine) 20 | 21 | 22 | async def close_pg(app): 23 | app.db.close() 24 | await app.db.wait_closed() 25 | 26 | -------------------------------------------------------------------------------- /routes/index.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import aiohttp_jinja2 4 | 5 | 6 | def init(app): 7 | app.router.add_get('/app', front_app) 8 | app.router.add_get('/', home) 9 | 10 | 11 | @aiohttp_jinja2.template('index.html') 12 | async def home(request): 13 | GOOGLE_SIGNIN_CLIENT_ID = os.getenv('GOOGLE_SIGNIN_CLIENT_ID', None) 14 | facebook_id = os.getenv('FACEBOOK_ID', None) 15 | url_redirect_after_login = os.getenv('URL_REDIRECT_AFTER_LOGIN', None) 16 | return { 17 | 'FACEBOOK_ID': facebook_id, 18 | 'URL_REDIRECT_AFTER_LOGIN': url_redirect_after_login, 19 | 'GOOGLE_SIGNIN_CLIENT_ID': GOOGLE_SIGNIN_CLIENT_ID 20 | } 21 | 22 | 23 | @aiohttp_jinja2.template('app.html') 24 | async def front_app(request): 25 | return {'data': ''} 26 | -------------------------------------------------------------------------------- /helpers/acl.py: -------------------------------------------------------------------------------- 1 | import functools 2 | from helpers.irc import irc 3 | from middleware.errors import CustomHTTPException 4 | 5 | 6 | def acl(roles: list): 7 | def wrapper(func): 8 | @functools.wraps(func) 9 | async def wrapped(*args): 10 | allowed = False 11 | if len(roles) == 0: 12 | return await func(*args) 13 | try: 14 | for role in roles: 15 | if role in args[0].user['roles']: 16 | allowed = True 17 | break 18 | 19 | if not allowed: 20 | return CustomHTTPException(irc['ACCESS_DENIED'], 401) 21 | except: 22 | return CustomHTTPException(irc['ACCESS_DENIED'], 401) 23 | return await func(*args) 24 | return wrapped 25 | return wrapper 26 | -------------------------------------------------------------------------------- /migrations/versions/da550eab4fba_added_default_image_for_group_chat.py: -------------------------------------------------------------------------------- 1 | """added default image for group chat 2 | 3 | Revision ID: da550eab4fba 4 | Revises: 5a9d2b2aa480 5 | Create Date: 2018-12-01 11:53:22.275753 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'da550eab4fba' 14 | down_revision = '5a9d2b2aa480' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('chats_permission', sa.Column('chat_image', sa.Text(), nullable=True)) 22 | # ### end Alembic commands ### 23 | 24 | 25 | def downgrade(): 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | op.drop_column('chats_permission', 'chat_image') 28 | # ### end Alembic commands ### 29 | -------------------------------------------------------------------------------- /routes/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | def apply_routes(app): 5 | 6 | for file in [file for file in os.listdir(app.config['root_path'] + "/routes/") if file != '__pycache__' and file != '__init__.py']: 7 | p, m = file.rsplit('.', 1) 8 | module_in_file = __import__("routes." + str(p)) 9 | files_module = getattr(module_in_file, p) 10 | init = getattr(files_module, 'init') 11 | if "init" in dir(): 12 | init(app) 13 | del init 14 | 15 | media = str(app.config['root_path']) + '/public/media' 16 | app.router.add_static("/media/", 17 | path=str(media), 18 | name="media") 19 | 20 | static = str(app.config['root_path']) + "/public/static" 21 | app.router.add_static("/", 22 | path=str(static), 23 | name="static") 24 | -------------------------------------------------------------------------------- /front/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Chat React Redux Webpack 4 8 | 9 | 10 | 11 | 12 | 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /migrations/versions/f5113818dbf4_seed_roles.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: f5113818dbf4 4 | Revises: 2e8df848f6cf 5 | Create Date: 2018-08-09 14:55:41.204721 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | from models.role import sa_role 14 | 15 | revision = 'f5113818dbf4' 16 | down_revision = '2e8df848f6cf' 17 | branch_labels = None 18 | depends_on = None 19 | 20 | 21 | def upgrade(): 22 | # ### commands auto generated by Alembic - please adjust! ### 23 | op.bulk_insert(sa_role, 24 | [ 25 | {'id': 1, 'name': 'admin'}, 26 | {'id': 2, 'name': 'user'}, 27 | ] 28 | ) 29 | # ### end Alembic commands ### 30 | 31 | 32 | def downgrade(): 33 | # ### commands auto generated by Alembic - please adjust! ### 34 | op.execute('TRUNCATE roles CASCADE ;') 35 | # ### end Alembic commands ### 36 | -------------------------------------------------------------------------------- /migrations/versions/a6338e77be80_changed_email_column_is_bulleble_now.py: -------------------------------------------------------------------------------- 1 | """changed email, column is bulleble now 2 | 3 | Revision ID: a6338e77be80 4 | Revises: 61280f3791c1 5 | Create Date: 2018-08-13 20:21:36.647486 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'a6338e77be80' 14 | down_revision = '61280f3791c1' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.alter_column('users', 'email', 22 | existing_type=sa.VARCHAR(), 23 | nullable=True) 24 | # ### end Alembic commands ### 25 | 26 | 27 | def downgrade(): 28 | # ### commands auto generated by Alembic - please adjust! ### 29 | op.alter_column('users', 'email', 30 | existing_type=sa.VARCHAR(), 31 | nullable=False) 32 | # ### end Alembic commands ### 33 | -------------------------------------------------------------------------------- /migrations/versions/61280f3791c1_add_cloumn_facebook_id_and_google_id.py: -------------------------------------------------------------------------------- 1 | """add cloumn facebook_id and google_id 2 | 3 | Revision ID: 61280f3791c1 4 | Revises: f5113818dbf4 5 | Create Date: 2018-08-13 19:59:54.122336 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '61280f3791c1' 14 | down_revision = 'f5113818dbf4' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('users', sa.Column('facebook_id', sa.String(), nullable=True)) 22 | op.add_column('users', sa.Column('google_id', sa.String(), nullable=True)) 23 | # ### end Alembic commands ### 24 | 25 | 26 | def downgrade(): 27 | # ### commands auto generated by Alembic - please adjust! ### 28 | op.drop_column('users', 'google_id') 29 | op.drop_column('users', 'facebook_id') 30 | # ### end Alembic commands ### 31 | -------------------------------------------------------------------------------- /helpers/irc.py: -------------------------------------------------------------------------------- 1 | irc = { 2 | 'INTERNAL_SERVER_ERROR': { 3 | "ERROR_MESSAGE": "Internal Server Error", 4 | "ERROR_CODE": 500 5 | }, 6 | 'USER_NOT_FOUND': { 7 | "ERROR_MESSAGE": "User not found", 8 | "ERROR_CODE": 1001 9 | }, 10 | 'INVALID_EMAIL_ADDRESS': { 11 | "ERROR_MESSAGE": "Invalid Email Address", 12 | "ERROR_CODE": 1002 13 | }, 14 | 'ACCESS_DENIED': { 15 | "ERROR_MESSAGE": "Access denied", 16 | "ERROR_CODE": 1003 17 | }, 18 | 'NOT_FOUND': { 19 | "ERROR_MESSAGE": "Not found", 20 | "ERROR_CODE": 1004 21 | }, 22 | 'USER_EXISTS': { 23 | "ERROR_MESSAGE": "User exists", 24 | "ERROR_CODE": 1005 25 | }, 26 | 'ROLE_NOT_FOUND': { 27 | "ERROR_MESSAGE": "Role not found", 28 | "ERROR_CODE": 1006 29 | }, 30 | 'EMAIL_ROLES_AND_PASSWORD_IS_REQUIRED': { 31 | "ERROR_MESSAGE": "Email, roles and password is required", 32 | "ERROR_CODE": 1007 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /models/user_group.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, ForeignKey 2 | from sqlalchemy.ext.declarative import declarative_base 3 | from config import config 4 | from helpers.irc import irc 5 | from middleware.errors import CustomHTTPException 6 | Base = declarative_base() 7 | 8 | 9 | class UserGroup(Base): 10 | __tablename__ = 'user_groups' 11 | id = Column(Integer, primary_key=True, nullable=False) 12 | user_id = Column(Integer, ForeignKey('users.id'), nullable=False) 13 | role_id = Column(Integer, ForeignKey('roles.id'), nullable=False) 14 | 15 | @staticmethod 16 | async def add_role_to_user(user_id: int, role_id: int) -> bool: 17 | async with config['db'].acquire() as conn: 18 | query = sa_user_group.insert().values({"user_id": user_id, "role_id": role_id}) 19 | result = list(map(lambda x: dict(x), await conn.execute(query))) 20 | if len(result) != 1: 21 | raise CustomHTTPException(irc['INTERNAL_SERVER_ERROR'], 500) 22 | return True 23 | 24 | 25 | sa_user_group = UserGroup.__table__ 26 | -------------------------------------------------------------------------------- /helpers/log.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import logging 3 | import os 4 | from logging.handlers import TimedRotatingFileHandler 5 | 6 | 7 | def create_loggers(app): 8 | log_path = app.config['root_path'] + "/" + app.config['log_path'] + "/" 9 | ensure_dir(log_path) 10 | rotation_logger = create_rotating_logger(log_path) 11 | 12 | setattr(app, 'loggers', { 13 | "rotating": rotation_logger 14 | }) 15 | 16 | 17 | def create_rotating_logger(path): 18 | logger = logging.getLogger("Rotating Log") 19 | logger.setLevel(logging.ERROR) 20 | file_name = "rotating" + datetime.datetime.now().strftime("%Y-%m-%d") + ".log" 21 | handler = TimedRotatingFileHandler(path+file_name, 22 | when="d", 23 | interval=1, 24 | backupCount=5) 25 | logger.addHandler(handler) 26 | return logger 27 | 28 | 29 | def ensure_dir(file_path): 30 | directory = os.path.dirname(file_path) 31 | if not os.path.exists(directory): 32 | os.makedirs(directory) 33 | -------------------------------------------------------------------------------- /public/static/js/google.js: -------------------------------------------------------------------------------- 1 | if (GOOGLE_SIGNIN_CLIENT_ID !== "None") { 2 | 3 | var clicked = false;//Global Variable 4 | 5 | function clickGoogleLogin() { 6 | clicked = true; 7 | } 8 | 9 | $(document).ready(function () { 10 | $(".google-auth").removeClass("d-none"); 11 | }); 12 | 13 | function onSignIn(googleUser) { 14 | if (clicked) { 15 | $.post("/api/user/login/google", JSON.stringify({ 16 | "token": googleUser.getAuthResponse().id_token 17 | }), function () { 18 | }, "json") 19 | .done(function (data) { 20 | localStorage.setItem('user', JSON.stringify(data.user)); 21 | localStorage.setItem('token', data.token); 22 | localStorage.setItem('roles', JSON.stringify(data.roles)); 23 | document.location.href = URL_REDIRECT_AFTER_LOGIN 24 | }) 25 | .fail(function () { 26 | $(".alert-google-warning").addClass('show').alert(); 27 | }); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /helpers/db_helper.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from helpers.irc import irc 3 | from middleware.errors import CustomHTTPException 4 | 5 | 6 | def as_dict(obj): 7 | if isinstance(obj, list): 8 | for items in obj: 9 | for item in items: 10 | if isinstance(items[item], datetime.datetime): 11 | items[item] = str(items[item]) 12 | if isinstance(items[item], datetime.timedelta): 13 | items[item] = str(items[item]) 14 | return obj 15 | if isinstance(obj, dict): 16 | for item in obj: 17 | if isinstance(obj[item], datetime.datetime): 18 | obj[item] = str(obj[item]) 19 | if isinstance(obj[item], datetime.timedelta): 20 | obj[item] = str(obj[item]) 21 | return obj 22 | return obj 23 | 24 | 25 | async def raise_db_exception(e): 26 | error = irc['INTERNAL_SERVER_ERROR'] 27 | if len(e.args) > 0: 28 | error = { 29 | "ERROR_MESSAGE": e.args[0], 30 | "ERROR_CODE": None 31 | } 32 | raise CustomHTTPException(error, 422) 33 | -------------------------------------------------------------------------------- /docker-compose.dev.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | nginx: 4 | depends_on: 5 | - web 6 | image: nginx:stable 7 | volumes: 8 | - ./nginx/dev/conf.d/web.conf:/etc/nginx/conf.d/web.conf 9 | - ./nginx/dev/nginx.conf:/etc/nginx/nginx.conf 10 | - .:/code 11 | restart: always 12 | ports: 13 | - 80:8888 14 | web: 15 | depends_on: 16 | - postgres 17 | - redis 18 | build: . 19 | env_file: .env 20 | restart: always 21 | command: bash -I -c "gunicorn app:app --config python:config.gunicorn" 22 | links: 23 | - postgres 24 | - redis 25 | volumes: 26 | - .:/code 27 | expose: 28 | - "8080" 29 | ports: 30 | - 8080:8080 31 | postgres: 32 | image: postgres:9.6 33 | env_file: .env 34 | volumes: 35 | - ./data/postgres-data:/var/lib/postgresql/data 36 | ports: 37 | - "8004:5432" 38 | redis: 39 | image: redis:latest 40 | env_file: .env 41 | volumes: 42 | - ./data/redis_data:/data 43 | ports: 44 | - "6380:6379" 45 | 46 | networks: 47 | default: 48 | external: 49 | name: webproxy -------------------------------------------------------------------------------- /migrations/versions/9cab8e7b6419_seeds.py: -------------------------------------------------------------------------------- 1 | """seeds 2 | 3 | Revision ID: 9cab8e7b6419 4 | Revises: aed52717ac26 5 | Create Date: 2018-12-02 16:28:03.602945 6 | 7 | """ 8 | from alembic import op 9 | 10 | # revision identifiers, used by Alembic. 11 | from models.chat import sa_chat 12 | 13 | revision = '9cab8e7b6419' 14 | down_revision = 'aed52717ac26' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.bulk_insert(sa_chat, [{}]) 22 | op.execute(""" 23 | INSERT INTO "public"."chats_permission" ("id", "chat_id", "user_id", "permission", "created_at", "updated_at", "chat_image", "chat_name") 24 | VALUES (DEFAULT, 1, NULL, 'user', '2018-10-18 17:39:26.026000', '2018-10-18 17:39:27.866000', '/media/avatars/default_group.png', 'General'); 25 | """) 26 | 27 | # ### end Alembic commands ### 28 | 29 | 30 | def downgrade(): 31 | # ### commands auto generated by Alembic - please adjust! ### 32 | op.execute('TRUNCATE chats_permission CASCADE ;') 33 | op.execute('TRUNCATE chats CASCADE ;') 34 | # ### end Alembic commands ### 35 | -------------------------------------------------------------------------------- /migrations/versions/aed52717ac26_move_chat_name_to_chat_permission.py: -------------------------------------------------------------------------------- 1 | """move chat name to chat permission 2 | 3 | Revision ID: aed52717ac26 4 | Revises: da550eab4fba 5 | Create Date: 2018-12-01 23:54:14.356783 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'aed52717ac26' 14 | down_revision = 'da550eab4fba' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.drop_constraint('chats_name_key', 'chats', type_='unique') 22 | op.drop_column('chats', 'name') 23 | op.add_column('chats_permission', sa.Column('chat_name', sa.String(), nullable=True)) 24 | # ### end Alembic commands ### 25 | 26 | 27 | def downgrade(): 28 | # ### commands auto generated by Alembic - please adjust! ### 29 | op.drop_column('chats_permission', 'chat_name') 30 | op.add_column('chats', sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=True)) 31 | op.create_unique_constraint('chats_name_key', 'chats', ['name']) 32 | # ### end Alembic commands ### 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /tmp 5 | /out-tsc 6 | 7 | # dependencies 8 | /node_modules 9 | 10 | # IDEs and editors 11 | /.idea 12 | .project 13 | .classpath 14 | .c9/ 15 | *.launch 16 | .settings/ 17 | *.sublime-workspace 18 | 19 | # IDE - VSCode 20 | .vscode/* 21 | !.vscode/settings.json 22 | !.vscode/tasks.json 23 | !.vscode/launch.json 24 | !.vscode/extensions.json 25 | 26 | # misc 27 | /.sass-cache 28 | /connect.lock 29 | /coverage 30 | /libpeerconnection.log 31 | npm-debug.log 32 | testem.log 33 | /typings 34 | 35 | # e2e 36 | /e2e/*.js 37 | /e2e/*.map 38 | 39 | # System Files 40 | .DS_Store 41 | Thumbs.db 42 | /static/client 43 | /static/member 44 | /log/* 45 | /logs/* 46 | 47 | *.egg-info 48 | *.pot 49 | *.py[co] 50 | .tox/ 51 | __pycache__ 52 | MANIFEST 53 | docs/_build/ 54 | docs/locale/ 55 | node_modules/ 56 | tests/coverage_html/ 57 | tests/.coverage 58 | build/ 59 | venv/ 60 | /venv/ 61 | tests/report/ 62 | /config/config.yaml 63 | config.yaml 64 | config.yml 65 | /logs/*.* 66 | logs 67 | redis_data 68 | postgres-data 69 | .DS_Store 70 | .env 71 | config/config.py 72 | docker-compose.yml -------------------------------------------------------------------------------- /public/static/js/custom_auth.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function () { 2 | $("form#formLogin").submit(function (event) { 3 | event.preventDefault(); 4 | 5 | var formData = $("#formLogin").serializeArray(); 6 | var data = {}; 7 | for (var i = 0; i < formData.length; i++) { 8 | data[formData[i].name] = formData[i].value; 9 | } 10 | $.ajax({ 11 | url: "/api/user/login", 12 | type: "POST", 13 | data: JSON.stringify(data), 14 | contentType: "application/json; charset=utf-8", 15 | dataType: "json", 16 | success: function (data) { 17 | localStorage.setItem('user', JSON.stringify(data.user)); 18 | localStorage.setItem('token', data.token); 19 | localStorage.setItem('roles', JSON.stringify(data.roles)); 20 | document.location.href = URL_REDIRECT_AFTER_LOGIN 21 | return false; 22 | }, 23 | error: function () { 24 | $(".alert-custom-auth-warning").addClass('show').alert(); 25 | return false; 26 | } 27 | }); 28 | }); 29 | 30 | }); -------------------------------------------------------------------------------- /socket_io/config.py: -------------------------------------------------------------------------------- 1 | users_socket = dict() 2 | users_by_user_id = dict() 3 | 4 | ROUTES = { 5 | 'FRONT': { 6 | 'CONNECT': 'connect', 7 | 'DISCONNECT': 'disconnect', 8 | 'AUTH': 'auth', 9 | 'USER': { 10 | 'ALL': 'user:all' 11 | }, 12 | 'CHAT': { 13 | 'STATUS': 'chat:status', 14 | 'PARTICIPATED': 'chat:participated', 15 | 'MESSAGE': { 16 | 'HISTORY': 'chat:message:history', 17 | 'NEW': 'chat:message:new' 18 | } 19 | } 20 | }, 21 | 'BACK': { 22 | 'CONNECT': 'connect', 23 | 'DISCONNECT': 'disconnect', 24 | 'USER': { 25 | 'INVITE': 'user:invite', 26 | 'EXCLUDE': 'user:exclude' 27 | }, 28 | 'CHAT': { 29 | 'CREATE': 'chat:create', 30 | 'REMOVE': 'chat:remove', 31 | 'INVITE': 'chat:invite', 32 | 'CHANGE': 'chat:change', 33 | 'MESSAGE': { 34 | 'NEW': 'chat:message:new', 35 | 'EDIT': 'chat:message:edit', 36 | 'REMOVE': 'chat:message:remove', 37 | } 38 | }, 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /models/chat.py: -------------------------------------------------------------------------------- 1 | from aiopg.sa import SAConnection 2 | from sqlalchemy import Column, DateTime, Integer, String, func, text, literal_column 3 | from sqlalchemy.ext.declarative import declarative_base 4 | from helpers.db_helper import raise_db_exception, as_dict 5 | from helpers.irc import irc 6 | from middleware.errors import CustomHTTPException 7 | 8 | 9 | Base = declarative_base() 10 | 11 | 12 | class Chat(Base): 13 | __tablename__ = 'chats' 14 | id = Column(Integer, primary_key=True, nullable=False) 15 | created_at = Column(DateTime, default=func.now()) 16 | updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) 17 | 18 | @staticmethod 19 | async def create_new_chat_by_name(connect: SAConnection) -> dict: 20 | try: 21 | query = sa_chat.insert(inline=True) 22 | query = query.values([]).returning(literal_column('*')) 23 | new_chat = as_dict(dict((await (await connect.execute(query)).fetchall())[0])) 24 | 25 | if not new_chat: 26 | raise CustomHTTPException(irc['INTERNAL_SERVER_ERROR'], 500) 27 | 28 | return new_chat 29 | except Exception as e: 30 | raise await raise_db_exception(e) 31 | 32 | 33 | sa_chat = Chat.__table__ 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aiohttp nginx postgresql redis docker 2 | # socker.io sqlalchemy alembic 3 | # google and fasebook auth 4 | 5 | 6 | ##### Setup: 7 | ``` 8 | cp docker-compose.dev.yml docker-compose.yml 9 | cp config/config_example.py config/config.py 10 | cp .env_example .env 11 | add FACEBOOK_ID to .env file 12 | add GOOGLE_SIGNIN_CLIENT_ID to .env file 13 | docker-compose up -d 14 | docker-compose exec web alembic upgrade head 15 | docker-compose exec web python create_admin.py 16 | ``` 17 | 18 | #### [Check it -> http://localhost/](http://localhost/) 19 | 20 | 21 | ##### Postman collection 22 | ``` 23 | aiohttp_sqlalchemy_alembic.postman_collection.json 24 | ``` 25 | 26 | 27 | ##### Rest api based on AIOHTTP with: 28 | ``` 29 | - Socket.io; 30 | - Sqlalchemy; 31 | - Alembic and auto generating migrations; 32 | - Provides session by redis storages; 33 | - Postgres database; 34 | - Access control list; 35 | - Authentication by JWT tokens. 36 | ``` 37 | 38 | ### Alembic for db migrations 39 | ``` 40 | Genarate new migration: 41 | - docker-compose exec web alembic revision --autogenerate -m "create tables" 42 | Migrate: 43 | - docker-compose exec web alembic upgrade head 44 | Migrate undo last: 45 | - docker-compose exec web alembic downgrade -1 46 | ``` -------------------------------------------------------------------------------- /nginx/dev/nginx.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes 2; 3 | 4 | error_log /var/log/nginx/error.log warn; 5 | pid /var/run/nginx.pid; 6 | 7 | events { 8 | worker_connections 1024; 9 | use epoll; 10 | accept_mutex off; 11 | } 12 | 13 | http { 14 | include /etc/nginx/mime.types; 15 | proxy_set_header X-Real-IP $remote_addr; 16 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 17 | 18 | default_type application/octet-stream; 19 | 20 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 21 | '$status $body_bytes_sent "$http_referer" ' 22 | '"$http_user_agent" "$http_x_forwarded_for"'; 23 | 24 | access_log /var/log/nginx/access.log main; 25 | 26 | sendfile on; 27 | #tcp_nopush on; 28 | 29 | keepalive_timeout 65; 30 | 31 | client_max_body_size 0; 32 | client_body_buffer_size 128k; 33 | 34 | gzip on; 35 | gzip_http_version 1.0; 36 | gzip_comp_level 6; 37 | gzip_min_length 0; 38 | gzip_buffers 16 8k; 39 | gzip_proxied any; 40 | gzip_types text/plain text/css text/xml text/javascript application/xml application/xml+rss application/javascript application/json; 41 | gzip_disable "MSIE [1-6]\."; 42 | gzip_vary on; 43 | 44 | include /etc/nginx/conf.d/*.conf; 45 | } -------------------------------------------------------------------------------- /models/role.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, String 2 | from sqlalchemy.ext.declarative import declarative_base 3 | from sqlalchemy.sql import text 4 | from config import config 5 | Base = declarative_base() 6 | 7 | 8 | class Role(Base): 9 | __tablename__ = 'roles' 10 | id = Column(Integer, primary_key=True, nullable=False) 11 | name = Column(String, nullable=False) 12 | 13 | @staticmethod 14 | async def get_roles_by_id(id: int) -> list: 15 | async with config['db'].acquire() as conn: 16 | query = text(""" 17 | SELECT 18 | r.name as name 19 | FROM user_groups 20 | left join roles r on user_groups.role_id = r.id 21 | where 22 | user_groups.user_id = :id 23 | ; 24 | """) 25 | return list(map(lambda x: dict(x), await conn.execute(query, id=id))) 26 | 27 | @staticmethod 28 | async def get_role_by_name(role_name: str) -> list or None: 29 | async with config['db'].acquire() as conn: 30 | query = text(""" 31 | SELECT 32 | roles.* 33 | FROM roles 34 | where 35 | roles.name = :role_name 36 | ; 37 | """) 38 | roles = list(map(lambda x: dict(x), await conn.execute(query, role_name=role_name))) 39 | if len(roles) == 1: 40 | return roles[0] 41 | return None 42 | 43 | 44 | sa_role = Role.__table__ 45 | -------------------------------------------------------------------------------- /socket_io/helper.py: -------------------------------------------------------------------------------- 1 | from models.chat_permission import ChatPermission 2 | from models.message import Message 3 | from socket_io.config import ROUTES, users_socket 4 | 5 | 6 | async def send_participated_by_user_id_and_send_messages(sio, sid: str, active_chat_id: int) -> None: 7 | participated = await get_and_send_participated_by_user_id(sio, int(users_socket[sid]['id']), sid) 8 | active_chat = participated[0] 9 | 10 | for chat_item in participated: 11 | if chat_item['chat_id'] == active_chat_id: 12 | active_chat = chat_item 13 | 14 | await send_messages_by_chat_name(sio, sid, active_chat) 15 | 16 | 17 | async def send_messages_by_chat_name(sio, sid: str, active_participated=None) -> None: 18 | if not active_participated: 19 | return await sio.emit(ROUTES['FRONT']['CHAT']['MESSAGE']['HISTORY'], { 20 | 'data': { 21 | 'messages': [], 22 | 'chat': {} 23 | } 24 | }, room=sid) 25 | 26 | first_participated_messages = await Message.get_messages_by_chat_id(active_participated['chat_id']) 27 | return await sio.emit(ROUTES['FRONT']['CHAT']['MESSAGE']['HISTORY'], { 28 | 'data': { 29 | 'messages': first_participated_messages, 30 | 'chat': active_participated 31 | } 32 | }, room=sid) 33 | 34 | 35 | async def get_and_send_participated_by_user_id(sio, user_id: int, sid: str) -> list: 36 | participated = await ChatPermission.get_participated_by_user_id(int(user_id)) 37 | await sio.emit(ROUTES['FRONT']['CHAT']['PARTICIPATED'], {'data': participated}, room=sid) 38 | return participated 39 | -------------------------------------------------------------------------------- /nginx/dev/conf.d/web.conf: -------------------------------------------------------------------------------- 1 | upstream web { 2 | ip_hash; 3 | server web:8080; 4 | } 5 | 6 | server { 7 | listen 8888; 8 | 9 | client_body_buffer_size 90M; 10 | client_max_body_size 100M; 11 | 12 | location /app/ { 13 | autoindex on; 14 | alias /code/front/dist/; 15 | } 16 | 17 | location /static/ { 18 | autoindex on; 19 | alias /code/public/static/; 20 | } 21 | 22 | location /media/ { 23 | autoindex on; 24 | alias /code/public/media/; 25 | } 26 | 27 | location /.well-known/apple-app-site-association { 28 | add_header Content-Type application/json; 29 | alias /var/www/html/apple-app-site-assocation; 30 | } 31 | 32 | location /apple-app-site-association { 33 | add_header Content-Type application/json; 34 | alias /var/www/html/apple-app-site-assocation; 35 | } 36 | 37 | location /socket.io { 38 | proxy_set_header Upgrade $http_upgrade; 39 | proxy_set_header Connection "upgrade"; 40 | proxy_http_version 1.1; 41 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 42 | proxy_set_header Host $host; 43 | proxy_pass http://web; 44 | } 45 | 46 | location / { 47 | proxy_set_header Upgrade $http_upgrade; 48 | proxy_set_header Connection "upgrade"; 49 | proxy_http_version 1.1; 50 | proxy_set_header Host $host; 51 | proxy_pass http://web/; 52 | proxy_connect_timeout 300; 53 | proxy_send_timeout 300; 54 | proxy_read_timeout 300; 55 | send_timeout 300; 56 | uwsgi_read_timeout 1800; 57 | uwsgi_send_timeout 300; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /migrations/versions/014e2778ec23_create_message_and_chat_tables.py: -------------------------------------------------------------------------------- 1 | """create message and chat tables 2 | 3 | Revision ID: 014e2778ec23 4 | Revises: a6338e77be80 5 | Create Date: 2018-10-06 20:37:57.864924 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '014e2778ec23' 14 | down_revision = 'a6338e77be80' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('chats', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('name', sa.String(), nullable=True), 24 | sa.Column('created_at', sa.DateTime(), nullable=True), 25 | sa.Column('updated_at', sa.DateTime(), nullable=True), 26 | sa.PrimaryKeyConstraint('id'), 27 | sa.UniqueConstraint('name') 28 | ) 29 | op.create_table('messages', 30 | sa.Column('id', sa.Integer(), nullable=False), 31 | sa.Column('chat_id', sa.Integer(), nullable=False), 32 | sa.Column('user_id', sa.Integer(), nullable=False), 33 | sa.Column('text', sa.String(), nullable=True), 34 | sa.Column('image', sa.String(), nullable=True), 35 | sa.Column('created_at', sa.DateTime(), nullable=True), 36 | sa.Column('updated_at', sa.DateTime(), nullable=True), 37 | sa.ForeignKeyConstraint(['chat_id'], ['chats.id'], ), 38 | sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), 39 | sa.PrimaryKeyConstraint('id') 40 | ) 41 | # ### end Alembic commands ### 42 | 43 | 44 | def downgrade(): 45 | # ### commands auto generated by Alembic - please adjust! ### 46 | op.drop_table('messages') 47 | op.drop_table('chats') 48 | # ### end Alembic commands ### 49 | -------------------------------------------------------------------------------- /migrations/versions/5a9d2b2aa480_create_chat_permission_tables.py: -------------------------------------------------------------------------------- 1 | """create chat permission tables 2 | 3 | Revision ID: 5a9d2b2aa480 4 | Revises: 4acf590dd55e 5 | Create Date: 2018-10-16 17:01:55.809708 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.dialects import postgresql 11 | 12 | # revision identifiers, used by Alembic. 13 | from models.chat import sa_chat 14 | 15 | revision = '5a9d2b2aa480' 16 | down_revision = '014e2778ec23' 17 | branch_labels = None 18 | depends_on = None 19 | 20 | 21 | def upgrade(): 22 | # ### commands auto generated by Alembic - please adjust! ### 23 | op.create_table('chats_permission', 24 | sa.Column('id', sa.Integer(), nullable=False), 25 | sa.Column('chat_id', sa.Integer(), nullable=False), 26 | sa.Column('user_id', sa.Integer(), nullable=False), 27 | sa.Column('permission', postgresql.ENUM('admin', 'user', 'guest', 'removed', name='chats_permission_enum'), nullable=True), 28 | sa.Column('created_at', sa.DateTime(), nullable=True), 29 | sa.Column('updated_at', sa.DateTime(), nullable=True), 30 | sa.ForeignKeyConstraint(['chat_id'], ['chats.id'], ), 31 | sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), 32 | sa.PrimaryKeyConstraint('id') 33 | ) 34 | # ### end Alembic commands ### 35 | 36 | op.alter_column('chats_permission', 'user_id', 37 | existing_type=sa.INTEGER(), 38 | nullable=True) 39 | 40 | 41 | def downgrade(): 42 | # ### commands auto generated by Alembic - please adjust! ### 43 | op.drop_table('chats_permission') 44 | op.execute(''' 45 | drop type chats_permission_enum; 46 | ''') 47 | 48 | op.execute('TRUNCATE chats CASCADE ;') 49 | # ### end Alembic commands ### 50 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import socketio 4 | from config.jinja_init import jinja_init 5 | from aiohttp import web 6 | from config import setup_config 7 | from config.connect_redis import redis_connect 8 | from config.db import init_pg, close_pg 9 | from aiohttp_session import setup 10 | from helpers.log import create_loggers 11 | from socket_io.main import get_socket_io_route 12 | from middleware.errors import errors_middleware 13 | from middleware.police import police_middleware 14 | from routes import apply_routes 15 | from aiojobs.aiohttp import setup as setup_aiojobs 16 | 17 | 18 | async def dispose_redis_pool(app): 19 | redis_pool.close() 20 | await redis_pool.wait_closed() 21 | 22 | 23 | sio = socketio.AsyncServer(async_mode='aiohttp') 24 | app = web.Application() 25 | sio.attach(app) 26 | 27 | # Add config to app 28 | setup_config(app) 29 | 30 | # Add templates render 31 | jinja_init(app) 32 | 33 | if bool(os.getenv('DEBUG', False)) is True: 34 | logging.getLogger().setLevel(logging.INFO) 35 | logging.debug("Logging started") 36 | 37 | # Redis connect 38 | storage, redis_pool = redis_connect(app) 39 | setup(app, storage) 40 | 41 | # Create log 42 | create_loggers(app) 43 | 44 | # Add routes 45 | apply_routes(app) 46 | 47 | # before 48 | app.middlewares.append(police_middleware) 49 | 50 | # after 51 | app.middlewares.append(errors_middleware) 52 | 53 | sio, background_task = get_socket_io_route(sio, app) 54 | 55 | app.on_startup.append(init_pg) 56 | app.on_cleanup.append(close_pg) 57 | app.on_cleanup.append(dispose_redis_pool) 58 | sio.start_background_task(background_task) 59 | setup_aiojobs(app) 60 | 61 | if __name__ == '__main__': 62 | web.run_app(app, host=os.getenv('HOST', '0.0.0.0'), port=os.getenv('PORT', '8080')) 63 | -------------------------------------------------------------------------------- /middleware/police.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | 3 | import jwt 4 | from aiohttp import web 5 | from aiohttp.web_exceptions import HTTPException 6 | from jwt import InvalidSignatureError 7 | 8 | from models.user import User 9 | from models.role import Role 10 | from middleware.errors import CustomHTTPException 11 | from helpers.irc import irc 12 | 13 | 14 | @web.middleware 15 | async def police_middleware(request, handler): 16 | try: 17 | if 'api/user/login' in str(request.rel_url) or 'api/user/me' in str(request.rel_url): 18 | response = await handler(request) 19 | return response 20 | if request.rel_url.raw_parts[1] == "api": 21 | data = request.headers.get('Authorization') 22 | if not data: 23 | return CustomHTTPException(irc['ACCESS_DENIED'], 401) 24 | try: 25 | decode = jwt.decode(data[6:], request.app.config['secret'], algorithms=['HS256']) 26 | user = await User.get_user_by_id(decode['user']['id']) 27 | user['roles'] = [role['name'] for role in await Role.get_roles_by_id(user['id']) if role['name']] 28 | if decode['user']['email'] == user['email']: 29 | request.user = user 30 | response = await handler(request) 31 | return response 32 | except Exception as e: 33 | print(str(e)) 34 | return CustomHTTPException(irc['ACCESS_DENIED'], 401) 35 | response = await handler(request) 36 | except HTTPException as e: 37 | return e 38 | except CustomHTTPException as e: 39 | return e 40 | except Exception as e: 41 | request.app.loggers['rotating'].error(str(traceback.format_exc())) 42 | return CustomHTTPException() 43 | 44 | return response 45 | 46 | -------------------------------------------------------------------------------- /migrations/versions/2e8df848f6cf_create_tables.py: -------------------------------------------------------------------------------- 1 | """create tables 2 | 3 | Revision ID: 2e8df848f6cf 4 | Revises: 5 | Create Date: 2018-08-09 14:35:51.967465 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '2e8df848f6cf' 14 | down_revision = None 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('roles', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('name', sa.String(), nullable=False), 24 | sa.PrimaryKeyConstraint('id') 25 | ) 26 | op.create_table('users', 27 | sa.Column('id', sa.Integer(), nullable=False), 28 | sa.Column('email', sa.String(), nullable=False), 29 | sa.Column('password', sa.String(), nullable=False), 30 | sa.Column('name', sa.String(), nullable=True), 31 | sa.Column('image', sa.String(), nullable=True, default='/media/avatars/default.png'), 32 | sa.Column('created_at', sa.DateTime(), nullable=True), 33 | sa.Column('updated_at', sa.DateTime(), nullable=True), 34 | sa.PrimaryKeyConstraint('id'), 35 | sa.UniqueConstraint('email') 36 | ) 37 | op.create_table('user_groups', 38 | sa.Column('id', sa.Integer(), nullable=False), 39 | sa.Column('user_id', sa.Integer(), nullable=False), 40 | sa.Column('role_id', sa.Integer(), nullable=False), 41 | sa.ForeignKeyConstraint(['role_id'], ['roles.id'], ), 42 | sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), 43 | sa.PrimaryKeyConstraint('id') 44 | ) 45 | # ### end Alembic commands ### 46 | 47 | 48 | def downgrade(): 49 | # ### commands auto generated by Alembic - please adjust! ### 50 | op.drop_table('user_groups') 51 | op.drop_table('users') 52 | op.drop_table('roles') 53 | # ### end Alembic commands ### 54 | -------------------------------------------------------------------------------- /middleware/errors.py: -------------------------------------------------------------------------------- 1 | import json 2 | import traceback 3 | from asyncio import CancelledError 4 | from aiohttp.web_exceptions import HTTPException 5 | from aiohttp.web_response import Response 6 | from helpers.irc import irc 7 | 8 | 9 | class CustomHTTPException(Response, Exception): 10 | 11 | def __init__(self, body=irc['INTERNAL_SERVER_ERROR'], status=500): 12 | Response.__init__(self, 13 | status=status, 14 | headers=None, 15 | reason=None, 16 | text=None, 17 | content_type='application/json', 18 | body=json.dumps({"errors": body}), 19 | ) 20 | 21 | 22 | async def errors_middleware(app, handler): 23 | 24 | async def errors_middleware_handler(request): 25 | try: 26 | response = await handler(request) 27 | except IOError as e: 28 | request.app.loggers['rotating'].error(str(traceback.format_exc())) 29 | return e 30 | except CancelledError as e: 31 | request.app.loggers['rotating'].error(str(traceback.format_exc())) 32 | return e 33 | except KeyError as e: 34 | request.app.loggers['rotating'].error(str(traceback.format_exc())) 35 | return e 36 | except HTTPException as e: 37 | request.app.loggers['rotating'].error(str(traceback.format_exc())) 38 | return e 39 | except CustomHTTPException as e: 40 | pass 41 | except FileNotFoundError as e: 42 | request.app.loggers['rotating'].error(str(traceback.format_exc())) 43 | return CustomHTTPException(irc['NOT_FOUND'], 404) 44 | except Exception as e: 45 | request.app.loggers['rotating'].error(str(traceback.format_exc())) 46 | return CustomHTTPException() 47 | 48 | return response 49 | return errors_middleware_handler 50 | -------------------------------------------------------------------------------- /alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # path to migration scripts 5 | script_location = migrations 6 | 7 | # template used to generate migration files 8 | # file_template = %%(rev)s_%%(slug)s 9 | 10 | # timezone to use when rendering the date 11 | # within the migration file as well as the filename. 12 | # string value is passed to dateutil.tz.gettz() 13 | # leave blank for localtime 14 | # timezone = 15 | 16 | # max length of characters to apply to the 17 | # "slug" field 18 | #truncate_slug_length = 40 19 | 20 | # set to 'true' to run the environment during 21 | # the 'revision' command, regardless of autogenerate 22 | # revision_environment = false 23 | 24 | # set to 'true' to allow .pyc and .pyo files without 25 | # a source .py file to be detected as revisions in the 26 | # versions/ directory 27 | # sourceless = false 28 | 29 | # version location specification; this defaults 30 | # to migrations/versions. When using multiple version 31 | # directories, initial revisions must be specified with --version-path 32 | # version_locations = %(here)s/bar %(here)s/bat migrations/versions 33 | 34 | # the output encoding used when revision files 35 | # are written from script.py.mako 36 | # output_encoding = utf-8 37 | 38 | #sqlalchemy.url = postgresql://localhost/mydb 39 | #postgresql://postgres:postgres@localhost:5432/test 40 | 41 | 42 | # Logging configuration 43 | [loggers] 44 | keys = root,sqlalchemy,alembic 45 | 46 | [handlers] 47 | keys = console 48 | 49 | [formatters] 50 | keys = generic 51 | 52 | [logger_root] 53 | level = WARN 54 | handlers = console 55 | qualname = 56 | 57 | [logger_sqlalchemy] 58 | level = WARN 59 | handlers = 60 | qualname = sqlalchemy.engine 61 | 62 | [logger_alembic] 63 | level = INFO 64 | handlers = 65 | qualname = alembic 66 | 67 | [handler_console] 68 | class = StreamHandler 69 | args = (sys.stderr,) 70 | level = NOTSET 71 | formatter = generic 72 | 73 | [formatter_generic] 74 | format = %(levelname)-5.5s [%(name)s] %(message)s 75 | datefmt = %H:%M:%S 76 | -------------------------------------------------------------------------------- /create_admin.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | import aiopg.sa 4 | import bcrypt 5 | from sqlalchemy import text 6 | from app import app 7 | from models.user import sa_user 8 | from models.user_group import sa_user_group 9 | 10 | 11 | async def create_admin(): 12 | email = 'admin@example.com' 13 | password = 'qwerqwer' 14 | 15 | engine = await aiopg.sa.create_engine( 16 | database=os.getenv('POSTGRES_DB'), 17 | user=os.getenv('POSTGRES_USER'), 18 | password=os.getenv('POSTGRES_PASSWORD'), 19 | host=os.getenv('POSTGRES_HOST'), 20 | port=os.getenv('POSTGRES_PORT'), 21 | minsize=1, 22 | maxsize=5, 23 | loop=app.loop) 24 | data = { 25 | 'email': email, 26 | 'name': 'admin', 27 | 'image': '/media/avatars/default.png', 28 | 'password': bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8'), 29 | 'roles': ['admin'] 30 | } 31 | roles = data['roles'] 32 | del data['roles'] 33 | async with engine.acquire() as conn: 34 | query = sa_user.insert().values(data) 35 | user = list(map(lambda x: dict(x), await conn.execute(query))) 36 | new_user_id = user[0]['id'] 37 | 38 | for role in roles: 39 | query = text(""" 40 | SELECT 41 | roles.* 42 | FROM roles 43 | where 44 | roles.name = :role_name 45 | ; 46 | """) 47 | query_result = await conn.execute(query, role_name=role) 48 | found_role = await query_result.fetchone() 49 | query = sa_user_group.insert().values({'user_id': new_user_id, 'role_id': dict(found_role)['id']}) 50 | await conn.execute(query) 51 | return new_user_id 52 | 53 | 54 | if __name__ == '__main__': 55 | loop = asyncio.get_event_loop() 56 | loop.run_until_complete(create_admin()) 57 | loop.close() 58 | -------------------------------------------------------------------------------- /public/static/js/fb.js: -------------------------------------------------------------------------------- 1 | // This is called with the results from from FB.getLoginStatus(). 2 | function statusChangeCallback(response) { 3 | if (response.status === 'connected') { 4 | $.ajax({ 5 | url: "/api/user/login/facebook", 6 | type: "POST", 7 | data: JSON.stringify({ 8 | "status": response.status, 9 | "token": response.authResponse.accessToken 10 | }), 11 | contentType: "application/json; charset=utf-8", 12 | dataType: "json", 13 | success: function (data) { 14 | localStorage.setItem('user', JSON.stringify(data.user)); 15 | localStorage.setItem('token', data.token); 16 | localStorage.setItem('roles', JSON.stringify(data.roles)); 17 | document.location.href = URL_REDIRECT_AFTER_LOGIN 18 | }, 19 | error: function () { 20 | $(".alert-facebook-warning").addClass('show').alert(); 21 | } 22 | }); 23 | } 24 | } 25 | 26 | function checkLoginState() { 27 | FB.init({ 28 | appId: FACEBOOK_ID, 29 | cookie: true, // enable cookies to allow the server to access 30 | // the session 31 | xfbml: true, // parse social plugins on this page 32 | version: 'v2.8' // use graph api version 2.8 33 | }); 34 | 35 | FB.getLoginStatus(function (response) { 36 | statusChangeCallback(response); 37 | }); 38 | } 39 | 40 | window.fbAsyncInit = function () { 41 | if (FACEBOOK_ID !== "None") { 42 | FB.init({ 43 | appId: FACEBOOK_ID, 44 | cookie: true, // enable cookies to allow the server to access 45 | // the session 46 | xfbml: true, // parse social plugins on this page 47 | version: 'v2.8' // use graph api version 2.8 48 | }); 49 | } 50 | 51 | }; 52 | 53 | // Load the SDK asynchronously 54 | (function (d, s, id) { 55 | var js, fjs = d.getElementsByTagName(s)[0]; 56 | if (d.getElementById(id)) return; 57 | js = d.createElement(s); 58 | js.id = id; 59 | js.src = "https://connect.facebook.net/en_US/sdk.js"; 60 | fjs.parentNode.insertBefore(js, fjs); 61 | }(document, 'script', 'facebook-jssdk')); 62 | -------------------------------------------------------------------------------- /front/dist/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /aiohttp_sqlalchemy_alembic.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "a8ece4ef-0874-58c4-fe7f-f2b322b2365f", 4 | "name": "aiohttp_sqlalchemy_alembic", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" 6 | }, 7 | "item": [ 8 | { 9 | "name": "/api/user/login", 10 | "request": { 11 | "method": "POST", 12 | "header": [ 13 | { 14 | "key": "Content-Type", 15 | "value": "application/json" 16 | } 17 | ], 18 | "body": { 19 | "mode": "raw", 20 | "raw": "{\n \"email\": \"admin@example.com\",\n \"password\": \"qwerqwer\"\n}" 21 | }, 22 | "url": { 23 | "raw": "{{server}}/api/user/login", 24 | "host": [ 25 | "{{server}}" 26 | ], 27 | "path": [ 28 | "api", 29 | "user", 30 | "login" 31 | ] 32 | } 33 | }, 34 | "response": [] 35 | }, 36 | { 37 | "name": "/api/user/login/google", 38 | "request": { 39 | "method": "POST", 40 | "header": [ 41 | { 42 | "key": "Content-Type", 43 | "value": "application/json" 44 | } 45 | ], 46 | "body": { 47 | "mode": "raw", 48 | "raw": "{\n \"token\": \"token\"\n}" 49 | }, 50 | "url": { 51 | "raw": "{{server}}/api/user/login/google", 52 | "host": [ 53 | "{{server}}" 54 | ], 55 | "path": [ 56 | "api", 57 | "user", 58 | "login", 59 | "google" 60 | ] 61 | } 62 | }, 63 | "response": [] 64 | }, 65 | { 66 | "name": "/api/user/login/facebook", 67 | "request": { 68 | "method": "POST", 69 | "header": [ 70 | { 71 | "key": "Content-Type", 72 | "value": "application/json" 73 | } 74 | ], 75 | "body": { 76 | "mode": "raw", 77 | "raw": "{\n \"token\": \"token\",\n \"status\": \"status\"\n}" 78 | }, 79 | "url": { 80 | "raw": "{{server}}/api/user/login/facebook", 81 | "host": [ 82 | "{{server}}" 83 | ], 84 | "path": [ 85 | "api", 86 | "user", 87 | "login", 88 | "facebook" 89 | ] 90 | } 91 | }, 92 | "response": [] 93 | }, 94 | { 95 | "name": "/api/user CREATE_USER", 96 | "request": { 97 | "method": "POST", 98 | "header": [ 99 | { 100 | "key": "Content-Type", 101 | "value": "application/json" 102 | }, 103 | { 104 | "key": "Authorization", 105 | "value": "Token {{token}}" 106 | } 107 | ], 108 | "body": { 109 | "mode": "raw", 110 | "raw": "{\n \"email\": \"god0@gmail.com\",\n \"password\": \"qwerqwer\",\n \"roles\": [\"user\", \"admin\"]\n} " 111 | }, 112 | "url": { 113 | "raw": "{{server}}/api/user", 114 | "host": [ 115 | "{{server}}" 116 | ], 117 | "path": [ 118 | "api", 119 | "user" 120 | ] 121 | } 122 | }, 123 | "response": [] 124 | } 125 | ] 126 | } -------------------------------------------------------------------------------- /socket_io/main.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import jwt 3 | from config.config import config 4 | from models.user import User 5 | from socket_io.helper import get_and_send_participated_by_user_id, send_messages_by_chat_name 6 | from socket_io.routes.chat import get_chat_routes 7 | from socket_io.config import ROUTES, users_socket, users_by_user_id 8 | from socket_io.routes.user import get_user_routes 9 | 10 | logger = logging.getLogger('Rotating Log') 11 | 12 | 13 | def get_socket_io_route(sio, app): 14 | 15 | get_user_routes(sio, app) 16 | 17 | get_chat_routes(sio, app) 18 | 19 | 20 | @sio.on(ROUTES['BACK']['CONNECT']) 21 | async def connect(sid, environ): 22 | token = environ.get('HTTP_AUTHORIZATION') 23 | 24 | try: 25 | decode = jwt.decode(token, config['secret'], algorithms=['HS256']) 26 | except jwt.DecodeError: 27 | return await sio.disconnect(sid) 28 | except Exception as e: 29 | return await sio.disconnect(sid) 30 | 31 | decode['user']['roles'] = decode['roles'] 32 | users_socket[sid] = decode['user'] 33 | users_socket[sid]['sid'] = sid 34 | if decode['user']['id'] not in users_by_user_id: 35 | users_by_user_id[decode['user']['id']] = list() 36 | users_by_user_id[decode['user']['id']].append(sid) 37 | 38 | await sio.emit(ROUTES['FRONT']['AUTH'], {'data': decode['user']}, room=sid) 39 | 40 | await sio.emit(ROUTES['FRONT']['USER']['ALL'], { 41 | 'data': await User.get_users_without_self(users_socket[sid]['id']) 42 | }, namespace='/') 43 | 44 | participated = await get_and_send_participated_by_user_id(sio, int(users_socket[sid]['id']), sid) 45 | 46 | if len(participated) == 0: 47 | participated = None 48 | 49 | await send_messages_by_chat_name(sio, sid, participated[0] if participated else None) 50 | 51 | @sio.on(ROUTES['BACK']['DISCONNECT']) 52 | async def disconnect(sid): 53 | 54 | if sid in users_socket: 55 | user = users_socket[sid] 56 | del users_by_user_id[user["id"]] 57 | sids_fir_remove = list() 58 | try: 59 | for user_data in users_socket: 60 | if users_socket[user_data]['id'] == user['id']: 61 | sids_fir_remove.append(user_data) 62 | 63 | for sid in sids_fir_remove: 64 | del users_socket[sid] 65 | del sio.environ[sid] 66 | 67 | if user: 68 | await sio.emit(ROUTES['FRONT']['USER']['ONLINE'], { 69 | 'data': [users_by_user_id[user] for user in users_by_user_id] 70 | }, namespace='/') 71 | except: 72 | pass 73 | 74 | return await sio.disconnect(sid) 75 | 76 | async def background_task(): 77 | 78 | await sio.sleep(5) 79 | 80 | while True: 81 | try: 82 | await sio.sleep(5) 83 | except Exception as e: 84 | logger.exception('') 85 | 86 | return sio, background_task 87 | -------------------------------------------------------------------------------- /migrations/env.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | import os, sys 3 | import pathlib 4 | sys.path.append(os.getcwd()) 5 | from sqlalchemy import engine_from_config, pool, MetaData, Table 6 | from alembic import context 7 | from logging.config import fileConfig 8 | config = context.config 9 | fileConfig(config.config_file_name) 10 | 11 | 12 | def combine_metadata(*args): 13 | m = MetaData() 14 | for metadata_temp in args: 15 | for metadata in metadata_temp: 16 | for t in metadata.tables.values(): 17 | t.tometadata(m) 18 | return m 19 | 20 | 21 | meta_list = list() 22 | 23 | for file in [file for file in os.listdir(str(pathlib.Path(__file__).parent.parent) + "/models/") if file != '__pycache__' and file != '__init__.py']: 24 | p, m = file.rsplit('.', 1) 25 | module_in_file = __import__("models." + str(p)) 26 | files_module_in_directory = getattr(module_in_file, p) 27 | 28 | new_model = [] 29 | for item in files_module_in_directory.__dict__: 30 | try: 31 | files_module = getattr(files_module_in_directory, item) 32 | if isinstance(files_module, Table) is True: 33 | meta_list.append(files_module.metadata) 34 | except Exception as e: 35 | print(e) 36 | 37 | target_metadata = combine_metadata(meta_list) 38 | 39 | # other values from the config, defined by the needs of env.py, 40 | # can be acquired: 41 | # my_important_option = config.get_main_option("my_important_option") 42 | # ... etc. 43 | 44 | 45 | def run_migrations_offline(): 46 | """Run migrations in 'offline' mode. 47 | 48 | This configures the context with just a URL 49 | and not an Engine, though an Engine is acceptable 50 | here as well. By skipping the Engine creation 51 | we don't even need a DBAPI to be available. 52 | 53 | Calls to context.execute() here emit the given string to the 54 | script output. 55 | 56 | """ 57 | url = f"{os.getenv('POSTGRES_TYPE')}://{os.getenv('POSTGRES_USER')}:{os.getenv('POSTGRES_PASSWORD')}@" \ 58 | f"{os.getenv('POSTGRES_HOST')}:{os.getenv('POSTGRES_PORT')}/{os.getenv('POSTGRES_DB')}" 59 | 60 | 61 | context.configure( 62 | url=url, target_metadata=target_metadata, literal_binds=True) 63 | 64 | with context.begin_transaction(): 65 | context.run_migrations() 66 | 67 | 68 | def run_migrations_online(): 69 | """Run migrations in 'online' mode. 70 | 71 | In this scenario we need to create an Engine 72 | and associate a connection with the context. 73 | 74 | """ 75 | url = f"{os.getenv('POSTGRES_TYPE')}://{os.getenv('POSTGRES_USER')}:{os.getenv('POSTGRES_PASSWORD')}@" \ 76 | f"{os.getenv('POSTGRES_HOST')}:{os.getenv('POSTGRES_PORT')}/{os.getenv('POSTGRES_DB')}" 77 | 78 | config_dict = dict() 79 | config_dict['sqlalchemy.url'] = url 80 | 81 | connectable = engine_from_config( 82 | config_dict, 83 | prefix='sqlalchemy.', 84 | poolclass=pool.NullPool) 85 | 86 | with connectable.connect() as connection: 87 | context.configure( 88 | connection=connection, 89 | target_metadata=target_metadata 90 | ) 91 | 92 | with context.begin_transaction(): 93 | context.run_migrations() 94 | 95 | 96 | if context.is_offline_mode(): 97 | run_migrations_offline() 98 | else: 99 | run_migrations_online() 100 | -------------------------------------------------------------------------------- /models/message.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, DateTime, Integer, String, func, ForeignKey, desc, asc, literal_column 2 | from sqlalchemy.ext.declarative import declarative_base 3 | import sqlalchemy as sa 4 | from sqlalchemy.sql import label 5 | 6 | from config import config 7 | from helpers.db_helper import as_dict, raise_db_exception 8 | from helpers.irc import irc 9 | from middleware.errors import CustomHTTPException 10 | from models.chat import sa_chat 11 | from models.user import sa_user 12 | 13 | Base = declarative_base() 14 | 15 | 16 | class Message(Base): 17 | __tablename__ = 'messages' 18 | id = Column(Integer, primary_key=True, nullable=False) 19 | chat_id = Column(Integer, ForeignKey('chats.id'), nullable=False) 20 | user_id = Column(Integer, ForeignKey('users.id'), nullable=False) 21 | text = Column(String, nullable=True) 22 | image = Column(String, nullable=True) 23 | created_at = Column(DateTime, default=func.now()) 24 | updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) 25 | 26 | @staticmethod 27 | async def get_messages_by_chat_id(chat_id: int) -> list: 28 | async with config['db'].acquire() as conn: 29 | query = sa.select([sa_message.c.id, 30 | sa_message.c.user_id, 31 | sa_message.c.text, 32 | label('message_text', sa_message.c.text), 33 | label('message_image', sa_message.c.image), 34 | sa_message.c.created_at, 35 | label('user_name', (sa_user.c.name)), 36 | sa_user.c.email, 37 | label('user_image', sa_user.c.image) 38 | ]) \ 39 | .select_from( 40 | sa_message 41 | .join(sa_chat, sa_message.c.chat_id == sa_chat.c.id, isouter=True) 42 | .join(sa_user, sa_message.c.user_id == sa_user.c.id, isouter=True) 43 | ) \ 44 | .where(sa_chat.c.id == chat_id) \ 45 | .order_by(asc(sa_message.c.id)) 46 | return list(map(lambda x: as_dict(dict(x)), await conn.execute(query))) 47 | 48 | @staticmethod 49 | async def new_messages_by_chat_id(message: str, chat_id: int, user_id: int) -> dict: 50 | try: 51 | query = sa_message.insert(inline=True) 52 | query = query.values([{ 53 | 'chat_id': chat_id, 54 | 'text':message, 55 | 'user_id': user_id 56 | }]).returning(literal_column('*')) 57 | async with config['db'].acquire() as conn: 58 | new_message = [as_dict(dict(message)) 59 | for message in (await (await conn.execute(query)).fetchall())] 60 | 61 | if not new_message: 62 | raise CustomHTTPException(irc['INTERNAL_SERVER_ERROR'], 500) 63 | 64 | query = sa.select([sa_message.c.id, 65 | sa_message.c.user_id, 66 | sa_message.c.text, 67 | label('message_text', sa_message.c.text), 68 | label('message_image', sa_message.c.image), 69 | sa_message.c.created_at, 70 | label('user_name', (sa_user.c.name)), 71 | sa_user.c.email, 72 | label('user_image', sa_user.c.image) 73 | ]) \ 74 | .select_from( 75 | sa_message 76 | .join(sa_chat, sa_message.c.chat_id == sa_chat.c.id) 77 | .join(sa_user, sa_message.c.user_id == sa_user.c.id) 78 | ) \ 79 | .where(sa_message.c.id == new_message[0]['id']) 80 | 81 | new_message_with_params = list(map(lambda x: as_dict(dict(x)), await conn.execute(query))) 82 | 83 | if len(new_message_with_params) != 0: 84 | return new_message_with_params[0] 85 | except Exception as e: 86 | raise await raise_db_exception(e) 87 | 88 | 89 | sa_message = Message.__table__ 90 | -------------------------------------------------------------------------------- /models/user.py: -------------------------------------------------------------------------------- 1 | import sqlalchemy as sa 2 | from sqlalchemy import Column, DateTime, Integer, String, func 3 | from sqlalchemy.ext.declarative import declarative_base 4 | from config import config 5 | from helpers.irc import irc 6 | from middleware.errors import CustomHTTPException 7 | from models.role import Role 8 | from models.user_group import UserGroup 9 | Base = declarative_base() 10 | 11 | 12 | class User(Base): 13 | __tablename__ = 'users' 14 | id = Column(Integer, primary_key=True, nullable=False) 15 | email = Column(String, nullable=True, unique=True) 16 | password = Column(String, nullable=False) 17 | name = Column(String, nullable=True) 18 | image = Column(String, nullable=True, default='/media/avatars/default.png') 19 | facebook_id = Column(String, nullable=True) 20 | google_id = Column(String, nullable=True) 21 | created_at = Column(DateTime, default=func.now()) 22 | updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) 23 | 24 | @staticmethod 25 | async def get_user_by_facebook_id(facebook_id: str) -> list: 26 | async with config['db'].acquire() as conn: 27 | query = sa.select([sa_user.c.id, 28 | sa_user.c.email, 29 | sa_user.c.password, 30 | sa_user.c.name, 31 | sa_user.c.image 32 | ]) \ 33 | .select_from(sa_user) \ 34 | .where(sa_user.c.facebook_id == facebook_id) 35 | users = list(map(lambda x: dict(x), await conn.execute(query))) 36 | return users[0] if len(users) == 1 else None 37 | 38 | @staticmethod 39 | async def get_user_by_google_id(google_id: str) -> list: 40 | async with config['db'].acquire() as conn: 41 | query = sa.select([sa_user.c.id, 42 | sa_user.c.email, 43 | sa_user.c.password, 44 | sa_user.c.name, 45 | sa_user.c.image 46 | ]) \ 47 | .select_from(sa_user) \ 48 | .where(sa_user.c.google_id == google_id) 49 | users = list(map(lambda x: dict(x), await conn.execute(query))) 50 | return users[0] if len(users) == 1 else None 51 | 52 | @staticmethod 53 | async def get_user_by_email(email: str) -> list: 54 | async with config['db'].acquire() as conn: 55 | query = sa.select([sa_user.c.id, 56 | sa_user.c.email, 57 | sa_user.c.password, 58 | sa_user.c.name, 59 | sa_user.c.image 60 | ]) \ 61 | .select_from(sa_user) \ 62 | .where(sa_user.c.email == email) 63 | return list(map(lambda x: dict(x), await conn.execute(query))) 64 | 65 | @staticmethod 66 | async def get_user_by_id(user_id: int): 67 | async with config['db'].acquire() as conn: 68 | query = sa.select([sa_user.c.id, 69 | sa_user.c.email, 70 | sa_user.c.password, 71 | sa_user.c.name, 72 | sa_user.c.image 73 | ]) \ 74 | .select_from(sa_user) \ 75 | .where(sa_user.c.id == user_id) 76 | users = list(map(lambda x: dict(x), await conn.execute(query))) 77 | return users[0] if len(users) == 1 else None 78 | 79 | @staticmethod 80 | async def create_user(data: dict) -> int: 81 | if 'roles' not in data: 82 | raise CustomHTTPException(irc['ACCESS_DENIED'], 401) 83 | roles = data['roles'] 84 | del data['roles'] 85 | async with config['db'].acquire() as conn: 86 | query = sa_user.insert().values(data) 87 | user = list(map(lambda x: dict(x), await conn.execute(query))) 88 | if len(user) != 1: 89 | raise CustomHTTPException(irc['INTERNAL_SERVER_ERROR'], 500) 90 | new_user_id = user[0]['id'] 91 | 92 | for role in roles: 93 | found_role = await Role.get_role_by_name(role) 94 | if not found_role: 95 | raise CustomHTTPException(irc['ROLE_NOT_FOUND'], 404) 96 | 97 | await UserGroup.add_role_to_user(new_user_id, found_role['id']) 98 | return new_user_id 99 | 100 | @classmethod 101 | async def get_users_without_self(cls, id: int) -> list: 102 | async with config['db'].acquire() as conn: 103 | query = sa.select([sa_user.c.id, 104 | sa_user.c.email, 105 | sa_user.c.password, 106 | sa_user.c.name, 107 | sa_user.c.image 108 | ]) \ 109 | .select_from(sa_user) \ 110 | .where(sa_user.c.id != id) \ 111 | .order_by(sa_user.c.name) 112 | return list(map(lambda x: dict(x), await conn.execute(query))) 113 | 114 | @classmethod 115 | async def get_users_by_id_list(cls, ids: list) -> list: 116 | async with config['db'].acquire() as conn: 117 | query = sa.select([sa_user.c.id, 118 | sa_user.c.email, 119 | sa_user.c.name, 120 | sa_user.c.image 121 | ]) \ 122 | .select_from(sa_user) \ 123 | .where(sa_user.c.id.in_(ids)) 124 | return list(map(lambda x: dict(x), await conn.execute(query))) 125 | 126 | 127 | sa_user = User.__table__ 128 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 12 | 13 | Login 14 | 19 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
30 |
31 |
32 | 33 |
34 |
35 |

Login

36 |
37 |
38 |
40 |
41 | 42 | 44 |
Oops, you missed this one.
45 |
46 |
47 | 48 | 50 |
Enter your password too!
51 |
52 |
53 |
54 | 59 |
60 | 65 |
66 |
67 | 68 |
69 | 70 | 71 |
72 | 73 | 74 |
75 | 76 | 77 |
78 | 79 | 80 | 81 |
82 |
83 | 89 | 90 | 96 | 97 | 103 |
104 | 105 | 106 |
107 | 108 | 109 | 112 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /socket_io/routes/chat.py: -------------------------------------------------------------------------------- 1 | from config import config 2 | from models.chat import Chat 3 | from models.message import Message 4 | from models.chat_permission import ChatPermission 5 | from models.user import User 6 | from socket_io.helper import send_participated_by_user_id_and_send_messages 7 | from socket_io.config import ROUTES, users_socket, users_by_user_id 8 | 9 | 10 | def get_chat_routes(sio, app): 11 | 12 | """ 13 | Создать чат с другим пользователь 14 | :return: 15 | - отправить список пользователей, не включая себя; 16 | - отправить список с чатами. 17 | """ 18 | @sio.on(ROUTES['BACK']['CHAT']['CREATE']) 19 | async def chat_invite(sid, data): 20 | print(ROUTES['BACK']['CHAT']['CREATE']) 21 | print(data) 22 | users_id = data['id'] 23 | users_id.append(users_socket[sid]['id']) 24 | participated_chat_id = await ChatPermission.get_chat_id_by_user_id_list(users_id) 25 | 26 | if participated_chat_id: 27 | # Отдаем список с активным этим чатом и историей сообщений 28 | # так как чат уже существует 29 | return await send_participated_by_user_id_and_send_messages(sio, sid, participated_chat_id) 30 | 31 | # создаем чат и отдаем список с активным этим чатом 32 | users = await User.get_users_by_id_list(users_id) 33 | default_image = False if len(users) == 2 else True 34 | 35 | chat_permission_bulk = [] 36 | chat_name = '' 37 | user_admin = users_id[-1] 38 | 39 | for user in users: 40 | chat_name += f" {user['name'] if 'name' in user else user['email']}" 41 | 42 | async with config['db'].acquire() as connection: 43 | try: 44 | trans = await connection.begin() 45 | chat = await Chat.create_new_chat_by_name(connection) 46 | if default_image is True: 47 | for user in users: 48 | permission_for_insert = { 49 | "chat_id": chat['id'], 50 | "user_id": user['id'], 51 | "chat_name": chat_name, 52 | "permission": "user" 53 | } 54 | 55 | if user['id'] == user_admin: 56 | permission_for_insert['permission'] = 'admin' 57 | 58 | chat_permission_bulk.append(permission_for_insert) 59 | else: 60 | chat_permission_bulk.append({ 61 | "chat_id": chat['id'], 62 | "user_id": users[0]['id'], 63 | "permission": "admin" if users[0]['id'] == user_admin else "user", 64 | "chat_image": users[1]['image'], 65 | "chat_name": users[1]['name'] if 'name' in users[1] else users[1]['email'] 66 | }) 67 | chat_permission_bulk.append({ 68 | "chat_id": chat['id'], 69 | "user_id": users[1]['id'], 70 | "permission": "admin" if users[1]['id'] == user_admin else "user", 71 | "chat_image": users[0]['image'], 72 | "chat_name": users[0]['name'] if 'name' in users[0] else users[0]['email'] 73 | }) 74 | 75 | new_chat_permissions = await ChatPermission.create_chat_permission_bulk(chat_permission_bulk, connection) 76 | 77 | await trans.commit() 78 | 79 | for new_chat_permission in new_chat_permissions: 80 | if new_chat_permission['user_id'] in users_by_user_id: 81 | for online_user_sid in users_by_user_id[new_chat_permission['user_id']]: 82 | await send_participated_by_user_id_and_send_messages(sio, online_user_sid, int(new_chat_permission['chat_id'])) 83 | 84 | except Exception as e: 85 | await trans.rollback() 86 | raise e 87 | 88 | @sio.on(ROUTES['BACK']['CHAT']['REMOVE']) 89 | async def chat_remove(sid): 90 | print(ROUTES['BACK']['CHAT']['REMOVE']) 91 | 92 | @sio.on(ROUTES['BACK']['CHAT']['INVITE']) 93 | async def chat_invite(sid): 94 | print(ROUTES['BACK']['CHAT']['INVITE']) 95 | 96 | """ 97 | Переключение на другой чат 98 | """ 99 | @sio.on(ROUTES['BACK']['CHAT']['CHANGE']) 100 | async def chat_change(sid, data): 101 | print(ROUTES['BACK']['CHAT']['CHANGE']) 102 | return await send_participated_by_user_id_and_send_messages(sio, sid, int(data['id'])) 103 | 104 | #MESSAGE 105 | @sio.on(ROUTES['BACK']['CHAT']['MESSAGE']['NEW']) 106 | async def chat_message_new(sid, data): 107 | user = users_socket[sid] 108 | if not user: 109 | return 110 | 111 | permission = await ChatPermission.get_participated_by_user_id_and_chat_id(data['activeChat']['chat_id'], user['id']) 112 | if not permission or len(permission) == 0: 113 | return 114 | 115 | permission = permission[0] 116 | new_message = await Message.new_messages_by_chat_id(data['message'], permission['chat_id'], user['id']) 117 | 118 | # TODO сделать отправку всем участникам 119 | await sio.emit(ROUTES['FRONT']['CHAT']['MESSAGE']['NEW'], { 120 | 'new_message': new_message, 121 | 'chat': data['activeChat'] 122 | }, namespace='/') 123 | print(ROUTES['BACK']['CHAT']['MESSAGE']['NEW']) 124 | 125 | @sio.on(ROUTES['BACK']['CHAT']['MESSAGE']['EDIT']) 126 | async def chat_message_edit(sid): 127 | print(ROUTES['BACK']['CHAT']['MESSAGE']['EDIT']) 128 | 129 | @sio.on(ROUTES['BACK']['CHAT']['MESSAGE']['REMOVE']) 130 | async def chat_message_remove(sid): 131 | print(ROUTES['BACK']['CHAT']['MESSAGE']['REMOVE']) 132 | -------------------------------------------------------------------------------- /models/chat_permission.py: -------------------------------------------------------------------------------- 1 | import sqlalchemy as sa 2 | from aiopg.sa import SAConnection 3 | from sqlalchemy import Column, DateTime, Integer, func, ForeignKey, desc, or_, Text, and_, asc, literal_column, String, \ 4 | text 5 | from sqlalchemy.dialects.postgresql import ENUM 6 | from sqlalchemy.ext.declarative import declarative_base 7 | from sqlalchemy.sql import label 8 | from config import config 9 | from helpers.db_helper import as_dict, raise_db_exception 10 | from helpers.irc import irc 11 | from middleware.errors import CustomHTTPException 12 | from models.chat import sa_chat 13 | from models.message import sa_message 14 | 15 | Base = declarative_base() 16 | 17 | 18 | class ChatPermission(Base): 19 | __tablename__ = 'chats_permission' 20 | id = Column(Integer, primary_key=True, nullable=False) 21 | chat_id = Column(Integer, ForeignKey('chats.id'), nullable=False) 22 | user_id = Column(Integer, ForeignKey('users.id'), nullable=True) 23 | permission = Column('permission', ENUM('admin', 'user', 'guest', 'removed', name='chats_permission_enum')) 24 | chat_image = Column(Text, default='/media/avatars/default_group.png') 25 | chat_name = Column(String, nullable=True) 26 | created_at = Column(DateTime, default=func.now()) 27 | updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) 28 | 29 | @staticmethod 30 | async def get_participated_by_user_id(user_id: int) -> list: 31 | async with config['db'].acquire() as conn: 32 | message_id = sa.select([sa_message.c.id]) \ 33 | .select_from(sa_message) \ 34 | .where(sa_chat.c.id == sa_message.c.chat_id) \ 35 | .order_by(desc(sa_message.c.id)) \ 36 | .limit(1) \ 37 | .as_scalar() 38 | 39 | query = sa.select([sa_chat_permission.c.id.label('chat_permission_id'), 40 | sa_chat_permission.c.permission, 41 | sa_chat_permission.c.chat_name, 42 | sa_chat_permission.c.chat_image, 43 | sa_chat.c.id.label('chat_id'), 44 | sa_chat.c.created_at, 45 | message_id.label('message_id') 46 | 47 | ]) \ 48 | .select_from( 49 | sa_chat 50 | .join(sa_chat_permission, sa_chat_permission.c.chat_id == sa_chat.c.id, isouter=True) 51 | ) \ 52 | .where(or_(sa_chat_permission.c.user_id == user_id, sa_chat_permission.c.user_id == None)) \ 53 | .order_by(asc('message_id')) 54 | 55 | return list(map(lambda x: as_dict(dict(x)), await conn.execute(query))) 56 | 57 | @staticmethod 58 | async def get_participated_by_user_id_and_chat_id(chat_id: int, user_id: int) -> list: 59 | async with config['db'].acquire() as conn: 60 | query = sa.select([sa_chat_permission.c.id.label('chat_permission_id'), 61 | sa_chat_permission.c.permission, 62 | sa_chat_permission.c.chat_image, 63 | sa_chat_permission.c.chat_name, 64 | sa_chat.c.id.label('chat_id'), 65 | sa_chat.c.created_at, 66 | ]) \ 67 | .select_from( 68 | sa_chat 69 | .join(sa_chat_permission, sa_chat_permission.c.chat_id == sa_chat.c.id, isouter=True) 70 | ) \ 71 | .where( 72 | and_( 73 | or_(sa_chat_permission.c.user_id == user_id, sa_chat_permission.c.user_id == None), 74 | sa_chat_permission.c.chat_id == chat_id 75 | ) 76 | ) 77 | 78 | return list(map(lambda x: as_dict(dict(x)), await conn.execute(query))) 79 | 80 | # @staticmethod 81 | # async def get_chat_id_by_user_id_list(users_id: list) -> int or None: 82 | # async with config['db'].acquire() as conn: 83 | # query = sa.select([sa_chat_permission.c.chat_id]) \ 84 | # .select_from(sa_chat_permission) \ 85 | # .where(sa_chat_permission.c.user_id.in_(users_id)) \ 86 | # .group_by(sa_chat_permission.c.chat_id) \ 87 | # .having(func.count(sa_chat_permission.c.chat_id) == len(users_id)) 88 | # 89 | # users_participated = list(map(lambda x: as_dict(dict(x)), await conn.execute(query))) 90 | # return users_participated[0]['chat_id'] if len(users_participated) > 0 else None 91 | 92 | @staticmethod 93 | async def get_chat_id_by_user_id_list(users_id: list) -> list: 94 | async with config['db'].acquire() as conn: 95 | query = text(""" 96 | SELECT chat_id 97 | from (SELECT 98 | cp.chat_id, 99 | count(cp.chat_id) as found_members, 100 | (select count(cps.id) as all_members 101 | from chats_permission cps 102 | where cp.chat_id = cps.chat_id 103 | ) as all_members 104 | FROM chats 105 | left join chats_permission cp on chats.id = cp.chat_id 106 | where cp.user_id IN :users_id 107 | group by cp.chat_id) as list 108 | where list.found_members = :users_count 109 | and list.all_members = :users_count; 110 | """) 111 | users_participated = list(map(lambda x: as_dict(dict(x)), await conn.execute(query, 112 | users_id=tuple(users_id), 113 | users_count=len(users_id)))) 114 | return users_participated[0]['chat_id'] if len(users_participated) == 1 else None 115 | 116 | @staticmethod 117 | async def create_chat_permission_bulk(chat_permissions: list, connect: SAConnection) -> dict or None: 118 | try: 119 | query = sa_chat_permission.insert(inline=True) 120 | query = query.values(chat_permissions).returning(literal_column('*')) 121 | new_permissions = [as_dict(dict(chat_permission)) 122 | for chat_permission in (await (await connect.execute(query)).fetchall())] 123 | 124 | if not new_permissions: 125 | raise CustomHTTPException(irc['INTERNAL_SERVER_ERROR'], 500) 126 | 127 | return new_permissions 128 | except Exception as e: 129 | raise await raise_db_exception(e) 130 | 131 | @staticmethod 132 | async def get_last_participated_by_user_id(user_id: int) -> dict or None: 133 | async with config['db'].acquire() as conn: 134 | query = sa.select([sa_chat_permission.c.permission, 135 | label('chat_id', sa_chat.c.id), 136 | sa_chat.c.created_at 137 | ]) \ 138 | .select_from( 139 | sa_chat_permission 140 | .join(sa_chat, sa_chat_permission.c.chat_id == sa_chat.c.id, isouter=True) 141 | .join(sa_message, sa_chat.c.id == sa_message.c.chat_id, isouter=True) 142 | ) \ 143 | .where(sa_chat_permission.c.user_id == user_id) \ 144 | .order_by(desc(sa_message.c.id)) \ 145 | .limit(1) 146 | 147 | result = list(map(lambda x: as_dict(dict(x)), await conn.execute(query))) 148 | 149 | if len(result) != 0: 150 | return result[0] 151 | 152 | return None 153 | 154 | 155 | sa_chat_permission = ChatPermission.__table__ 156 | -------------------------------------------------------------------------------- /routes/user.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | import aiohttp 3 | import bcrypt 4 | import jwt 5 | from aiohttp.web import json_response 6 | from jwt import InvalidSignatureError 7 | 8 | from helpers.acl import acl 9 | from helpers.irc import irc 10 | from helpers.validator_schema import validate 11 | from middleware.errors import CustomHTTPException 12 | from models.user import User 13 | from models.role import Role 14 | 15 | 16 | def init(app): 17 | prefix = '/api/user' 18 | app.router.add_get(prefix + '/me', me) 19 | app.router.add_post(prefix + '/login', login) 20 | app.router.add_post(prefix + '', create_user) 21 | app.router.add_post(prefix + '/login/facebook', user_facebook_login) 22 | app.router.add_post(prefix + '/login/google', user_google_login) 23 | 24 | 25 | async def me(request): 26 | token = request.query.get('token', None) 27 | schema = {'token': {'required': True, "type": 'string'}} 28 | await validate({"token": token}, schema) 29 | 30 | try: 31 | decode = jwt.decode(token, request.app.config['secret'], algorithms=['HS256']) 32 | except InvalidSignatureError: 33 | return CustomHTTPException(irc['ACCESS_DENIED'], 401) 34 | user = await User.get_user_by_id(decode['user']['id']) 35 | roles = [role['name'] for role in await Role.get_roles_by_id(user['id']) if role['name']] 36 | del user['password'] 37 | encoded = jwt.encode({'user': user, 'roles': roles}, request.app.config['secret'], 38 | algorithm='HS256').decode('utf-8') 39 | return json_response({'token': encoded, 'user': user, 'roles': roles}) 40 | 41 | 42 | @acl(['admin']) 43 | async def create_user(request): 44 | data = await request.json() 45 | 46 | schema = { 47 | 'email': {'required': True, "type": 'string'}, 48 | 'password': {'required': True, "type": 'string'}, 49 | 'roles': {'type': 'list', 'required': True, 'items': [{'type': 'string'}]} 50 | } 51 | await validate(data, schema) 52 | 53 | users = await User.get_user_by_email(data['email']) 54 | if len(users) >= 1: 55 | return CustomHTTPException(irc['USER_EXISTS'], 404) 56 | 57 | password = data['password'].encode('utf-8') 58 | data['password'] = bcrypt.hashpw(password, bcrypt.gensalt()).decode('utf-8') 59 | 60 | user_id = await User.create_user(data) 61 | 62 | user = await User.get_user_by_id(user_id) 63 | roles = [role['name'] for role in await Role.get_roles_by_id(user['id']) if role['name']] 64 | 65 | if bcrypt.checkpw(password, str(user['password']).encode('utf-8')): 66 | del user['password'] 67 | encoded = jwt.encode({'user': user, 'roles': roles}, request.app.config['secret'], algorithm='HS256').decode('utf-8') 68 | else: 69 | return CustomHTTPException(irc['ACCESS_DENIED'], 401) 70 | 71 | return json_response({'token': encoded, 'user': user, 'roles': roles}) 72 | 73 | 74 | async def user_facebook_login(request): 75 | data = await request.json() 76 | 77 | schema = {'token': {'required': True, "type": 'string'}, 'status': {'required': True, "type": 'string'}} 78 | await validate(data, schema) 79 | 80 | status = data.get('status', None) 81 | token = data.get('token', None) 82 | 83 | if status == 'connected': 84 | 85 | facebook_url_me = f'https://graph.facebook.com/me?redirect=false&access_token={token}' 86 | 87 | async with aiohttp.ClientSession() as session: 88 | async with session.get(facebook_url_me) as r: 89 | data_from_facebook = await r.json() 90 | user = await User.get_user_by_facebook_id(data_from_facebook['id']) 91 | 92 | if not user: 93 | data_user = dict() 94 | password = str(uuid.uuid4()).encode('utf-8') 95 | data_user['password'] = bcrypt.hashpw(password, bcrypt.gensalt()).decode('utf-8') 96 | data_user['roles'] = ['user'] 97 | data_user['facebook_id'] = data_from_facebook['id'] 98 | 99 | if 'name' in data_from_facebook: 100 | name_list = data_from_facebook['name'].split(' ') 101 | if len(name_list) > 1: 102 | data_user['name'] = data_from_facebook['name'] 103 | user_id = await User.create_user(data_user) 104 | 105 | user = await User.get_user_by_id(user_id) 106 | roles = [role['name'] for role in await Role.get_roles_by_id(user['id']) if role['name']] 107 | 108 | if bcrypt.checkpw(password, str(user['password']).encode('utf-8')): 109 | del user['password'] 110 | encoded = jwt.encode({'user': user, 'roles': roles}, request.app.config['secret'], 111 | algorithm='HS256').decode('utf-8') 112 | else: 113 | return CustomHTTPException(irc['ACCESS_DENIED'], 401) 114 | 115 | return json_response({'token': encoded, 'user': user, 'roles': roles}) 116 | else: 117 | roles = [role['name'] for role in await Role.get_roles_by_id(user['id']) if role['name']] 118 | del user['password'] 119 | encoded = jwt.encode({'user': user, 'roles': roles}, request.app.config['secret'], 120 | algorithm='HS256').decode('utf-8') 121 | return json_response({'token': encoded, 'user': user, 'roles': roles}) 122 | 123 | return json_response({"status": "failed"}, status=401) 124 | 125 | 126 | async def login(request): 127 | data = await request.json() 128 | 129 | schema = {'email': {'required': True}, 'password': {'required': True}} 130 | await validate(data, schema) 131 | 132 | users = await User.get_user_by_email(data['email']) 133 | 134 | if len(users) == 1: 135 | password = users[0]['password'].encode('utf-8') 136 | if bcrypt.checkpw(str(data['password']).encode('utf-8'), password): 137 | roles = [role['name'] for role in await Role.get_roles_by_id(users[0]['id']) if role['name']] 138 | del users[0]['password'] 139 | encoded = jwt.encode({'user': users[0], "roles": roles}, request.app.config['secret'], 140 | algorithm='HS256').decode('utf-8') 141 | else: 142 | return CustomHTTPException(irc['ACCESS_DENIED'], 401) 143 | else: 144 | return CustomHTTPException(irc['USER_NOT_FOUND'], 404) 145 | return json_response({"token": encoded, "user": users[0], "roles": roles}) 146 | 147 | 148 | async def user_google_login(request): 149 | data = await request.json() 150 | 151 | schema = {'token': {'required': True, "type": 'string'}} 152 | await validate(data, schema) 153 | 154 | token = data.get('token') 155 | 156 | if token: 157 | google_url = f'https://www.googleapis.com/oauth2/v3/tokeninfo?id_token={token}' 158 | 159 | async with aiohttp.ClientSession() as session: 160 | async with session.get(google_url) as r: 161 | data_from_google = await r.json() 162 | user = await User.get_user_by_google_id(data_from_google['sub']) 163 | 164 | if not user: 165 | data_user = dict() 166 | password = str(uuid.uuid4()).encode('utf-8') 167 | data_user['password'] = bcrypt.hashpw(password, bcrypt.gensalt()).decode('utf-8') 168 | data_user['roles'] = ['user'] 169 | data_user['google_id'] = data_from_google['sub'] 170 | 171 | if 'name' in data_from_google: 172 | name_list = data_from_google['name'].split(' ') 173 | 174 | if len(name_list) > 1: 175 | data_user['name'] = data_from_google['name'] 176 | 177 | if 'picture' in data_from_google: 178 | data_user['image'] = data_from_google['picture'] 179 | 180 | if 'email' in data_from_google and data_from_google['email_verified'] == 'true': 181 | data_user['email'] = data_from_google['email'] 182 | 183 | user_id = await User.create_user(data_user) 184 | user = await User.get_user_by_id(user_id) 185 | roles = [role['name'] for role in await Role.get_roles_by_id(user['id']) if role['name']] 186 | 187 | if bcrypt.checkpw(password, str(user['password']).encode('utf-8')): 188 | del user['password'] 189 | encoded = jwt.encode({'user': user, 'roles': roles}, request.app.config['secret'], 190 | algorithm='HS256').decode('utf-8') 191 | else: 192 | return CustomHTTPException(irc['ACCESS_DENIED'], 401) 193 | 194 | return json_response({'token': encoded, 'user': user, 'roles': roles}) 195 | else: 196 | roles = [role['name'] for role in await Role.get_roles_by_id(user['id']) if role['name']] 197 | del user['password'] 198 | encoded = jwt.encode({'user': user, 'roles': roles}, request.app.config['secret'], 199 | algorithm='HS256').decode('utf-8') 200 | return json_response({'token': encoded, 'user': user, 'roles': roles}) 201 | 202 | return json_response({"status": "failed"}, status=401) 203 | --------------------------------------------------------------------------------