From 7a3228d3ce274d216cb13532314315a7e8dd1260 Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Wed, 06 Oct 2021 00:37:38 +0100 Subject: [PATCH] Frontend for OAuth --- Cargo.lock | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ chartered-web/Cargo.toml | 5 ++++- chartered-db/src/users.rs | 7 +++++-- chartered-frontend/src/index.tsx | 8 +++++++- chartered-frontend/src/useAuth.tsx | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ chartered-web/src/main.rs | 15 +++++++++++++++ chartered-frontend/src/pages/Login.tsx | 102 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------- 7 files changed, 235 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 733132f..2ad5784 100644 --- a/Cargo.lock +++ a/Cargo.lock @@ -334,6 +334,7 @@ "chartered-fs", "chartered-types", "chrono", + "clap", "env_logger", "futures", "headers", @@ -350,6 +351,7 @@ "sha2", "thiserror", "tokio", + "toml", "tower", "tower-http 0.1.1 (git+https://github.com/tower-rs/tower-http?branch=cors)", ] @@ -378,6 +380,37 @@ ] [[package]] +name = "clap" +version = "3.0.0-beta.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcd70aa5597dbc42f7217a543f9ef2768b2ef823ba29036072d30e1d88e98406" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "indexmap", + "lazy_static", + "os_str_bytes", + "strsim", + "termcolor", + "textwrap", + "vec_map", +] + +[[package]] +name = "clap_derive" +version = "3.0.0-beta.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5bb0d655624a0b8770d1c178fb8ffcb1f91cc722cb08f451e3dc72465421ac" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] name = "const-sha1" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1346,6 +1379,12 @@ "heck", "serde", ] + +[[package]] +name = "os_str_bytes" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6acbef58a60fe69ab50510a55bc8cdd4d6cf2283d27ad338f54cb52747a9cf2d" [[package]] name = "parking_lot" @@ -1841,6 +1880,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "subtle" @@ -1886,6 +1931,15 @@ checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" dependencies = [ "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" +dependencies = [ + "unicode-width", ] [[package]] @@ -2064,6 +2118,15 @@ "log", "pin-project-lite", "tokio", +] + +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "serde", ] [[package]] @@ -2191,6 +2254,12 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" + +[[package]] +name = "unicode-width" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" [[package]] name = "unicode-xid" @@ -2287,6 +2356,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" [[package]] name = "version_check" diff --git a/chartered-web/Cargo.toml b/chartered-web/Cargo.toml index 7ef5394..623d7f5 100644 --- a/chartered-web/Cargo.toml +++ a/chartered-web/Cargo.toml @@ -1,5 +1,6 @@ [package] name = "chartered-web" +authors = ["Jordan Doyle "] version = "0.1.0" edition = "2018" @@ -15,6 +16,7 @@ bytes = "1" chacha20poly1305 = { version = "0.9", features = ["std"] } chrono = { version = "0.4", features = ["serde"] } +clap = { version = "3.0.0-beta.4", features = ["std", "suggestions", "color"] } env_logger = "0.9" futures = "0.3" headers = "0.3" @@ -23,7 +25,7 @@ nom = "7" once_cell = "1.8" openid = "0.9" -rand = "*" +rand = "0.8" regex = "1.5" reqwest = "0.11" serde = { version = "1", features = ["derive"] } @@ -34,3 +36,4 @@ tower = { version = "0.4", features = ["util", "filter"] } # tower-http = { version = "0.1", features = ["trace", "set-header"] } tower-http = { git = "https://github.com/tower-rs/tower-http", branch = "cors", features = ["trace", "set-header", "cors"] } +toml = "0.5" diff --git a/chartered-db/src/users.rs b/chartered-db/src/users.rs index 2470f2f..9b92cec 100644 --- a/chartered-db/src/users.rs +++ a/chartered-db/src/users.rs @@ -114,7 +114,7 @@ } pub async fn find_or_create(conn: ConnectionPool, given_username: String) -> Result { - use crate::schema::users::dsl::username; + use crate::schema::users::dsl::{username, uuid}; tokio::task::spawn_blocking(move || { let conn = conn.get()?; @@ -129,7 +129,10 @@ } diesel::insert_into(users::table) - .values(username.eq(&given_username)) + .values(( + username.eq(&given_username), + uuid.eq(SqlUuid::random()) + )) .execute(&conn)?; Ok(crate::schema::users::table diff --git a/chartered-frontend/src/index.tsx b/chartered-frontend/src/index.tsx index 51d6620..0e800f6 100644 --- a/chartered-frontend/src/index.tsx +++ a/chartered-frontend/src/index.tsx @@ -12,7 +12,7 @@ useLocation, } from "react-router-dom"; -import { ProvideAuth, useAuth } from "./useAuth"; +import { ProvideAuth, HandleOAuthLogin, useAuth } from "./useAuth"; import Login from "./pages/Login"; import Dashboard from "./pages/Dashboard"; @@ -33,6 +33,12 @@ unauthedOnly path="/login" component={() => } + /> + } /> Promise; + oauthLogin: (provider: string) => Promise; logout: () => Promise; getAuthKey: () => Promise; + setAuth: ([string, string]) => any; } const authContext = createContext(null); @@ -19,6 +23,34 @@ return {children}; } +export function HandleOAuthLogin() { + const location = useLocation(); + const auth = useAuth(); + const [result, setResult] = useState(null); + + useEffect(async () => { + try { + let result = await fetch(unauthenticatedEndpoint(`login/oauth/complete${location.search}`)); + let json = await result.json(); + + if (json.error) { + throw new Error(json.error); + } + + auth.setAuth([json.key, new Date(json.expires)]); + } catch (err) { + setResult( + + ); + } + }); + + return result ?? ; +} + export const useAuth = (): AuthContext | null => { return useContext(authContext); }; @@ -53,6 +85,23 @@ setAuth([json.key, new Date(json.expires)]); }; + + const oauthLogin = async (provider: string) => { + let res = await fetch(unauthenticatedEndpoint(`login/oauth/${provider}/begin`), { + method: "GET", + headers: { + "Content-Type": "application/json", + "User-Agent": window.navigator.userAgent, + } + }); + let json = await res.json(); + + if (json.error) { + throw new Error(json.error); + } + + window.location.href = json.redirect_url; + } const logout = async () => { // todo call the service so we can purge the key from the db @@ -71,6 +120,8 @@ login, logout, getAuthKey, + oauthLogin, + setAuth, }; } diff --git a/chartered-web/src/main.rs b/chartered-web/src/main.rs index 37fc215..af12155 100644 --- a/chartered-web/src/main.rs +++ a/chartered-web/src/main.rs @@ -13,7 +13,19 @@ use std::sync::Arc; use tower::ServiceBuilder; use tower_http::cors::{Any, CorsLayer}; +use clap::Clap; +use std::path::PathBuf; +#[derive(Clap)] +#[clap(version = clap::crate_version!(), author = clap::crate_authors!())] +#[clap(setting = clap::AppSettings::ColoredHelp)] +pub struct Opts { + #[clap(short, long, parse(from_occurrences))] + verbose: i32, + #[clap(short, long)] + config: PathBuf, +} + #[allow(clippy::unused_async)] async fn hello_world() -> &'static str { "hello, world!" @@ -45,6 +57,9 @@ #[tokio::main] #[allow(clippy::semicolon_if_nothing_returned)] // lint breaks with tokio::main async fn main() { + let opts: Opts = Opts::parse(); + let config: config::Config = toml::from_slice(&std::fs::read(&opts.config).unwrap()).unwrap(); + env_logger::init(); let pool = chartered_db::init().unwrap(); diff --git a/chartered-frontend/src/pages/Login.tsx b/chartered-frontend/src/pages/Login.tsx index 0a4eb38..41ab0c1 100644 --- a/chartered-frontend/src/pages/Login.tsx +++ a/chartered-frontend/src/pages/Login.tsx @@ -1,21 +1,31 @@ import React = require("react"); import { useState, useEffect, useRef } from "react"; +import { useLocation } from "react-router-dom"; import { useAuth } from "../useAuth"; import { useUnauthenticatedRequest } from "../util"; +interface OAuthProviders { + providers: string[]; +} + export default function Login() { + const location = useLocation(); const auth = useAuth(); const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const [error, setError] = useState(""); - const [loading, setLoading] = useState(false); + const [loading, setLoading] = useState(null); const isMountedRef = useRef(null); - const { response: oauthProviders } = useUnauthenticatedRequest({ endpoint: "login/oauth/providers" }); + const { response: oauthProviders } = useUnauthenticatedRequest({ endpoint: "login/oauth/providers" }); useEffect(() => { + if (location.state?.error) { + setError(location.state.error); + } + isMountedRef.current = true; return () => (isMountedRef.current = false); }); @@ -24,7 +34,7 @@ evt.preventDefault(); setError(""); - setLoading(true); + setLoading("password"); try { await auth.login(username, password); @@ -32,10 +42,25 @@ setError(e.message); } finally { if (isMountedRef.current) { - setLoading(false); + setLoading(null); } } }; + + const handleOAuthLogin = async (provider) => { + setError(""); + setLoading(provider); + + try { + await auth.oauthLogin(provider); + } catch (e) { + setError(e.message); + } finally { + if (isMountedRef.current) { + setLoading(null); + } + } + } return (
@@ -70,7 +95,7 @@ className="form-control" placeholder="john.smith" id="username" - disabled={loading} + disabled={!!loading} value={username} onChange={(e) => setUsername(e.target.value)} /> @@ -84,7 +109,7 @@ className="form-control" placeholder="••••••••••••" id="password" - disabled={loading} + disabled={!!loading} value={password} onChange={(e) => setPassword(e.target.value)} /> @@ -92,29 +117,33 @@
-
- - -
- Logging in... -
-
+ {oauthProviders?.providers.length > 0 ? (<>
or
- {oauthProviders.providers.map((v, i) => Login with {v})} + {oauthProviders.providers.map((v, i) => ( + { + evt.preventDefault(); + handleOAuthLogin(v); + }} + /> + ))} ): <>} @@ -122,3 +151,28 @@ ); } + +function ButtonOrSpinner({ type, variant, disabled, showSpinner, text, onClick }: { + type: "button" | "submit", + variant: string, + disabled: boolean, + showSpinner: boolean, + text: string, + onClick: (evt) => any, +}) { + if (showSpinner) { + return ( +
+ Logging in... +
+ ); + } + + if (type) { + return ( + + ); + } +}-- rgit 0.1.3