Added GitHub and Google OAuth

This commit is contained in:
Mawoka
2022-06-11 16:48:24 +02:00
parent 7e12c05a9a
commit 39dce66b78
13 changed files with 480 additions and 63 deletions
+3
View File
@@ -27,6 +27,9 @@ bleach = "*"
bidict = "*" bidict = "*"
xlsxwriter = "*" xlsxwriter = "*"
pillow = ">=9.1.1" pillow = ">=9.1.1"
authlib = "*"
httpx = "*"
itsdangerous = "*"
[dev-packages] [dev-packages]
coverage = "*" coverage = "*"
Generated
+74 -4
View File
@@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "7d4797eb27534a4d9abc7ae6cb3033255b792e22a0d67204153ab7cb0b2f5715" "sha256": "ae2fc73c14a009d1476e9449a47a8d0060c551dac518176c2815edc38416c288"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@@ -224,6 +224,14 @@
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==21.4.0" "version": "==21.4.0"
}, },
"authlib": {
"hashes": [
"sha256:1286e2d5ef5bfe5a11cc2d0a0d1031f0393f6ce4d61f5121cfe87fa0054e98bd",
"sha256:6e74a4846ac36dfc882b3cc2fbd3d9eb410a627f2f2dc11771276655345223b1"
],
"index": "pypi",
"version": "==1.0.1"
},
"bidict": { "bidict": {
"hashes": [ "hashes": [
"sha256:415126d23a0c81e1a8c584a8fb1f6905ea090c772571803aeee0a2242e8e7ba0", "sha256:415126d23a0c81e1a8c584a8fb1f6905ea090c772571803aeee0a2242e8e7ba0",
@@ -319,6 +327,34 @@
"markers": "python_version >= '3.7'", "markers": "python_version >= '3.7'",
"version": "==8.1.3" "version": "==8.1.3"
}, },
"cryptography": {
"hashes": [
"sha256:093cb351031656d3ee2f4fa1be579a8c69c754cf874206be1d4cf3b542042804",
"sha256:0cc20f655157d4cfc7bada909dc5cc228211b075ba8407c46467f63597c78178",
"sha256:1b9362d34363f2c71b7853f6251219298124aa4cc2075ae2932e64c91a3e2717",
"sha256:1f3bfbd611db5cb58ca82f3deb35e83af34bb8cf06043fa61500157d50a70982",
"sha256:2bd1096476aaac820426239ab534b636c77d71af66c547b9ddcd76eb9c79e004",
"sha256:31fe38d14d2e5f787e0aecef831457da6cec68e0bb09a35835b0b44ae8b988fe",
"sha256:3b8398b3d0efc420e777c40c16764d6870bcef2eb383df9c6dbb9ffe12c64452",
"sha256:3c81599befb4d4f3d7648ed3217e00d21a9341a9a688ecdd615ff72ffbed7336",
"sha256:419c57d7b63f5ec38b1199a9521d77d7d1754eb97827bbb773162073ccd8c8d4",
"sha256:46f4c544f6557a2fefa7ac8ac7d1b17bf9b647bd20b16decc8fbcab7117fbc15",
"sha256:471e0d70201c069f74c837983189949aa0d24bb2d751b57e26e3761f2f782b8d",
"sha256:59b281eab51e1b6b6afa525af2bd93c16d49358404f814fe2c2410058623928c",
"sha256:731c8abd27693323b348518ed0e0705713a36d79fdbd969ad968fbef0979a7e0",
"sha256:95e590dd70642eb2079d280420a888190aa040ad20f19ec8c6e097e38aa29e06",
"sha256:a68254dd88021f24a68b613d8c51d5c5e74d735878b9e32cc0adf19d1f10aaf9",
"sha256:a7d5137e556cc0ea418dca6186deabe9129cee318618eb1ffecbd35bee55ddc1",
"sha256:aeaba7b5e756ea52c8861c133c596afe93dd716cbcacae23b80bc238202dc023",
"sha256:dc26bb134452081859aa21d4990474ddb7e863aa39e60d1592800a8865a702de",
"sha256:e53258e69874a306fcecb88b7534d61820db8a98655662a3dd2ec7f1afd9132f",
"sha256:ef15c2df7656763b4ff20a9bc4381d8352e6640cfeb95c2972c38ef508e75181",
"sha256:f224ad253cc9cea7568f49077007d2263efa57396a2f2f78114066fd54b5c68e",
"sha256:f8ec91983e638a9bcd75b39f1396e5c0dc2330cbd9ce4accefe68717e6779e0a"
],
"markers": "python_version >= '3.6'",
"version": "==37.0.2"
},
"databases": { "databases": {
"hashes": [ "hashes": [
"sha256:abf088900e6665952fede331cb126a1810a097fb3aad54e02a4f58521419dabf", "sha256:abf088900e6665952fede331cb126a1810a097fb3aad54e02a4f58521419dabf",
@@ -503,11 +539,19 @@
}, },
"h11": { "h11": {
"hashes": [ "hashes": [
"sha256:70813c1135087a248a4d38cc0e1a0181ffab2188141a93eaf567940c3957ff06", "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6",
"sha256:8ddd78563b633ca55346c8cd41ec0af27d3c79931828beffb46ce70a379e7442" "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"
], ],
"markers": "python_version >= '3.6'", "markers": "python_version >= '3.6'",
"version": "==0.13.0" "version": "==0.12.0"
},
"httpcore": {
"hashes": [
"sha256:1105b8b73c025f23ff7c36468e4432226cbb959176eab66864b8e31c4ee27fa6",
"sha256:18b68ab86a3ccf3e7dc0f43598eaddcf472b602aba29f9aa6ab85fe2ada3980b"
],
"markers": "python_version >= '3.7'",
"version": "==0.15.0"
}, },
"httptools": { "httptools": {
"hashes": [ "hashes": [
@@ -548,6 +592,14 @@
], ],
"version": "==0.4.0" "version": "==0.4.0"
}, },
"httpx": {
"hashes": [
"sha256:42974f577483e1e932c3cdc3cd2303e883cbfba17fe228b0f63589764d7b9c4b",
"sha256:f28eac771ec9eb4866d3fb4ab65abd42d38c424739e80c08d8d20570de60b0ef"
],
"index": "pypi",
"version": "==0.23.0"
},
"idna": { "idna": {
"hashes": [ "hashes": [
"sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff",
@@ -556,6 +608,14 @@
"markers": "python_version >= '3.5'", "markers": "python_version >= '3.5'",
"version": "==3.3" "version": "==3.3"
}, },
"itsdangerous": {
"hashes": [
"sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44",
"sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"
],
"index": "pypi",
"version": "==2.1.2"
},
"jinja2": { "jinja2": {
"hashes": [ "hashes": [
"sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852", "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852",
@@ -997,6 +1057,16 @@
"markers": "python_version >= '3.7' and python_version < '4'", "markers": "python_version >= '3.7' and python_version < '4'",
"version": "==2.28.0" "version": "==2.28.0"
}, },
"rfc3986": {
"extras": [
"idna2008"
],
"hashes": [
"sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835",
"sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"
],
"version": "==1.5.0"
},
"rsa": { "rsa": {
"hashes": [ "hashes": [
"sha256:5c6bd9dc7a543b7fe4304a631f8a8a3b674e2bbfc49c2ae96200cdbe55df6b17", "sha256:5c6bd9dc7a543b7fe4304a631f8a8a3b674e2bbfc49c2ae96200cdbe55df6b17",
+2
View File
@@ -2,6 +2,7 @@ import sentry_sdk
from fastapi import FastAPI, Request from fastapi import FastAPI, Request
from sentry_sdk.integrations.redis import RedisIntegration from sentry_sdk.integrations.redis import RedisIntegration
from socketio import ASGIApp from socketio import ASGIApp
from starlette.middleware.sessions import SessionMiddleware
from classquiz.config import settings, meilisearch from classquiz.config import settings, meilisearch
from classquiz.db import database from classquiz.db import database
@@ -42,6 +43,7 @@ async def shutdown() -> None:
await database_.disconnect() await database_.disconnect()
app.add_middleware(SessionMiddleware, secret_key=settings.secret_key)
app.include_router(users.router, tags=["users"], prefix="/api/v1/users") app.include_router(users.router, tags=["users"], prefix="/api/v1/users")
app.include_router(quiz.router, tags=["quiz"], prefix="/api/v1/quiz") app.include_router(quiz.router, tags=["quiz"], prefix="/api/v1/quiz")
app.include_router(utils.router, tags=["utils"], prefix="/api/v1/utils") app.include_router(utils.router, tags=["utils"], prefix="/api/v1/utils")
+5
View File
@@ -3,6 +3,7 @@ from functools import lru_cache
import redis.asyncio as redis_lib import redis.asyncio as redis_lib
from pydantic import BaseSettings, RedisDsn, PostgresDsn from pydantic import BaseSettings, RedisDsn, PostgresDsn
import meilisearch as MeiliSearch import meilisearch as MeiliSearch
from typing import Optional
from classquiz.storage import Storage from classquiz.storage import Storage
@@ -28,6 +29,10 @@ class Settings(BaseSettings):
sentry_dsn: str | None sentry_dsn: str | None
meilisearch_url: str meilisearch_url: str
meilisearch_index: str = "classquiz" meilisearch_index: str = "classquiz"
google_client_id: Optional[str]
google_client_secret: Optional[str]
github_client_id: Optional[str]
github_client_secret: Optional[str]
# storage_backend # storage_backend
storage_backend: str | None = "deta" storage_backend: str | None = "deta"
+13 -2
View File
@@ -4,10 +4,16 @@ from typing import Optional
import ormar import ormar
from pydantic import BaseModel, Json from pydantic import BaseModel, Json
from enum import Enum
from . import metadata, database from . import metadata, database
class UserAuthTypes(Enum):
LOCAL = "LOCAL"
GOOGLE = "GOOGLE"
GITHUB = "GITHUB"
class User(ormar.Model): class User(ormar.Model):
""" """
The user model in the database The user model in the database
@@ -16,16 +22,21 @@ class User(ormar.Model):
id: uuid.UUID = ormar.UUID(primary_key=True, default=uuid.uuid4()) id: uuid.UUID = ormar.UUID(primary_key=True, default=uuid.uuid4())
email: str = ormar.String(unique=True, max_length=100) email: str = ormar.String(unique=True, max_length=100)
username: str = ormar.String(unique=True, max_length=100) username: str = ormar.String(unique=True, max_length=100)
password: str = ormar.String(unique=True, max_length=100) password: Optional[str] = ormar.String(max_length=100, nullable=True)
verified: bool = ormar.Boolean(default=False) verified: bool = ormar.Boolean(default=False)
verify_key: str = ormar.String(unique=True, max_length=100, nullable=True) verify_key: str = ormar.String(unique=True, max_length=100, nullable=True)
created_at: datetime = ormar.DateTime(default=datetime.now()) created_at: datetime = ormar.DateTime(default=datetime.now())
auth_type: UserAuthTypes = ormar.Enum(enum_class=UserAuthTypes, default=UserAuthTypes.LOCAL)
google_uid: Optional[str] = ormar.String(unique=True, max_length=255, nullable=True)
class Meta: class Meta:
tablename = "users" tablename = "users"
metadata = metadata metadata = metadata
database = database database = database
class Config:
use_enum_values = True
class UserSession(ormar.Model): class UserSession(ormar.Model):
""" """
+6
View File
@@ -0,0 +1,6 @@
from fastapi import APIRouter
from classquiz.oauth import google, github
router = APIRouter()
router.include_router(google.router, prefix="/google")
router.include_router(github.router, prefix="/github")
+70
View File
@@ -0,0 +1,70 @@
from classquiz.db.models import User, UserSession
from fastapi import Response, Request, HTTPException
import os
import uuid
from classquiz.config import settings, redis
from datetime import timedelta, datetime
from classquiz.auth import create_access_token
settings = settings()
async def log_user_in(user: User, request: Request, response: Response):
print(user)
if user is None:
raise HTTPException(status_code=401, detail="User not matched!")
remote_ip = None
if request.headers.get("X-Forwarded-For") is None:
remote_ip = request.client.host
else:
if "," in request.headers.get("X-Forwarded-For"):
remote_ip = request.headers.get("X-Forwarded-For").split(", ")[0]
else:
remote_ip = request.headers.get("X-Forwarded-For")
session_key = os.urandom(32).hex()
user_session = UserSession(
user=user,
session_key=session_key,
ip_address=remote_ip,
user_agent=request.headers.get("User-Agent"),
id=uuid.uuid4(),
)
await user_session.save()
# await user_session.save()
access_token_expires = timedelta(minutes=settings.access_token_expire_minutes)
access_token = create_access_token(data={"sub": user.email}, expires_delta=access_token_expires)
await redis.set(access_token, user.email, ex=settings.access_token_expire_minutes * 60)
response.set_cookie(
key="access_token",
value=f"Bearer {access_token}",
httponly=True,
samesite="lax",
max_age=settings.access_token_expire_minutes * 60,
)
response.set_cookie(key="expiry", value="", max_age=settings.access_token_expire_minutes * 60)
response.set_cookie(key="rememberme", value="", max_age=60 * 60 * 24 * 365)
response.set_cookie(
key="rememberme_token", value=session_key, httponly=True, samesite="lax", max_age=60 * 60 * 24 * 365
)
return {"access_token": access_token, "token_type": "bearer"}
async def rememberme_check(rememberme_token: str, response: Response):
user_session: UserSession | None = (
await UserSession.objects.filter(session_key=rememberme_token).select_related(UserSession.user).get_or_none()
)
if (user_session is None) or (user_session.user is None):
raise HTTPException(status_code=401, detail="No user session")
access_token_expires = timedelta(minutes=settings.access_token_expire_minutes * 60)
access_token = create_access_token(data={"sub": user_session.user.email}, expires_delta=access_token_expires)
response.set_cookie(
key="access_token",
value=f"Bearer {access_token}",
httponly=True,
samesite="lax",
max_age=settings.access_token_expire_minutes * 60,
)
response.set_cookie(key="expiry", value="", max_age=settings.access_token_expire_minutes * 60)
response.status_code = 200
await user_session.update(last_seen=datetime.now())
+123
View File
@@ -0,0 +1,123 @@
from fastapi import APIRouter, Request, HTTPException, Response
from classquiz.config import settings
from authlib.integrations.starlette_client import OAuth
from classquiz.db.models import User, UserAuthTypes
from pydantic import BaseModel, ValidationError
from classquiz.auth import check_token, credentials_exception
from classquiz.oauth.authenticate_user import log_user_in, rememberme_check
from datetime import datetime
settings = settings()
router = APIRouter()
oauth = OAuth()
oauth.register(
name="github",
client_kwargs={"scope": "read:user user:email"},
access_token_url="https://github.com/login/oauth/access_token",
access_token_params=None,
authorize_url="https://github.com/login/oauth/authorize",
authorize_params=None,
api_base_url="https://api.github.com/",
client_id=settings.github_client_id,
client_secret=settings.github_client_secret,
)
class Plan(BaseModel):
name: str
space: int
collaborators: int
private_repos: int
class GitHubOauthResponse(BaseModel):
login: str
id: int
node_id: str
avatar_url: str
gravatar_id: str
url: str
html_url: str
followers_url: str
following_url: str
gists_url: str
starred_url: str
subscriptions_url: str
organizations_url: str
repos_url: str
events_url: str
received_events_url: str
type: str
site_admin: bool
name: str
company: None
blog: str
location: str
email: str
hireable: None
bio: str
twitter_username: str
public_repos: int
public_gists: int
followers: int
following: int
created_at: datetime
updated_at: datetime
private_gists: int
total_private_repos: int
owned_private_repos: int
disk_usage: int
collaborators: int
two_factor_authentication: bool
plan: Plan
@router.get("/login")
async def github_login(req: Request):
if settings.github_client_id is None or settings.github_client_secret is None:
raise HTTPException(status_code=501, detail="GitHub-Login isn't available on this server")
return await oauth.github.authorize_redirect(req, f"{settings.root_address}/api/v1/users/oauth/github/auth")
@router.get("/auth")
async def auth(request: Request, response: Response):
if settings.github_client_id is None or settings.github_client_secret is None:
raise HTTPException(status_code=501, detail="GitHub-Login isn't available on this server")
access_token = request.cookies.get("access_token")
rememberme_token = request.cookies.get("rememberme_token")
print(access_token, rememberme_token)
if access_token is not None:
try:
data = await check_token(access_token)
if data is not None:
return
except:
pass
if rememberme_token is not None:
return await rememberme_check(rememberme_token=rememberme_token, response=response)
try:
token = await oauth.github.authorize_access_token(request)
except:
raise HTTPException(status_code=401, detail="OAuth didn't work!")
resp = await oauth.github.get("user", token=token)
user_data = GitHubOauthResponse(**resp.json())
user_in_db = await User.objects.get_or_none(email=user_data.email)
if user_in_db is None:
# REGISTER USER
create_user = User(
email=user_data.email,
username=user_data.login,
verified=True,
auth_type=UserAuthTypes.GITHUB,
)
try:
await create_user.save()
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
user = await User.objects.get_or_none(
email=user_data.email, username=user_data.login, auth_type=UserAuthTypes.GITHUB, verified=True
)
return await log_user_in(user=user, request=request, response=response)
+100
View File
@@ -0,0 +1,100 @@
from fastapi import APIRouter, Request, HTTPException, Response
from classquiz.config import settings
from authlib.integrations.starlette_client import OAuth
from classquiz.db.models import User, UserAuthTypes
from pydantic import BaseModel, ValidationError
from classquiz.auth import check_token, credentials_exception
from classquiz.oauth.authenticate_user import log_user_in, rememberme_check
settings = settings()
router = APIRouter()
oauth = OAuth()
oauth.register(
name="github",
server_metadata_url="https://accounts.google.com/.well-known/openid-configuration",
client_kwargs={"scope": "openid email profile"},
client_id=settings.google_client_id,
client_secret=settings.google_client_secret,
)
class Userinfo(BaseModel):
iss: str
azp: str
aud: str
sub: str
email: str
email_verified: bool
at_hash: str
nonce: str
name: str
picture: str
given_name: str
locale: str
iat: int
exp: int
class OauthGoogleResponse(BaseModel):
access_token: str
expires_in: int
scope: str
token_type: str
id_token: str
expires_at: int
userinfo: Userinfo
@router.get("/login")
async def google_login(req: Request):
if settings.google_client_secret is None or settings.google_client_id is None:
raise HTTPException(status_code=501, detail="Google-Login isn't available on this server")
return await oauth.google.authorize_redirect(req, f"{settings.root_address}/api/v1/users/oauth/google/auth")
@router.get("/auth")
async def auth(request: Request, response: Response):
if settings.google_client_secret is None or settings.google_client_id is None:
raise HTTPException(status_code=501, detail="Google-Login isn't available on this server")
access_token = request.cookies.get("access_token")
rememberme_token = request.cookies.get("rememberme_token")
print(rememberme_token, access_token)
if access_token is not None:
try:
data = await check_token(access_token)
if data is not None:
return
except:
pass
if rememberme_token is not None:
return await rememberme_check(rememberme_token=rememberme_token, response=response)
try:
user_data = await oauth.google.authorize_access_token(request)
except:
raise HTTPException(status_code=401, detail="OAuth didn't work!")
try:
user_data = OauthGoogleResponse(**user_data).userinfo
except (TypeError, ValidationError):
raise HTTPException(status_code=401, detail="Something went wrong.")
user_in_db = await User.objects.get_or_none(email=user_data.email)
if user_in_db is None:
# REGISTER USER
create_user = User(
email=user_data.email,
username=user_data.name,
verified=user_data.email_verified,
auth_type=UserAuthTypes.GOOGLE,
google_uid=user_data.sub,
)
try:
await create_user.save()
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
user = await User.objects.get_or_none(
email=user_data.email, google_uid=user_data.sub, auth_type=UserAuthTypes.GOOGLE, verified=True
)
return await log_user_in(user=user, request=request, response=response)
+8 -56
View File
@@ -8,6 +8,8 @@ from fastapi.background import BackgroundTasks
from fastapi.responses import JSONResponse, RedirectResponse from fastapi.responses import JSONResponse, RedirectResponse
from fastapi.security import OAuth2PasswordRequestForm from fastapi.security import OAuth2PasswordRequestForm
import html import html
from classquiz import oauth
from classquiz.oauth.authenticate_user import rememberme_check, log_user_in
from classquiz.auth import ( from classquiz.auth import (
get_password_hash, get_password_hash,
@@ -37,9 +39,12 @@ async def _sign_out_everywhere(user: User) -> None:
await clear_cache_for_account(user) await clear_cache_for_account(user)
router.include_router(oauth.router, tags=["users", "oauth"], prefix="/oauth")
@router.post( @router.post(
"/create", "/create",
response_model=User, # response_model=User,
response_model_include={"id": ..., "verified": ..., "email": ...}, response_model_include={"id": ..., "verified": ..., "email": ...},
) )
async def create_user(user: route_user, background_task: BackgroundTasks) -> User | JSONResponse: async def create_user(user: route_user, background_task: BackgroundTasks) -> User | JSONResponse:
@@ -77,41 +82,7 @@ async def login_for_cookie_access_token(
detail="Incorrect username or password", detail="Incorrect username or password",
) )
remote_ip = None return await log_user_in(response=response, request=request, user=user)
if request.headers.get("X-Forwarded-For") is None:
remote_ip = request.client.host
else:
if "," in request.headers.get("X-Forwarded-For"):
remote_ip = request.headers.get("X-Forwarded-For").split(", ")[0]
else:
remote_ip = request.headers.get("X-Forwarded-For")
session_key = os.urandom(32).hex()
user_session = UserSession(
user=user,
session_key=session_key,
ip_address=remote_ip,
user_agent=request.headers.get("User-Agent"),
id=uuid.uuid4(),
)
await user_session.save()
# await user_session.save()
access_token_expires = timedelta(minutes=settings.access_token_expire_minutes)
access_token = create_access_token(data={"sub": user.email}, expires_delta=access_token_expires)
await redis.set(access_token, user.email, ex=settings.access_token_expire_minutes * 60)
response.set_cookie(
key="access_token",
value=f"Bearer {access_token}",
httponly=True,
samesite="strict",
max_age=settings.access_token_expire_minutes * 60,
)
response.set_cookie(key="expiry", value="", max_age=settings.access_token_expire_minutes * 60)
response.set_cookie(key="rememberme", value="", max_age=60 * 60 * 24 * 365)
response.set_cookie(
key="rememberme_token", value=session_key, httponly=True, samesite="strict", max_age=60 * 60 * 24 * 365
)
return {"access_token": access_token, "token_type": "bearer"}
@router.get("/token/rememberme") @router.get("/token/rememberme")
@@ -119,26 +90,7 @@ async def rememberme_token(request: Request, response: Response):
rememberme_token_lol = request.cookies.get("rememberme_token") rememberme_token_lol = request.cookies.get("rememberme_token")
if rememberme_token_lol is None: if rememberme_token_lol is None:
raise HTTPException(status_code=400, detail="No rememberme cookie") raise HTTPException(status_code=400, detail="No rememberme cookie")
user_session: UserSession | None = ( return await rememberme_check(rememberme_token=rememberme_token_lol, response=response)
await UserSession.objects.filter(session_key=rememberme_token_lol)
.select_related(UserSession.user)
.get_or_none()
)
if (user_session is None) or (user_session.user is None):
raise HTTPException(status_code=401, detail="No user session")
access_token_expires = timedelta(minutes=settings.access_token_expire_minutes * 60)
access_token = create_access_token(data={"sub": user_session.user.email}, expires_delta=access_token_expires)
response.set_cookie(
key="access_token",
value=f"Bearer {access_token}",
httponly=True,
samesite="strict",
max_age=settings.access_token_expire_minutes * 60,
)
response.set_cookie(key="expiry", value="", max_age=settings.access_token_expire_minutes * 60)
response.status_code = 200
await user_session.update(last_seen=datetime.now())
return response
@router.get("/logout") @router.get("/logout")
@@ -0,0 +1,43 @@
"""added google_oauth
Revision ID: 0c081a52ab8a
Revises: 6dc09ad6f6ef
Create Date: 2022-06-11 14:55:03.312529
"""
from alembic import op
import sqlalchemy as sa
import ormar
# revision identifiers, used by Alembic.
revision = "0c081a52ab8a"
down_revision = "6dc09ad6f6ef"
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.execute("CREATE TYPE userauthtypes AS ENUM ('LOCAL', 'GOOGLE');")
op.create_unique_constraint(None, "quiz", ["id"])
op.add_column(
"users",
sa.Column("auth_type", sa.Enum("LOCAL", "GOOGLE", name="userauthtypes"), nullable=True, server_default="LOCAL"),
)
op.add_column("users", sa.Column("google_uid", sa.String(length=255), nullable=True))
op.alter_column("users", "password", existing_type=sa.VARCHAR(length=100), nullable=True)
op.drop_constraint("users_password_key", "users", type_="unique")
op.create_unique_constraint(None, "users", ["google_uid"])
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint("users_google_uid_key", "users", type_="unique")
op.create_unique_constraint("users_password_key", "users", ["password"])
op.alter_column("users", "password", existing_type=sa.VARCHAR(length=100), nullable=False)
op.drop_column("users", "google_uid")
op.drop_column("users", "auth_type")
op.drop_constraint("quiz_id_key", "quiz", type_="unique")
op.execute("DROP TYPE IF EXISTS userauthtypes;")
# ### end Alembic commands ###
@@ -0,0 +1,29 @@
"""added github_oauth
Revision ID: da778d551bf4
Revises: 0c081a52ab8a
Create Date: 2022-06-11 16:41:35.761391
"""
from alembic import op
import sqlalchemy as sa
import ormar
# revision identifiers, used by Alembic.
revision = "da778d551bf4"
down_revision = "0c081a52ab8a"
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.execute("ALTER TYPE userauthtypes ADD VALUE 'GITHUB';")
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.execute("ALTER TYPE userauthtypes DROP VALUE 'GITHUB';")
# ### end Alembic commands ###
+4 -1
View File
@@ -5,12 +5,15 @@ run_tests() {
stop() { stop() {
docker container stop test_redis docker container stop test_redis
docker container stop test_meili docker container stop test_meili
docker container stop classquiz_db
} }
init() { init() {
mkdir /tmp/storage mkdir /tmp/storage
docker run --rm -d -p 6379:6379 --name test_redis redis:alpine docker run --rm -d -p 6379:6379 --name test_redis redis:alpine
docker run -it --rm -d -p 7700:7700 --name test_meili getmeili/meilisearch:latest docker run -it --rm -d -p 7700:7700 --name test_meili getmeili/meilisearch:latest
docker volume create classquiz_db_data
docker run --name classquiz_db -p 5432:5432 --rm -d -e POSTGRES_PASSWORD=mysecretpassword -v classquiz_db_data:/var/lib/postgresql/data -e POSTGRES_DB=classquiz postgres
pipenv run alembic upgrade head pipenv run alembic upgrade head
} }
@@ -18,7 +21,7 @@ case $1 in
+) init ;; +) init ;;
-) stop ;; -) stop ;;
a) a)
rm classquiz.db docker volume rm classquiz_db_data
init init
run_tests run_tests
stop stop