├── 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 |
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 |
82 |
83 |
84 |
87 | Facebook login failed.
88 |
89 |
90 |
91 |
94 | Google login failed.
95 |
96 |
97 |
98 |
101 | Login failed.
102 |
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 |
--------------------------------------------------------------------------------