Merge
This commit is contained in:
commit
b1dd4c52b5
4
Pipfile
4
Pipfile
|
@ -13,13 +13,15 @@ passlib = "*"
|
||||||
PyJWT = "*"
|
PyJWT = "*"
|
||||||
SQLAlchemy = "*"
|
SQLAlchemy = "*"
|
||||||
itsdangerous = "*"
|
itsdangerous = "*"
|
||||||
httpx = "*"
|
httpx = "<0.18.2"
|
||||||
psycopg2-binary = "*"
|
psycopg2-binary = "*"
|
||||||
Authlib = "*"
|
Authlib = "*"
|
||||||
bson = "*"
|
bson = "*"
|
||||||
python-frontmatter = "*"
|
python-frontmatter = "*"
|
||||||
bs4 = "*"
|
bs4 = "*"
|
||||||
transliterate = "*"
|
transliterate = "*"
|
||||||
|
psycopg2 = "*"
|
||||||
|
requests = "*"
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
|
|
||||||
|
|
|
@ -8,10 +8,12 @@ from starlette.requests import HTTPConnection
|
||||||
|
|
||||||
from auth.credentials import AuthCredentials, AuthUser
|
from auth.credentials import AuthCredentials, AuthUser
|
||||||
from auth.token import Token
|
from auth.token import Token
|
||||||
|
from auth.authorize import Authorize
|
||||||
from exceptions import InvalidToken, OperationNotAllowed
|
from exceptions import InvalidToken, OperationNotAllowed
|
||||||
from orm import User
|
from orm import User
|
||||||
|
from orm.base import local_session
|
||||||
from redis import redis
|
from redis import redis
|
||||||
from settings import JWT_AUTH_HEADER
|
from settings import JWT_AUTH_HEADER, EMAIL_TOKEN_LIFE_SPAN
|
||||||
|
|
||||||
|
|
||||||
class _Authenticate:
|
class _Authenticate:
|
||||||
|
@ -65,9 +67,38 @@ class JWTAuthenticate(AuthenticationBackend):
|
||||||
if payload is None:
|
if payload is None:
|
||||||
return AuthCredentials(scopes=[]), AuthUser(user_id=None)
|
return AuthCredentials(scopes=[]), AuthUser(user_id=None)
|
||||||
|
|
||||||
|
if not payload.device in ("pc", "mobile"):
|
||||||
|
return AuthCredentials(scopes=[]), AuthUser(user_id=None)
|
||||||
|
|
||||||
scopes = User.get_permission(user_id=payload.user_id)
|
scopes = User.get_permission(user_id=payload.user_id)
|
||||||
return AuthCredentials(user_id=payload.user_id, scopes=scopes, logged_in=True), AuthUser(user_id=payload.user_id)
|
return AuthCredentials(user_id=payload.user_id, scopes=scopes, logged_in=True), AuthUser(user_id=payload.user_id)
|
||||||
|
|
||||||
|
class EmailAuthenticate:
|
||||||
|
@staticmethod
|
||||||
|
async def get_email_token(user):
|
||||||
|
token = await Authorize.authorize(
|
||||||
|
user,
|
||||||
|
device="email",
|
||||||
|
life_span=EMAIL_TOKEN_LIFE_SPAN
|
||||||
|
)
|
||||||
|
return token
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def authenticate(token):
|
||||||
|
payload = await _Authenticate.verify(token)
|
||||||
|
if payload is None:
|
||||||
|
raise InvalidToken("invalid token")
|
||||||
|
if payload.device != "email":
|
||||||
|
raise InvalidToken("invalid token")
|
||||||
|
with local_session() as session:
|
||||||
|
user = session.query(User).filter_by(id=payload.user_id).first()
|
||||||
|
if not user:
|
||||||
|
raise Exception("user not exist")
|
||||||
|
if not user.emailConfirmed:
|
||||||
|
user.emailConfirmed = True
|
||||||
|
session.commit()
|
||||||
|
auth_token = await Authorize.authorize(user)
|
||||||
|
return (auth_token, user)
|
||||||
|
|
||||||
def login_required(func):
|
def login_required(func):
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
|
|
|
@ -8,14 +8,14 @@ from auth.validations import User
|
||||||
|
|
||||||
class Authorize:
|
class Authorize:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def authorize(user: User, device: str = "pc", auto_delete=True) -> str:
|
async def authorize(user: User, device: str = "pc", life_span = JWT_LIFE_SPAN, auto_delete=True) -> str:
|
||||||
"""
|
"""
|
||||||
:param user:
|
:param user:
|
||||||
:param device:
|
:param device:
|
||||||
:param auto_delete: Whether the expiration is automatically deleted, the default is True
|
:param auto_delete: Whether the expiration is automatically deleted, the default is True
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
exp = datetime.utcnow() + timedelta(seconds=JWT_LIFE_SPAN)
|
exp = datetime.utcnow() + timedelta(seconds=life_span)
|
||||||
token = Token.encode(user, exp=exp, device=device)
|
token = Token.encode(user, exp=exp, device=device)
|
||||||
await redis.execute("SET", f"{user.id}-{token}", "True")
|
await redis.execute("SET", f"{user.id}-{token}", "True")
|
||||||
if auto_delete:
|
if auto_delete:
|
||||||
|
@ -37,13 +37,3 @@ class Authorize:
|
||||||
async def revoke_all(user: User):
|
async def revoke_all(user: User):
|
||||||
tokens = await redis.execute("KEYS", f"{user.id}-*")
|
tokens = await redis.execute("KEYS", f"{user.id}-*")
|
||||||
await redis.execute("DEL", *tokens)
|
await redis.execute("DEL", *tokens)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def confirm(token: str):
|
|
||||||
try:
|
|
||||||
# NOTE: auth_token and email_token are different
|
|
||||||
payload = Token.decode(token) # TODO: check to decode here the proper way
|
|
||||||
auth_token = self.authorize(payload.user)
|
|
||||||
return auth_token, payload.user
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
45
auth/email.py
Normal file
45
auth/email.py
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import requests
|
||||||
|
from starlette.responses import PlainTextResponse
|
||||||
|
from starlette.exceptions import HTTPException
|
||||||
|
|
||||||
|
from auth.authenticate import EmailAuthenticate
|
||||||
|
|
||||||
|
from settings import BACKEND_URL, MAILGUN_API_KEY, MAILGUN_DOMAIN
|
||||||
|
|
||||||
|
MAILGUN_API_URL = "https://api.mailgun.net/v3/%s/messages" % (MAILGUN_DOMAIN)
|
||||||
|
MAILGUN_FROM = "postmaster <postmaster@%s>" % (MAILGUN_DOMAIN)
|
||||||
|
|
||||||
|
AUTH_URL = "%s/email_authorize" % (BACKEND_URL)
|
||||||
|
|
||||||
|
async def send_confirm_email(user):
|
||||||
|
text = "<html><body>To confirm registration follow the <a href='%s'>link</link></body></html>"
|
||||||
|
await send_email(user, text)
|
||||||
|
|
||||||
|
async def send_auth_email(user):
|
||||||
|
text = "<html><body>To enter the site follow the <a href='%s'>link</link></body></html>"
|
||||||
|
await send_email(user, text)
|
||||||
|
|
||||||
|
async def send_email(user, text):
|
||||||
|
token = await EmailAuthenticate.get_email_token(user)
|
||||||
|
|
||||||
|
to = "%s <%s>" % (user.username, user.email)
|
||||||
|
auth_url_with_token = "%s?token=%s" % (AUTH_URL, token)
|
||||||
|
text = text % (auth_url_with_token)
|
||||||
|
response = requests.post(
|
||||||
|
MAILGUN_API_URL,
|
||||||
|
auth = ("api", MAILGUN_API_KEY),
|
||||||
|
data = {
|
||||||
|
"from": MAILGUN_FROM,
|
||||||
|
"to": to,
|
||||||
|
"subject": "authorize log in",
|
||||||
|
"html": text
|
||||||
|
}
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
async def email_authorize(request):
|
||||||
|
token = request.query_params.get('token')
|
||||||
|
if not token:
|
||||||
|
raise HTTPException(500, "invalid url")
|
||||||
|
auth_token, user = await EmailAuthenticate.authenticate(token)
|
||||||
|
return PlainTextResponse(auth_token)
|
|
@ -17,7 +17,7 @@ oauth.register(
|
||||||
authorize_url='https://www.facebook.com/v11.0/dialog/oauth',
|
authorize_url='https://www.facebook.com/v11.0/dialog/oauth',
|
||||||
authorize_params=None,
|
authorize_params=None,
|
||||||
api_base_url='https://graph.facebook.com/',
|
api_base_url='https://graph.facebook.com/',
|
||||||
client_kwargs={'scope': 'user:email'},
|
client_kwargs={'scope': 'public_profile email'},
|
||||||
)
|
)
|
||||||
|
|
||||||
oauth.register(
|
oauth.register(
|
||||||
|
@ -36,14 +36,30 @@ oauth.register(
|
||||||
name='google',
|
name='google',
|
||||||
client_id=OAUTH_CLIENTS["GOOGLE"]["id"],
|
client_id=OAUTH_CLIENTS["GOOGLE"]["id"],
|
||||||
client_secret=OAUTH_CLIENTS["GOOGLE"]["key"],
|
client_secret=OAUTH_CLIENTS["GOOGLE"]["key"],
|
||||||
access_token_url='https://oauth2.googleapis.com/token',
|
server_metadata_url="https://accounts.google.com/.well-known/openid-configuration",
|
||||||
access_token_params=None,
|
|
||||||
authorize_url='https://accounts.google.com/o/oauth2/v2/auth',
|
|
||||||
authorize_params=None,
|
|
||||||
api_base_url='https://oauth2.googleapis.com/',
|
|
||||||
client_kwargs={'scope': 'openid email profile'}
|
client_kwargs={'scope': 'openid email profile'}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def google_profile(client, request, token):
|
||||||
|
profile = await client.parse_id_token(request, token)
|
||||||
|
profile["id"] = profile["sub"]
|
||||||
|
return profile
|
||||||
|
|
||||||
|
async def facebook_profile(client, request, token):
|
||||||
|
profile = await client.get('me?fields=name,id,email', token=token)
|
||||||
|
return profile.json()
|
||||||
|
|
||||||
|
async def github_profile(client, request, token):
|
||||||
|
profile = await client.get('user', token=token)
|
||||||
|
return profile.json()
|
||||||
|
|
||||||
|
profile_callbacks = {
|
||||||
|
"google" : google_profile,
|
||||||
|
"facebook" : facebook_profile,
|
||||||
|
"github" : github_profile
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async def oauth_login(request):
|
async def oauth_login(request):
|
||||||
provider = request.path_params['provider']
|
provider = request.path_params['provider']
|
||||||
request.session['provider'] = provider
|
request.session['provider'] = provider
|
||||||
|
@ -55,14 +71,14 @@ async def oauth_authorize(request):
|
||||||
provider = request.session['provider']
|
provider = request.session['provider']
|
||||||
client = oauth.create_client(provider)
|
client = oauth.create_client(provider)
|
||||||
token = await client.authorize_access_token(request)
|
token = await client.authorize_access_token(request)
|
||||||
resp = await client.get('user', token=token)
|
get_profile = profile_callbacks[provider]
|
||||||
profile = resp.json()
|
profile = await get_profile(client, request, token)
|
||||||
oauth = profile["id"]
|
user_oauth_info = "%s:%s" % (provider, profile["id"])
|
||||||
user_input = {
|
user_input = {
|
||||||
"oauth" : oauth,
|
"oauth" : user_oauth_info,
|
||||||
"email" : profile["email"],
|
"email" : profile["email"],
|
||||||
"username" : profile["name"]
|
"username" : profile["name"]
|
||||||
}
|
}
|
||||||
user = Identity.identity_oauth(user_input)
|
user = Identity.identity_oauth(user_input)
|
||||||
token = await Authorize.authorize(user, device="pc", auto_delete=False)
|
token = await Authorize.authorize(user, device="pc")
|
||||||
return PlainTextResponse(token)
|
return PlainTextResponse(token)
|
||||||
|
|
4
main.py
4
main.py
|
@ -10,6 +10,7 @@ from starlette.routing import Route
|
||||||
|
|
||||||
from auth.authenticate import JWTAuthenticate
|
from auth.authenticate import JWTAuthenticate
|
||||||
from auth.oauth import oauth_login, oauth_authorize
|
from auth.oauth import oauth_login, oauth_authorize
|
||||||
|
from auth.email import email_authorize
|
||||||
from redis import redis
|
from redis import redis
|
||||||
from resolvers.base import resolvers
|
from resolvers.base import resolvers
|
||||||
from resolvers.zine import GitTask
|
from resolvers.zine import GitTask
|
||||||
|
@ -34,7 +35,8 @@ async def shutdown():
|
||||||
|
|
||||||
routes = [
|
routes = [
|
||||||
Route("/oauth/{provider}", endpoint=oauth_login),
|
Route("/oauth/{provider}", endpoint=oauth_login),
|
||||||
Route("/authorize", endpoint=oauth_authorize)
|
Route("/oauth_authorize", endpoint=oauth_authorize),
|
||||||
|
Route("/email_authorize", endpoint=email_authorize)
|
||||||
]
|
]
|
||||||
|
|
||||||
app = Starlette(debug=True, on_startup=[start_up], on_shutdown=[shutdown], middleware=middleware, routes=routes)
|
app = Starlette(debug=True, on_startup=[start_up], on_shutdown=[shutdown], middleware=middleware, routes=routes)
|
||||||
|
|
|
@ -56,7 +56,7 @@ class Operation(Base):
|
||||||
|
|
||||||
class Resource(Base):
|
class Resource(Base):
|
||||||
__tablename__ = "resource"
|
__tablename__ = "resource"
|
||||||
resource_class: Type[Base] = Column(ClassType, nullable=False, unique=True, comment="Resource class")
|
resource_class: str = Column(String, nullable=False, unique=True, comment="Resource class")
|
||||||
name: str = Column(String, nullable=False, unique=True, comment="Resource name")
|
name: str = Column(String, nullable=False, unique=True, comment="Resource name")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from typing import List
|
from typing import List
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from sqlalchemy import Table, Column, Integer, String, ForeignKey, Boolean, DateTime, JSON as JSONType
|
from sqlalchemy import Table, Column, Integer, String, ForeignKey, Boolean, DateTime, JSON as JSONType
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
|
@ -42,8 +43,8 @@ class User(Base):
|
||||||
slug: str = Column(String, unique=True, comment="User's slug")
|
slug: str = Column(String, unique=True, comment="User's slug")
|
||||||
muted: bool = Column(Boolean, default=False)
|
muted: bool = Column(Boolean, default=False)
|
||||||
emailConfirmed: bool = Column(Boolean, default=False)
|
emailConfirmed: bool = Column(Boolean, default=False)
|
||||||
createdAt: DateTime = Column(DateTime, nullable=False, comment="Created at")
|
createdAt: DateTime = Column(DateTime, nullable=False, default = datetime.now, comment="Created at")
|
||||||
wasOnlineAt: DateTime = Column(DateTime, nullable=False, comment="Was online at")
|
wasOnlineAt: DateTime = Column(DateTime, nullable=False, default = datetime.now, comment="Was online at")
|
||||||
links: JSONType = Column(JSONType, nullable=True, comment="Links")
|
links: JSONType = Column(JSONType, nullable=True, comment="Links")
|
||||||
oauth: str = Column(String, nullable=True)
|
oauth: str = Column(String, nullable=True)
|
||||||
notifications = relationship(lambda: UserNotifications)
|
notifications = relationship(lambda: UserNotifications)
|
||||||
|
|
|
@ -5,6 +5,7 @@ from auth.authorize import Authorize
|
||||||
from auth.identity import Identity
|
from auth.identity import Identity
|
||||||
from auth.password import Password
|
from auth.password import Password
|
||||||
from auth.validations import CreateUser
|
from auth.validations import CreateUser
|
||||||
|
from auth.email import send_confirm_email, send_auth_email
|
||||||
from orm import User
|
from orm import User
|
||||||
from orm.base import local_session
|
from orm.base import local_session
|
||||||
from resolvers.base import mutation, query
|
from resolvers.base import mutation, query
|
||||||
|
@ -29,11 +30,8 @@ async def register(*_, email: str, password: str = ""):
|
||||||
create_user = CreateUser(**inp)
|
create_user = CreateUser(**inp)
|
||||||
create_user.username = email.split('@')[0]
|
create_user.username = email.split('@')[0]
|
||||||
if not password:
|
if not password:
|
||||||
# NOTE: 1 hour confirm_token expire
|
user = User.create(**create_user.dict())
|
||||||
confirm_token = Token.encode(create_user, datetime.now() + timedelta(hours = 1) , "email")
|
await send_confirm_email(user)
|
||||||
# TODO: sendAuthEmail(confirm_token)
|
|
||||||
# без пароля не возвращаем, а высылаем токен на почту
|
|
||||||
#
|
|
||||||
return { "user": user }
|
return { "user": user }
|
||||||
else:
|
else:
|
||||||
create_user.password = Password.encode(create_user.password)
|
create_user.password = Password.encode(create_user.password)
|
||||||
|
@ -43,12 +41,16 @@ async def register(*_, email: str, password: str = ""):
|
||||||
|
|
||||||
|
|
||||||
@query.field("signIn")
|
@query.field("signIn")
|
||||||
async def login(_, info: GraphQLResolveInfo, email: str, password: str):
|
async def login(_, info: GraphQLResolveInfo, email: str, password: str = ""):
|
||||||
with local_session() as session:
|
with local_session() as session:
|
||||||
orm_user = session.query(User).filter(User.email == email).first()
|
orm_user = session.query(User).filter(User.email == email).first()
|
||||||
if orm_user is None:
|
if orm_user is None:
|
||||||
return {"error" : "invalid email"}
|
return {"error" : "invalid email"}
|
||||||
|
|
||||||
|
if not password:
|
||||||
|
await send_auth_email(orm_user)
|
||||||
|
return {}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
device = info.context["request"].headers['device']
|
device = info.context["request"].headers['device']
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
|
|
@ -58,7 +58,7 @@ type Mutation {
|
||||||
confirmEmail(token: String!): AuthResult!
|
confirmEmail(token: String!): AuthResult!
|
||||||
requestPasswordReset(email: String!): Boolean!
|
requestPasswordReset(email: String!): Boolean!
|
||||||
confirmPasswordReset(token: String!): Boolean!
|
confirmPasswordReset(token: String!): Boolean!
|
||||||
registerUser(email: String!, password: String!): AuthResult!
|
registerUser(email: String!, password: String): AuthResult!
|
||||||
# updatePassword(password: String!, token: String!): Token!
|
# updatePassword(password: String!, token: String!): Token!
|
||||||
# invalidateAllTokens: Boolean!
|
# invalidateAllTokens: Boolean!
|
||||||
# invalidateTokenById(id: Int!): Boolean!
|
# invalidateTokenById(id: Int!): Boolean!
|
||||||
|
@ -81,7 +81,7 @@ type Mutation {
|
||||||
type Query {
|
type Query {
|
||||||
# auth
|
# auth
|
||||||
isEmailFree(email: String!): Result!
|
isEmailFree(email: String!): Result!
|
||||||
signIn(email: String!, password: String!): AuthResult!
|
signIn(email: String!, password: String): AuthResult!
|
||||||
signOut: Result!
|
signOut: Result!
|
||||||
# user profile
|
# user profile
|
||||||
getCurrentUser: UserResult!
|
getCurrentUser: UserResult!
|
||||||
|
|
|
@ -3,13 +3,19 @@ from os import environ
|
||||||
|
|
||||||
PORT = 8080
|
PORT = 8080
|
||||||
|
|
||||||
|
BACKEND_URL = "https://localhost:8080"
|
||||||
|
|
||||||
DB_URL = environ.get("DB_URL") or "sqlite:///db.sqlite3"
|
DB_URL = environ.get("DB_URL") or "sqlite:///db.sqlite3"
|
||||||
JWT_ALGORITHM = "HS256"
|
JWT_ALGORITHM = "HS256"
|
||||||
JWT_SECRET_KEY = "8f1bd7696ffb482d8486dfbc6e7d16dd-secret-key"
|
JWT_SECRET_KEY = "8f1bd7696ffb482d8486dfbc6e7d16dd-secret-key"
|
||||||
JWT_LIFE_SPAN = 24 * 60 * 60 # seconds
|
JWT_LIFE_SPAN = 24 * 60 * 60 # seconds
|
||||||
JWT_AUTH_HEADER = "Auth"
|
JWT_AUTH_HEADER = "Auth"
|
||||||
|
EMAIL_TOKEN_LIFE_SPAN = 1 * 60 * 60 # seconds
|
||||||
REDIS_URL = environ.get("REDIS_URL") or "redis://127.0.0.1"
|
REDIS_URL = environ.get("REDIS_URL") or "redis://127.0.0.1"
|
||||||
|
|
||||||
|
MAILGUN_API_KEY = environ.get("MAILGUN_API_KEY")
|
||||||
|
MAILGUN_DOMAIN = "sandbox6afe2b71cd354c8fa59e0b868c20a23b.mailgun.org"
|
||||||
|
|
||||||
OAUTH_PROVIDERS = ("GITHUB", "FACEBOOK", "GOOGLE")
|
OAUTH_PROVIDERS = ("GITHUB", "FACEBOOK", "GOOGLE")
|
||||||
OAUTH_CLIENTS = {}
|
OAUTH_CLIENTS = {}
|
||||||
for provider in OAUTH_PROVIDERS:
|
for provider in OAUTH_PROVIDERS:
|
||||||
|
|
Loading…
Reference in New Issue
Block a user