migration, auth, refactoring, formatting

This commit is contained in:
2022-09-17 21:12:14 +03:00
parent 6b4c00d9e7
commit 3136eecd7e
68 changed files with 968 additions and 930 deletions

View File

@@ -3,9 +3,42 @@ from resolvers.auth import (
sign_out,
is_email_used,
register,
confirm,
auth_forget,
auth_reset,
confirm_email,
auth_send_link,
)
from resolvers.collab import remove_author, invite_author
from resolvers.community import (
create_community,
delete_community,
get_community,
get_communities,
)
# from resolvers.collab import invite_author, remove_author
from resolvers.editor import create_shout, delete_shout, update_shout
from resolvers.profile import (
get_users_by_slugs,
get_current_user,
get_user_reacted_shouts,
get_user_roles,
get_top_authors,
)
# from resolvers.feed import shouts_for_feed, my_candidates
from resolvers.reactions import (
create_reaction,
delete_reaction,
update_reaction,
reactions_unfollow,
reactions_follow,
get_shout_reactions,
)
from resolvers.topics import (
topic_follow,
topic_unfollow,
topics_by_author,
topics_by_community,
topics_all,
)
from resolvers.zine import (
get_shout_by_slug,
@@ -21,36 +54,6 @@ from resolvers.zine import (
shouts_by_topics,
shouts_by_communities,
)
from resolvers.profile import (
get_users_by_slugs,
get_current_user,
get_user_reacted_shouts,
get_user_roles,
get_top_authors
)
from resolvers.topics import (
topic_follow,
topic_unfollow,
topics_by_author,
topics_by_community,
topics_all,
)
# from resolvers.feed import shouts_for_feed, my_candidates
from resolvers.reactions import (
create_reaction,
delete_reaction,
update_reaction,
)
# from resolvers.collab import invite_author, remove_author
from resolvers.editor import create_shout, delete_shout, update_shout
from resolvers.community import (
create_community,
delete_community,
get_community,
get_communities,
)
__all__ = [
"follow",
@@ -59,9 +62,8 @@ __all__ = [
"login",
"register",
"is_email_used",
"confirm",
"auth_forget",
"auth_reset",
"confirm_email",
"auth_send_link",
"sign_out",
# profile
"get_current_user",
@@ -69,10 +71,7 @@ __all__ = [
"get_user_roles",
"get_top_authors",
# zine
"shouts_for_feed",
"my_candidates",
"recent_published",
"recent_reacted",
"recent_all",
"shouts_by_topics",
"shouts_by_authors",
@@ -82,7 +81,6 @@ __all__ = [
"top_overall",
"top_viewed",
"view_shout",
"view_reaction",
"get_shout_by_slug",
# editor
"create_shout",

View File

@@ -1,29 +1,42 @@
from graphql import GraphQLResolveInfo
from transliterate import translit
from urllib.parse import quote_plus
from auth.authenticate import login_required, ResetPassword
from auth.authorize import Authorize
from auth.identity import Identity
from auth.password import Password
from auth.email import send_confirm_email, send_auth_email, send_reset_password_email
from orm import User, Role
from auth.tokenstorage import TokenStorage
from graphql.type import GraphQLResolveInfo
from transliterate import translit
from auth.authenticate import login_required
from auth.email import send_auth_email
from auth.identity import Identity, Password
from base.exceptions import (
InvalidPassword,
InvalidToken,
ObjectNotExist,
OperationNotAllowed,
)
from base.orm import local_session
from base.resolvers import mutation, query
from orm import User, Role
from resolvers.profile import get_user_info
from base.exceptions import InvalidPassword, InvalidToken, ObjectNotExist, OperationNotAllowed
from settings import JWT_AUTH_HEADER
from settings import SESSION_TOKEN_HEADER
@mutation.field("confirmEmail")
async def confirm(*_, confirm_token):
async def confirm_email(*_, confirm_token):
"""confirm owning email address"""
auth_token, user = await Authorize.confirm(confirm_token)
if auth_token:
user.emailConfirmed = True
user.save()
return {"token": auth_token, "user": user}
else:
# not an error, warns user
user_id = None
try:
user_id = await TokenStorage.get(confirm_token)
with local_session() as session:
user = session.query(User).where(User.id == user_id).first()
session_token = TokenStorage.create_session(user)
user.emailConfirmed = True
session.add(user)
session.commit()
return {"token": session_token, "user": user}
except InvalidToken as e:
raise InvalidToken(e.message)
except Exception as e:
print(e) # FIXME: debug only
return {"error": "email not confirmed"}
@@ -50,40 +63,21 @@ async def register(*_, email: str, password: str = ""):
session.add(user)
session.commit()
await send_confirm_email(user)
token = await TokenStorage.create_onetime(user)
await send_auth_email(user, token)
return {"user": user}
@mutation.field("requestPasswordUpdate")
async def auth_forget(_, info, email):
"""send email to recover account"""
@mutation.field("sendLink")
async def auth_send_link(_, info, email):
"""send link with confirm code to email"""
with local_session() as session:
user = session.query(User).filter(User.email == email).first()
if not user:
raise ObjectNotExist("User not found")
await send_reset_password_email(user)
return {}
@mutation.field("updatePassword")
async def auth_reset(_, info, password, resetToken):
"""set the new password"""
try:
user_id = await ResetPassword.verify(resetToken)
except InvalidToken as e:
raise InvalidToken(e.message)
# return {"error": e.message}
with local_session() as session:
user = session.query(User).filter_by(id=user_id).first()
if not user:
raise ObjectNotExist("User not found")
user.password = Password.encode(password)
session.commit()
token = await TokenStorage.create_onetime(user)
await send_auth_email(user, token)
return {}
@@ -92,48 +86,44 @@ 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:
print(f"signIn {email}: email not found")
# return {"error": "email not found"}
raise ObjectNotExist("User not found")
if orm_user is None:
print(f"[auth] {email}: email not found")
# return {"error": "email not found"}
raise ObjectNotExist("User not found") # contains webserver status
if not password:
print(f"signIn {email}: send auth email")
await send_auth_email(orm_user)
return {""}
if not password:
print(f"[auth] send confirm link to {email}")
token = await TokenStorage.create_onetime(orm_user)
await send_auth_email(orm_user, token)
# FIXME: not an error, warning
return {"error": "no password, email link was sent"}
if not orm_user.emailConfirmed:
# not an error, warns users
return {"error": "email not confirmed"}
try:
device = info.context["request"].headers["device"]
except KeyError:
device = "pc"
auto_delete = False if device == "mobile" else True # why autodelete with mobile?
try:
user = Identity.identity(orm_user, password)
except InvalidPassword:
print(f"signIn {email}: invalid password")
raise InvalidPassword("invalid passoword")
# return {"error": "invalid password"}
token = await Authorize.authorize(user, device=device, auto_delete=auto_delete)
print(f"signIn {email}: OK")
return {
"token": token,
"user": orm_user,
"info": await get_user_info(orm_user.slug),
}
else:
# sign in using password
if not orm_user.emailConfirmed:
# not an error, warns users
return {"error": "please, confirm email"}
else:
try:
user = Identity.password(orm_user, password)
session_token = await TokenStorage.create_session(user)
print(f"[auth] user {email} authorized")
return {
"token": session_token,
"user": user,
"info": await get_user_info(user.slug),
}
except InvalidPassword:
print(f"[auth] {email}: invalid password")
raise InvalidPassword("invalid passoword") # contains webserver status
# return {"error": "invalid password"}
@query.field("signOut")
@login_required
async def sign_out(_, info: GraphQLResolveInfo):
token = info.context["request"].headers[JWT_AUTH_HEADER]
status = await Authorize.revoke(token)
token = info.context["request"].headers[SESSION_TOKEN_HEADER]
status = await TokenStorage.revoke(token)
return status

View File

@@ -1,10 +1,11 @@
from datetime import datetime
from auth.authenticate import login_required
from base.orm import local_session
from base.resolvers import query, mutation
from orm.collab import Collab
from orm.shout import Shout
from orm.user import User
from base.resolvers import query, mutation
from auth.authenticate import login_required
@query.field("getCollabs")
@@ -12,11 +13,10 @@ from auth.authenticate import login_required
async def get_collabs(_, info):
auth = info.context["request"].auth
user_id = auth.user_id
collabs = []
with local_session() as session:
user = session.query(User).where(User.id == user_id).first()
collabs = session.query(Collab).filter(user.slug in Collab.authors)
return collabs
return collabs
@mutation.field("inviteAuthor")
@@ -37,7 +37,7 @@ async def invite_author(_, info, author, shout):
return {"error": "already added"}
shout.authors.append(author)
shout.updated_at = datetime.now()
shout.save()
session.add(shout)
session.commit()
# TODO: email notify
@@ -63,7 +63,7 @@ async def remove_author(_, info, author, shout):
return {"error": "not in authors"}
shout.authors.remove(author)
shout.updated_at = datetime.now()
shout.save()
session.add(shout)
session.commit()
# result = Result("INVITED")

View File

@@ -1,11 +1,13 @@
from orm.collection import Collection, ShoutCollection
from base.orm import local_session
from orm.user import User
from base.resolvers import mutation, query
from auth.authenticate import login_required
from datetime import datetime
from sqlalchemy import and_
from auth.authenticate import login_required
from base.orm import local_session
from base.resolvers import mutation, query
from orm.collection import Collection, ShoutCollection
from orm.user import User
@mutation.field("createCollection")
@login_required
@@ -27,7 +29,7 @@ async def create_collection(_, _info, inp):
async def update_collection(_, info, inp):
auth = info.context["request"].auth
user_id = auth.user_id
collection_slug = input.get("slug", "")
collection_slug = inp.get("slug", "")
with local_session() as session:
owner = session.query(User).filter(User.id == user_id) # note list here
collection = (
@@ -57,6 +59,7 @@ async def delete_collection(_, info, slug):
if collection.owner != user_id:
return {"error": "access denied"}
collection.deletedAt = datetime.now()
session.add(collection)
session.commit()
return {}

View File

@@ -1,12 +1,14 @@
from orm.community import Community, CommunityFollower
from base.orm import local_session
from orm.user import User
from base.resolvers import mutation, query
from auth.authenticate import login_required
from datetime import datetime
from typing import List
from sqlalchemy import and_
from auth.authenticate import login_required
from base.orm import local_session
from base.resolvers import mutation, query
from orm.community import Community, CommunityFollower
from orm.user import User
@mutation.field("createCommunity")
@login_required
@@ -23,6 +25,8 @@ async def create_community(_, info, input):
createdBy=user.slug,
createdAt=datetime.now(),
)
session.add(community)
session.commit()
return {"community": community}
@@ -48,6 +52,7 @@ async def update_community(_, info, input):
community.desc = input.get("desc", "")
community.pic = input.get("pic", "")
community.updatedAt = datetime.now()
session.add(community)
session.commit()
@@ -64,6 +69,7 @@ async def delete_community(_, info, slug):
if community.owner != user_id:
return {"error": "access denied"}
community.deletedAt = datetime.now()
session.add(community)
session.commit()
return {}

View File

@@ -1,37 +1,38 @@
from orm import Shout
from datetime import datetime
from auth.authenticate import login_required
from base.orm import local_session
from base.resolvers import mutation
from orm import Shout
from orm.rbac import Resource
from orm.shout import ShoutAuthor, ShoutTopic
from orm.user import User
from base.resolvers import mutation
from resolvers.reactions import reactions_follow, reactions_unfollow
from auth.authenticate import login_required
from datetime import datetime
from services.zine.gittask import GitTask
@mutation.field("createShout")
@login_required
async def create_shout(_, info, input):
async def create_shout(_, info, inp):
user = info.context["request"].user
topic_slugs = input.get("topic_slugs", [])
topic_slugs = inp.get("topic_slugs", [])
if topic_slugs:
del input["topic_slugs"]
del inp["topic_slugs"]
new_shout = Shout.create(**input)
new_shout = Shout.create(**inp)
ShoutAuthor.create(shout=new_shout.slug, user=user.slug)
reactions_follow(user, new_shout.slug, True)
if "mainTopic" in input:
topic_slugs.append(input["mainTopic"])
if "mainTopic" in inp:
topic_slugs.append(inp["mainTopic"])
for slug in topic_slugs:
ShoutTopic.create(shout=new_shout.slug, topic=slug)
new_shout.topic_slugs = topic_slugs
GitTask(input, user.username, user.email, "new shout %s" % (new_shout.slug))
GitTask(inp, user.username, user.email, "new shout %s" % (new_shout.slug))
# await ShoutCommentsStorage.send_shout(new_shout)
@@ -40,11 +41,11 @@ async def create_shout(_, info, input):
@mutation.field("updateShout")
@login_required
async def update_shout(_, info, input):
async def update_shout(_, info, inp):
auth = info.context["request"].auth
user_id = auth.user_id
slug = input["slug"]
slug = inp["slug"]
session = local_session()
user = session.query(User).filter(User.id == user_id).first()
@@ -60,15 +61,16 @@ async def update_shout(_, info, input):
if Resource.shout_id not in scopes:
return {"error": "access denied"}
shout.update(input)
shout.update(inp)
shout.updatedAt = datetime.now()
session.add(shout)
session.commit()
session.close()
for topic in input.get("topic_slugs", []):
for topic in inp.get("topic_slugs", []):
ShoutTopic.create(shout=slug, topic=topic)
GitTask(input, user.username, user.email, "update shout %s" % (slug))
GitTask(inp, user.username, user.email, "update shout %s" % (slug))
return {"shout": shout}
@@ -89,6 +91,7 @@ async def delete_shout(_, info, slug):
for a in authors:
reactions_unfollow(a.slug, slug, True)
shout.deletedAt = datetime.now()
session.add(shout)
session.commit()
return {}

View File

@@ -1,11 +1,13 @@
from typing import List
from sqlalchemy import and_, desc
from auth.authenticate import login_required
from base.orm import local_session
from base.resolvers import query
from sqlalchemy import and_, desc
from orm.shout import Shout, ShoutAuthor, ShoutTopic
from orm.topic import TopicFollower
from orm.user import AuthorFollower
from typing import List
from services.zine.shoutscache import prepare_shouts
@@ -22,14 +24,14 @@ def get_user_feed(_, info, offset, limit) -> List[Shout]:
.where(AuthorFollower.follower == user.slug)
.order_by(desc(Shout.createdAt))
)
topicrows = (
topic_rows = (
session.query(Shout)
.join(ShoutTopic)
.join(TopicFollower)
.where(TopicFollower.follower == user.slug)
.order_by(desc(Shout.createdAt))
)
shouts = shouts.union(topicrows).limit(limit).offset(offset).all()
shouts = shouts.union(topic_rows).limit(limit).offset(offset).all()
return shouts
@@ -37,7 +39,6 @@ def get_user_feed(_, info, offset, limit) -> List[Shout]:
@login_required
async def user_unpublished_shouts(_, info, offset, limit) -> List[Shout]:
user = info.context["request"].user
shouts = []
with local_session() as session:
shouts = prepare_shouts(
session.query(Shout)
@@ -48,4 +49,4 @@ async def user_unpublished_shouts(_, info, offset, limit) -> List[Shout]:
.offset(offset)
.all()
)
return shouts
return shouts

View File

@@ -1,10 +1,11 @@
from base.resolvers import mutation, query, subscription
from auth.authenticate import login_required
import asyncio
import uuid
import json
import uuid
from datetime import datetime
from auth.authenticate import login_required
from base.redis import redis
from base.resolvers import mutation, query, subscription
class ChatFollowing:

View File

@@ -1,18 +1,21 @@
from datetime import datetime
from orm.user import User, UserRole, Role, UserRating, AuthorFollower
from services.auth.users import UserStorage
from orm.shout import Shout
from orm.reaction import Reaction
from base.orm import local_session
from orm.topic import Topic, TopicFollower
from base.resolvers import mutation, query
from resolvers.community import get_followed_communities
from resolvers.reactions import get_shout_reactions
from auth.authenticate import login_required
from resolvers.inbox import get_unread_counter
from typing import List
from sqlalchemy import and_, desc
from sqlalchemy.orm import selectinload
from typing import List
from auth.authenticate import login_required
from auth.tokenstorage import TokenStorage
from base.orm import local_session
from base.resolvers import mutation, query
from orm.reaction import Reaction
from orm.shout import Shout
from orm.topic import Topic, TopicFollower
from orm.user import User, UserRole, Role, UserRating, AuthorFollower
from resolvers.community import get_followed_communities
from resolvers.inbox import get_unread_counter
from resolvers.reactions import get_shout_reactions
from services.auth.users import UserStorage
@query.field("userReactedShouts")
@@ -87,12 +90,13 @@ async def get_user_info(slug):
@login_required
async def get_current_user(_, info):
user = info.context["request"].user
user.lastSeen = datetime.now()
with local_session() as session:
user.lastSeen = datetime.now()
user.save()
session.add(user)
session.commit()
token = await TokenStorage.create_session(user)
return {
"token": "", # same token?
"token": token,
"user": user,
"info": await get_user_info(user.slug),
}
@@ -133,7 +137,8 @@ async def update_profile(_, info, profile):
user = session.query(User).filter(User.id == user_id).first()
if user:
User.update(user, **profile)
session.commit()
session.add(user)
session.commit()
return {}

View File

@@ -1,11 +1,13 @@
from datetime import datetime
from sqlalchemy import desc
from orm.reaction import Reaction
from auth.authenticate import login_required
from base.orm import local_session
from base.resolvers import mutation, query
from orm.reaction import Reaction
from orm.shout import ShoutReactionsFollower
from orm.user import User
from base.resolvers import mutation, query
from auth.authenticate import login_required
from datetime import datetime
from services.auth.users import UserStorage
from services.stat.reacted import ReactedStorage

View File

@@ -1,12 +1,14 @@
from orm.topic import Topic, TopicFollower
from services.zine.topics import TopicStorage
from services.stat.topicstat import TopicStat
import random
from sqlalchemy import and_
from auth.authenticate import login_required
from base.orm import local_session
from base.resolvers import mutation, query
from auth.authenticate import login_required
from sqlalchemy import and_
import random
from orm.topic import Topic, TopicFollower
from services.stat.topicstat import TopicStat
from services.zine.shoutscache import ShoutsCache
from services.zine.topics import TopicStorage
@query.field("topicsAll")
@@ -60,7 +62,7 @@ async def update_topic(_, _info, inp):
async def topic_follow(user, slug):
TopicFollower.create(follower=user.slug, topic=slug)
TopicFollower.create(topic=slug, follower=user.slug)
await TopicStorage.update_topic(slug)

View File

@@ -1,18 +1,19 @@
from sqlalchemy.orm import selectinload
from sqlalchemy.sql.expression import and_, select, desc
from auth.authenticate import login_required
from base.orm import local_session
from base.resolvers import mutation, query
from orm.collection import ShoutCollection
from orm.shout import Shout, ShoutAuthor, ShoutTopic
from orm.topic import Topic
from base.orm import local_session
from base.resolvers import mutation, query
from resolvers.community import community_follow, community_unfollow
from resolvers.profile import author_follow, author_unfollow
from resolvers.reactions import reactions_follow, reactions_unfollow
from resolvers.topics import topic_follow, topic_unfollow
from services.stat.viewed import ViewedStorage
from services.zine.shoutauthor import ShoutAuthorStorage
from services.zine.shoutscache import ShoutsCache
from services.stat.viewed import ViewedStorage
from resolvers.profile import author_follow, author_unfollow
from resolvers.topics import topic_follow, topic_unfollow
from resolvers.community import community_follow, community_unfollow
from resolvers.reactions import reactions_follow, reactions_unfollow
from auth.authenticate import login_required
from sqlalchemy import select, desc, asc, and_
from sqlalchemy.orm import selectinload
@mutation.field("incrementView")
@@ -33,6 +34,12 @@ async def top_month(_, _info, offset, limit):
return ShoutsCache.top_month[offset : offset + limit]
@query.field("topCommented")
async def top_commented(_, _info, offset, limit):
async with ShoutsCache.lock:
return ShoutsCache.top_commented[offset : offset + limit]
@query.field("topOverall")
async def top_overall(_, _info, offset, limit):
async with ShoutsCache.lock:
@@ -105,7 +112,7 @@ async def get_search_results(_, _info, query, offset, limit):
for s in shouts:
for a in s.authors:
a.caption = await ShoutAuthorStorage.get_author_caption(s.slug, a.slug)
s.stat.search = 1 # FIXME
s.stat.relevance = 1 # FIXME
return shouts
@@ -116,7 +123,7 @@ async def shouts_by_topics(_, _info, slugs, offset, limit):
session.query(Shout)
.join(ShoutTopic)
.where(and_(ShoutTopic.topic.in_(slugs), bool(Shout.publishedAt)))
.order_by(asc(Shout.publishedAt))
.order_by(desc(Shout.publishedAt))
.limit(limit)
.offset(offset)
)
@@ -134,7 +141,7 @@ async def shouts_by_collection(_, _info, collection, offset, limit):
session.query(Shout)
.join(ShoutCollection, ShoutCollection.collection == collection)
.where(and_(ShoutCollection.shout == Shout.slug, bool(Shout.publishedAt)))
.order_by(asc(Shout.publishedAt))
.order_by(desc(Shout.publishedAt))
.limit(limit)
.offset(offset)
)
@@ -151,7 +158,7 @@ async def shouts_by_authors(_, _info, slugs, offset, limit):
session.query(Shout)
.join(ShoutAuthor)
.where(and_(ShoutAuthor.user.in_(slugs), bool(Shout.publishedAt)))
.order_by(asc(Shout.publishedAt))
.order_by(desc(Shout.publishedAt))
.limit(limit)
.offset(offset)
)
@@ -184,7 +191,7 @@ async def shouts_by_communities(_, info, slugs, offset, limit):
),
)
)
.order_by(desc(Shout.publishedAt))
.order_by(desc("publishedAt"))
.limit(limit)
.offset(offset)
)