Merge
This commit is contained in:
commit
b1dd4c52b5
4
Pipfile
4
Pipfile
|
@ -13,13 +13,15 @@ passlib = "*"
|
|||
PyJWT = "*"
|
||||
SQLAlchemy = "*"
|
||||
itsdangerous = "*"
|
||||
httpx = "*"
|
||||
httpx = "<0.18.2"
|
||||
psycopg2-binary = "*"
|
||||
Authlib = "*"
|
||||
bson = "*"
|
||||
python-frontmatter = "*"
|
||||
bs4 = "*"
|
||||
transliterate = "*"
|
||||
psycopg2 = "*"
|
||||
requests = "*"
|
||||
|
||||
[dev-packages]
|
||||
|
||||
|
|
|
@ -8,10 +8,12 @@ from starlette.requests import HTTPConnection
|
|||
|
||||
from auth.credentials import AuthCredentials, AuthUser
|
||||
from auth.token import Token
|
||||
from auth.authorize import Authorize
|
||||
from exceptions import InvalidToken, OperationNotAllowed
|
||||
from orm import User
|
||||
from orm.base import local_session
|
||||
from redis import redis
|
||||
from settings import JWT_AUTH_HEADER
|
||||
from settings import JWT_AUTH_HEADER, EMAIL_TOKEN_LIFE_SPAN
|
||||
|
||||
|
||||
class _Authenticate:
|
||||
|
@ -65,9 +67,38 @@ class JWTAuthenticate(AuthenticationBackend):
|
|||
if payload is 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)
|
||||
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):
|
||||
@wraps(func)
|
||||
|
|
|
@ -8,14 +8,14 @@ from auth.validations import User
|
|||
|
||||
class Authorize:
|
||||
@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 device:
|
||||
:param auto_delete: Whether the expiration is automatically deleted, the default is True
|
||||
:return:
|
||||
"""
|
||||
exp = datetime.utcnow() + timedelta(seconds=JWT_LIFE_SPAN)
|
||||
exp = datetime.utcnow() + timedelta(seconds=life_span)
|
||||
token = Token.encode(user, exp=exp, device=device)
|
||||
await redis.execute("SET", f"{user.id}-{token}", "True")
|
||||
if auto_delete:
|
||||
|
@ -37,13 +37,3 @@ class Authorize:
|
|||
async def revoke_all(user: User):
|
||||
tokens = await redis.execute("KEYS", f"{user.id}-*")
|
||||
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_params=None,
|
||||
api_base_url='https://graph.facebook.com/',
|
||||
client_kwargs={'scope': 'user:email'},
|
||||
client_kwargs={'scope': 'public_profile email'},
|
||||
)
|
||||
|
||||
oauth.register(
|
||||
|
@ -36,14 +36,30 @@ oauth.register(
|
|||
name='google',
|
||||
client_id=OAUTH_CLIENTS["GOOGLE"]["id"],
|
||||
client_secret=OAUTH_CLIENTS["GOOGLE"]["key"],
|
||||
access_token_url='https://oauth2.googleapis.com/token',
|
||||
access_token_params=None,
|
||||
authorize_url='https://accounts.google.com/o/oauth2/v2/auth',
|
||||
authorize_params=None,
|
||||
api_base_url='https://oauth2.googleapis.com/',
|
||||
server_metadata_url="https://accounts.google.com/.well-known/openid-configuration",
|
||||
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):
|
||||
provider = request.path_params['provider']
|
||||
request.session['provider'] = provider
|
||||
|
@ -55,14 +71,14 @@ async def oauth_authorize(request):
|
|||
provider = request.session['provider']
|
||||
client = oauth.create_client(provider)
|
||||
token = await client.authorize_access_token(request)
|
||||
resp = await client.get('user', token=token)
|
||||
profile = resp.json()
|
||||
oauth = profile["id"]
|
||||
get_profile = profile_callbacks[provider]
|
||||
profile = await get_profile(client, request, token)
|
||||
user_oauth_info = "%s:%s" % (provider, profile["id"])
|
||||
user_input = {
|
||||
"oauth" : oauth,
|
||||
"oauth" : user_oauth_info,
|
||||
"email" : profile["email"],
|
||||
"username" : profile["name"]
|
||||
}
|
||||
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)
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
#!/bin/bash
|
||||
|
||||
openssl req -newkey rsa:4096 \
|
||||
-x509 \
|
||||
-sha256 \
|
||||
-days 3650 \
|
||||
-nodes \
|
||||
-out discours.crt \
|
||||
-keyout discours.key \
|
||||
-subj "/C=RU/ST=Moscow/L=Moscow/O=Discours/OU=Site/CN=test-api.discours.io"
|
||||
#!/bin/bash
|
||||
|
||||
openssl req -newkey rsa:4096 \
|
||||
-x509 \
|
||||
-sha256 \
|
||||
-days 3650 \
|
||||
-nodes \
|
||||
-out discours.crt \
|
||||
-keyout discours.key \
|
||||
-subj "/C=RU/ST=Moscow/L=Moscow/O=Discours/OU=Site/CN=test-api.discours.io"
|
||||
|
|
4
main.py
4
main.py
|
@ -10,6 +10,7 @@ from starlette.routing import Route
|
|||
|
||||
from auth.authenticate import JWTAuthenticate
|
||||
from auth.oauth import oauth_login, oauth_authorize
|
||||
from auth.email import email_authorize
|
||||
from redis import redis
|
||||
from resolvers.base import resolvers
|
||||
from resolvers.zine import GitTask
|
||||
|
@ -34,7 +35,8 @@ async def shutdown():
|
|||
|
||||
routes = [
|
||||
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)
|
||||
|
|
|
@ -56,7 +56,7 @@ class Operation(Base):
|
|||
|
||||
class Resource(Base):
|
||||
__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")
|
||||
|
||||
@staticmethod
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from typing import List
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import Table, Column, Integer, String, ForeignKey, Boolean, DateTime, JSON as JSONType
|
||||
from sqlalchemy.orm import relationship
|
||||
|
@ -42,8 +43,8 @@ class User(Base):
|
|||
slug: str = Column(String, unique=True, comment="User's slug")
|
||||
muted: bool = Column(Boolean, default=False)
|
||||
emailConfirmed: bool = Column(Boolean, default=False)
|
||||
createdAt: DateTime = Column(DateTime, nullable=False, comment="Created at")
|
||||
wasOnlineAt: DateTime = Column(DateTime, nullable=False, comment="Was online at")
|
||||
createdAt: DateTime = Column(DateTime, nullable=False, default = datetime.now, comment="Created at")
|
||||
wasOnlineAt: DateTime = Column(DateTime, nullable=False, default = datetime.now, comment="Was online at")
|
||||
links: JSONType = Column(JSONType, nullable=True, comment="Links")
|
||||
oauth: str = Column(String, nullable=True)
|
||||
notifications = relationship(lambda: UserNotifications)
|
||||
|
|
|
@ -5,6 +5,7 @@ from auth.authorize import Authorize
|
|||
from auth.identity import Identity
|
||||
from auth.password import Password
|
||||
from auth.validations import CreateUser
|
||||
from auth.email import send_confirm_email, send_auth_email
|
||||
from orm import User
|
||||
from orm.base import local_session
|
||||
from resolvers.base import mutation, query
|
||||
|
@ -29,11 +30,8 @@ async def register(*_, email: str, password: str = ""):
|
|||
create_user = CreateUser(**inp)
|
||||
create_user.username = email.split('@')[0]
|
||||
if not password:
|
||||
# NOTE: 1 hour confirm_token expire
|
||||
confirm_token = Token.encode(create_user, datetime.now() + timedelta(hours = 1) , "email")
|
||||
# TODO: sendAuthEmail(confirm_token)
|
||||
# без пароля не возвращаем, а высылаем токен на почту
|
||||
#
|
||||
user = User.create(**create_user.dict())
|
||||
await send_confirm_email(user)
|
||||
return { "user": user }
|
||||
else:
|
||||
create_user.password = Password.encode(create_user.password)
|
||||
|
@ -43,12 +41,16 @@ async def register(*_, email: str, password: str = ""):
|
|||
|
||||
|
||||
@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:
|
||||
orm_user = session.query(User).filter(User.email == email).first()
|
||||
if orm_user is None:
|
||||
return {"error" : "invalid email"}
|
||||
|
||||
if not password:
|
||||
await send_auth_email(orm_user)
|
||||
return {}
|
||||
|
||||
try:
|
||||
device = info.context["request"].headers['device']
|
||||
except KeyError:
|
||||
|
|
|
@ -58,7 +58,7 @@ type Mutation {
|
|||
confirmEmail(token: String!): AuthResult!
|
||||
requestPasswordReset(email: String!): Boolean!
|
||||
confirmPasswordReset(token: String!): Boolean!
|
||||
registerUser(email: String!, password: String!): AuthResult!
|
||||
registerUser(email: String!, password: String): AuthResult!
|
||||
# updatePassword(password: String!, token: String!): Token!
|
||||
# invalidateAllTokens: Boolean!
|
||||
# invalidateTokenById(id: Int!): Boolean!
|
||||
|
@ -81,7 +81,7 @@ type Mutation {
|
|||
type Query {
|
||||
# auth
|
||||
isEmailFree(email: String!): Result!
|
||||
signIn(email: String!, password: String!): AuthResult!
|
||||
signIn(email: String!, password: String): AuthResult!
|
||||
signOut: Result!
|
||||
# user profile
|
||||
getCurrentUser: UserResult!
|
||||
|
|
|
@ -3,13 +3,19 @@ from os import environ
|
|||
|
||||
PORT = 8080
|
||||
|
||||
BACKEND_URL = "https://localhost:8080"
|
||||
|
||||
DB_URL = environ.get("DB_URL") or "sqlite:///db.sqlite3"
|
||||
JWT_ALGORITHM = "HS256"
|
||||
JWT_SECRET_KEY = "8f1bd7696ffb482d8486dfbc6e7d16dd-secret-key"
|
||||
JWT_LIFE_SPAN = 24 * 60 * 60 # seconds
|
||||
JWT_AUTH_HEADER = "Auth"
|
||||
EMAIL_TOKEN_LIFE_SPAN = 1 * 60 * 60 # seconds
|
||||
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_CLIENTS = {}
|
||||
for provider in OAUTH_PROVIDERS:
|
||||
|
|
Loading…
Reference in New Issue
Block a user