Refactor auth into user.py, Implement login/out in frontend, Add FontAwesome

This commit is contained in:
Kai Vogelgesang 2022-09-05 18:50:02 +02:00
parent 563eac3de4
commit 6f7279c049
Signed by: kai
GPG Key ID: 0A95D3B6E62C0879
8 changed files with 189 additions and 41 deletions

View File

@ -8,6 +8,7 @@
"name": "frontend", "name": "frontend",
"version": "0.0.0", "version": "0.0.0",
"devDependencies": { "devDependencies": {
"@fortawesome/fontawesome-free": "^6.2.0",
"@sveltejs/vite-plugin-svelte": "^1.0.1", "@sveltejs/vite-plugin-svelte": "^1.0.1",
"@tsconfig/svelte": "^3.0.0", "@tsconfig/svelte": "^3.0.0",
"bulma": "^0.9.4", "bulma": "^0.9.4",
@ -36,6 +37,16 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/@fortawesome/fontawesome-free": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.2.0.tgz",
"integrity": "sha512-CNR7qRIfCwWHNN7FnKUniva94edPdyQzil/zCwk3v6k4R6rR2Fr8i4s3PM7n/lyfPA6Zfko9z5WDzFxG9SW1uQ==",
"dev": true,
"hasInstallScript": true,
"engines": {
"node": ">=6"
}
},
"node_modules/@jridgewell/resolve-uri": { "node_modules/@jridgewell/resolve-uri": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
@ -1492,6 +1503,12 @@
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"@fortawesome/fontawesome-free": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.2.0.tgz",
"integrity": "sha512-CNR7qRIfCwWHNN7FnKUniva94edPdyQzil/zCwk3v6k4R6rR2Fr8i4s3PM7n/lyfPA6Zfko9z5WDzFxG9SW1uQ==",
"dev": true
},
"@jridgewell/resolve-uri": { "@jridgewell/resolve-uri": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",

View File

@ -10,15 +10,16 @@
"check": "svelte-check --tsconfig ./tsconfig.json" "check": "svelte-check --tsconfig ./tsconfig.json"
}, },
"devDependencies": { "devDependencies": {
"@fortawesome/fontawesome-free": "^6.2.0",
"@sveltejs/vite-plugin-svelte": "^1.0.1", "@sveltejs/vite-plugin-svelte": "^1.0.1",
"@tsconfig/svelte": "^3.0.0", "@tsconfig/svelte": "^3.0.0",
"bulma": "^0.9.4",
"sass": "^1.53.0",
"svelte": "^3.49.0", "svelte": "^3.49.0",
"svelte-check": "^2.8.0", "svelte-check": "^2.8.0",
"svelte-preprocess": "^4.10.7", "svelte-preprocess": "^4.10.7",
"tslib": "^2.4.0", "tslib": "^2.4.0",
"typescript": "^4.6.4", "typescript": "^4.6.4",
"vite": "^3.0.7", "vite": "^3.0.7"
"sass": "^1.53.0",
"bulma": "^0.9.4"
} }
} }

View File

@ -1,8 +1,17 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte";
import { user } from "./stores";
import Navbar from "./Navbar.svelte"; import Navbar from "./Navbar.svelte";
onMount(async () => {
const res = await fetch("/user/me");
$user = await res.json();
});
</script> </script>
<Navbar /> <Navbar />
<section class="section"> <section class="section">
Hallo i bims 1 frontend <p>Hallo i bims 1 frontend</p>
</section> </section>

View File

