fix CSRF, more user routes

This commit is contained in:
Yusur 2025-09-11 20:49:04 +02:00
parent b6fa88f201
commit aa9adb075a
2 changed files with 61 additions and 21 deletions

View file

@ -39,6 +39,7 @@ class AppConfig(ConfigOptions):
app_name = ConfigValue() app_name = ConfigValue()
server_name = ConfigValue() server_name = ConfigValue()
private_assets = ConfigValue(cast=ssv_list) private_assets = ConfigValue(cast=ssv_list)
# deprecated
jquery_url = ConfigValue(default='https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js') jquery_url = ConfigValue(default='https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js')
app_is_behind_proxy = ConfigValue(cast=int, default=0) app_is_behind_proxy = ConfigValue(cast=int, default=0)
impressum = ConfigValue(cast=twocolon_list, default='') impressum = ConfigValue(cast=twocolon_list, default='')
@ -142,6 +143,7 @@ async def _load_user():
logger.error(f'{e}') logger.error(f'{e}')
g.no_user = True g.no_user = True
@app.after_request @app.after_request
async def _unload_user(resp): async def _unload_user(resp):
try: try:

View file

@ -13,17 +13,21 @@ from werkzeug.security import check_password_hash
from suou.quart import add_rest from suou.quart import add_rest
from freak.accounts import LoginStatus, check_login from freak.accounts import LoginStatus, check_login
from freak.algorithms import topic_timeline from freak.algorithms import topic_timeline, user_timeline
from ..models import Guild, Post, User, db from ..models import Guild, Post, User, db
from .. import UserLoader, app, app_config, __version__ as freak_version from .. import UserLoader, app, app_config, __version__ as freak_version, csrf
bp = Blueprint('rest', __name__, url_prefix='/v1') bp = Blueprint('rest', __name__, url_prefix='/v1')
rest = add_rest(app, '/v1', '/ajax') rest = add_rest(app, '/v1', '/ajax')
## XXX potential security hole, but needed for REST to work
csrf.exempt(bp)
current_user: UserLoader current_user: UserLoader
## TODO deprecate auth_required since it does not work ## TODO deprecate auth_required since it does not work
## will be removed in 0.6
from suou.flask_sqlalchemy import require_auth from suou.flask_sqlalchemy import require_auth
auth_required = deprecated('use login_required() and current_user instead')(require_auth(User, db)) auth_required = deprecated('use login_required() and current_user instead')(require_auth(User, db))
@ -37,11 +41,22 @@ async def get_nurupo():
@bp.get('/health') @bp.get('/health')
async def health(): async def health():
async with db as session:
hi = dict(
version=freak_version,
name = app_config.app_name,
post_count = await Post.count(),
user_count = await User.active_count(),
me = Snowflake(current_user.id).to_b32l() if current_user else None
)
return hi
@bp.get('/oath')
async def oath():
return dict( return dict(
version=freak_version, ## XXX might break any time!
name = app_config.app_name, csrf_token= await csrf._get_csrf_token()
post_count = await Post.count(),
user_count = await User.active_count()
) )
## TODO coverage of REST is still partial, but it's planned ## TODO coverage of REST is still partial, but it's planned
@ -56,16 +71,10 @@ async def health():
@bp.get('/user/@me') @bp.get('/user/@me')
@login_required @login_required
async def get_user_me(): async def get_user_me():
return redirect(url_for(f'rest.user_get', current_user.id)), 302 return redirect(url_for(f'rest.user_get', id=current_user.id)), 302
@bp.get('/user/<b32l:id>') def _user_info(u: User):
async def user_get(id: int): return dict(
## TODO sanizize REST to make blocked users inaccessible
async with db as session:
u: User | None = (await session.execute(select(User).where(User.id == id))).scalar()
if u is None:
return dict(error='User not found'), 404
uj = dict(
id = f'{Snowflake(u.id):l}', id = f'{Snowflake(u.id):l}',
username = u.username, username = u.username,
display_name = u.display_name, display_name = u.display_name,
@ -75,8 +84,33 @@ async def user_get(id: int):
biography=u.biography, biography=u.biography,
badges = u.badges() badges = u.badges()
) )
@bp.get('/user/<b32l:id>')
async def user_get(id: int):
## TODO sanizize REST to make blocked users inaccessible
async with db as session:
u: User | None = (await session.execute(select(User).where(User.id == id))).scalar()
if u is None:
return dict(error='User not found'), 404
uj = _user_info(u)
return dict(users={f'{Snowflake(id):l}': uj}) return dict(users={f'{Snowflake(id):l}': uj})
@bp.get('/user/<b32l:id>/feed')
async def user_feed_get(id: int):
async with db as session:
u: User | None = (await session.execute(select(User).where(User.id == id))).scalar()
if u is None:
return dict(error='User not found'), 404
uj = _user_info(u)
feed = []
algo = user_timeline(u)
posts = await db.paginate(algo)
async for p in posts:
feed.append(p.feed_info())
return dict(users={f'{Snowflake(id):l}': uj}, feed=feed)
@bp.get('/user/@<username>') @bp.get('/user/@<username>')
async def resolve_user(username: str): async def resolve_user(username: str):
async with db as session: async with db as session:
@ -85,6 +119,14 @@ async def resolve_user(username: str):
abort(404, 'User not found') abort(404, 'User not found')
return redirect(url_for('rest.user_get', id=uid)), 302 return redirect(url_for('rest.user_get', id=uid)), 302
@bp.get('/user/@<username>/feed')
async def resolve_user_feed(username: str):
async with db as session:
uid: User | None = (await session.execute(select(User.id).select_from(User).where(User.username == username))).scalar()
if uid is None:
abort(404, 'User not found')
return redirect(url_for('rest.user_feed_get', id=uid)), 302
## POSTS ## ## POSTS ##
@bp.get('/post/<b32l:id>') @bp.get('/post/<b32l:id>')
@ -138,8 +180,6 @@ async def guild_feed(gname: str):
if gu is None: if gu is None:
return dict(error='Not found'), 404 return dict(error='Not found'), 404
gj = await _guild_info(gu) gj = await _guild_info(gu)
# TODO add feed
feed = [] feed = []
algo = topic_timeline(gname) algo = topic_timeline(gname)
posts = await db.paginate(algo) posts = await db.paginate(algo)
@ -158,8 +198,6 @@ class LoginIn(BaseModel):
@bp.post('/login') @bp.post('/login')
@validate_request(LoginIn) @validate_request(LoginIn)
async def login(data: LoginIn): async def login(data: LoginIn):
print(data)
async with db as session: async with db as session:
u = (await session.execute(select(User).where(User.username == data.username))).scalar() u = (await session.execute(select(User).where(User.username == data.username))).scalar()
match check_login(u, data.password): match check_login(u, data.password):
@ -169,7 +207,7 @@ async def login(data: LoginIn):
login_user(UserLoader(u.get_id()), remember=True) login_user(UserLoader(u.get_id()), remember=True)
else: else:
login_user(UserLoader(u.get_id())) login_user(UserLoader(u.get_id()))
return {'id': f'{Snowflake(u.id):l}'}, 204 return {'id': f'{Snowflake(u.id):l}'}, 200
case LoginStatus.ERROR: case LoginStatus.ERROR:
abort(404, 'Invalid username or password') abort(404, 'Invalid username or password')
case LoginStatus.SUSPENDED: case LoginStatus.SUSPENDED:
@ -182,5 +220,5 @@ async def login(data: LoginIn):
@login_required @login_required
async def logout(): async def logout():
logout_user() logout_user()
return {}, 204 return '', 204