@ -1,13 +1,71 @@
<script lang="ts"> <script lang="ts">
import logo from "./assets/turtle.png"; import logo from "./assets/turtle.png";
import { user } from "./stores";
let menu_open = false;
</script> </script>
<nav class="navbar"> <nav class="navbar">
<div class="navbar-brand"> <div class="navbar-brand">
<div class="navbar-item"> <a class="navbar-item" href="/">
<a href="/"> <span class="icon-text">
<img src={logo} alt="logo"/> <img class="image is-24x24" src={logo} alt="logo" />
</a> <span><strong>Control Panel</strong></span>
</span>
</a>
<!-- svelte-ignore a11y-missing-attribute -->
<a
role="button"
class="navbar-burger {menu_open ? 'is-active' : ''}"
on:click={() => (menu_open = !menu_open)}
aria-label="menu"
aria-expanded="false"
>
<span aria-hidden="true" />
<span aria-hidden="true" />
<span aria-hidden="true" />
</a>
</div>
<div class="navbar-menu {menu_open ? 'is-active' : ''}">
<div class="navbar-start">
<a class="navbar-item" href="/foo"> Foo </a>
<a class="navbar-item" href="/bar"> Bar </a>
</div>
<div class="navbar-end">
{#if $user !== null}
<div class="navbar-item has-dropdown is-hoverable">
<!-- svelte-ignore a11y-missing-attribute -->
<a class="navbar-link">
<img
class="image is-24x24 mr-2"
src={$user.picture}
alt="{$user.name}'s picture"
/>
<span>{$user.name}</span>
</a>
<div class="navbar-dropdown is-right">
<div class="navbar-item">
<p>{$user.email}</p>
</div>
<hr class="navbar-divider" />
<a
href="/user/logout"
class="navbar-item has-text-danger"
>
Log out
</a>
</div>
</div>
{:else}
<div class="navbar-item">
<a class="button is-primary" href="/user/login">
<strong>Log in</strong>
</a>
</div>
{/if}
</div> </div>
</div> </div>
</nav> </nav>

View File

@ -9,3 +9,10 @@
@import "bulma/sass/grid/_all"; @import "bulma/sass/grid/_all";
@import "bulma/sass/helpers/_all"; @import "bulma/sass/helpers/_all";
@import "bulma/sass/layout/_all"; @import "bulma/sass/layout/_all";
$fa-font-path: "@fortawesome/fontawesome-free/webfonts";
@import "@fortawesome/fontawesome-free/scss/fontawesome.scss";
@import "@fortawesome/fontawesome-free/scss/regular.scss";
@import "@fortawesome/fontawesome-free/scss/solid.scss";
@import "@fortawesome/fontawesome-free/scss/brands.scss";
@import "@fortawesome/fontawesome-free/scss/v4-shims.scss";

9
frontend/src/stores.ts Normal file
View File

@ -0,0 +1,9 @@
import { writable, type Writable } from "svelte/store";
type User = {
name: string,
email: string,
picture: string,
}
export const user: Writable<User | null> = writable(null);

View File

@ -7,18 +7,11 @@ from starlette.responses import HTMLResponse, RedirectResponse
from authlib.integrations.starlette_client import OAuth, OAuthError from authlib.integrations.starlette_client import OAuth, OAuthError
from .settings import settings from .settings import settings
from .user import user_auth
config = Config("secret.env") # TODO unify this with settings
oauth = OAuth(config)
oauth.register(
name="gitea",
server_metadata_url="https://git.leafbla.de/.well-known/openid-configuration",
)
app = FastAPI() app = FastAPI()
app.add_middleware(SessionMiddleware, secret_key=config.get("SESSION_SECRET_KEY"))
app.mount("/user/", user_auth)
templates = Jinja2Templates(directory="templates") templates = Jinja2Templates(directory="templates")
@ -38,29 +31,5 @@ async def index(request: Request):
) )
@app.get("/login")
async def login(request: Request):
redirect_uri = request.url_for("auth")
return await oauth.gitea.authorize_redirect(request, redirect_uri)
@app.get("/auth")
async def auth(request: Request):
try:
token = await oauth.gitea.authorize_access_token(request)
except OAuthError as e:
return HTMLResponse(f"<h1>{e.error}</h1>")
user = await oauth.gitea.userinfo(token=token)
if user:
request.session["user"] = dict(user)
return RedirectResponse(url="/")
@app.get("/logout")
async def logout(request: Request):
request.session.pop("user", None)
return RedirectResponse(url="/")
if not settings.dev_mode: if not settings.dev_mode:
app.mount("/", StaticFiles(directory=settings.frontend_path)) app.mount("/", StaticFiles(directory=settings.frontend_path))

78
server/server/user.py Normal file
View File

@ -0,0 +1,78 @@
from fastapi import FastAPI, Request
from pydantic import BaseModel, HttpUrl
from starlette.config import Config
from starlette.middleware.sessions import SessionMiddleware
from starlette.responses import HTMLResponse, RedirectResponse
from authlib.integrations.starlette_client import OAuth, OAuthError
config = Config("secret.env") # TODO unify this with settings
oauth = OAuth(config)
oauth.register(
name="gitea",
server_metadata_url="https://git.leafbla.de/.well-known/openid-configuration",
)
user_auth = FastAPI()
user_auth.add_middleware(SessionMiddleware, secret_key=config.get("SESSION_SECRET_KEY"))
@user_auth.get("/login")
async def login(request: Request):
redirect_uri = request.url_for("auth")
return await oauth.gitea.authorize_redirect(request, redirect_uri)
@user_auth.get("/auth")
async def auth(request: Request):
try:
token = await oauth.gitea.authorize_access_token(request)
except OAuthError as e:
return HTMLResponse(f"<h1>{e.error}</h1>")
user = await oauth.gitea.userinfo(token=token)
if user:
request.session["user"] = dict(user)
return RedirectResponse(url="/")
@user_auth.get("/logout")
async def logout(request: Request):
request.session.pop("user", None)
return RedirectResponse(url="/")
"""
user={
'sub': '1',
'name': 'Kai Vogelgesang',
'preferred_username': 'kai',
'email': 'kai@leafbla.de',
'picture': 'https://git.leafbla.de/avatars/279a0c06517e4dd112b291cf78c0c659',
'groups': [
'gitolite-legacy',
'gitolite-legacy:owners',
'infrastructure',
'infrastructure:owners',
'next-website',
'next-website:owners',
'turtles',
'turtles:owners'
]
} """
class MeResponse(BaseModel):
name: str
email: str
picture: HttpUrl
@user_auth.get("/me", response_model=MeResponse | None)
async def display_current_user(request: Request):
user = request.session.get("user")
if user is None:
return None
print(f"[/me] {user=}")
return MeResponse(
name=user["preferred_username"], email=user["email"], picture=user["picture"]
)