Skip to content

Commit

Permalink
Implement password authentication and registration
Browse files Browse the repository at this point in the history
  • Loading branch information
w4 committed Oct 30, 2021
1 parent 9acf633 commit 5fd9217
Show file tree
Hide file tree
Showing 13 changed files with 400 additions and 34 deletions.
12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions chartered-db/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ pub enum Error {
MissingOrganisation,
/// Version {0} already exists for this crate
VersionConflict(String),
/// Username is already taken
UsernameTaken,
}

impl Error {
Expand Down
1 change: 1 addition & 0 deletions chartered-db/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ table! {
id -> Integer,
uuid -> Binary,
username -> Text,
password -> Nullable<Text>,
name -> Nullable<Text>,
nick -> Nullable<Text>,
email -> Nullable<Text>,
Expand Down
35 changes: 33 additions & 2 deletions chartered-db/src/users.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ use super::{
permissions::UserPermission,
schema::{user_crate_permissions, user_sessions, user_ssh_keys, users},
uuid::SqlUuid,
ConnectionPool, Result,
ConnectionPool, Error, Result,
};
use diesel::result::DatabaseErrorKind;
use diesel::{
insert_into, prelude::*, result::Error as DieselError, Associations, Identifiable, Queryable,
};
use diesel::{insert_into, prelude::*, Associations, Identifiable, Queryable};
use rand::{thread_rng, Rng};
use std::sync::Arc;
use thrussh_keys::PublicKeyBase64;
Expand All @@ -15,6 +18,7 @@ pub struct User {
pub id: i32,
pub uuid: SqlUuid,
pub username: String,
pub password: Option<String>,
pub name: Option<String>,
pub nick: Option<String>,
pub email: Option<String>,
Expand Down Expand Up @@ -172,6 +176,33 @@ impl User {
.await?
}

pub async fn register(
conn: ConnectionPool,
username: String,
password_hash: String,
) -> Result<()> {
tokio::task::spawn_blocking(move || {
let conn = conn.get()?;

let res = diesel::insert_into(users::table)
.values((
users::username.eq(&username),
users::uuid.eq(SqlUuid::random()),
users::password.eq(&password_hash),
))
.execute(&conn);

match res {
Ok(_) => Ok(()),
Err(DieselError::DatabaseError(DatabaseErrorKind::UniqueViolation, _)) => {
Err(Error::UsernameTaken)
}
Err(e) => Err(e.into()),
}
})
.await?
}

/// Parses an ssh key from its `ssh-add -L` format (`ssh-ed25519 AAAAC3N...`) and
/// inserts it to the database for the user.
pub async fn insert_ssh_key(
Expand Down
3 changes: 3 additions & 0 deletions chartered-frontend/src/dark.sass
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ html, body
&.btn-danger
color: white

.btn-outline-primary:hover
color: white

.btn-outline-light
border-color: $link-color

Expand Down
7 changes: 7 additions & 0 deletions chartered-frontend/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import CreateOrganisation from "./pages/organisations/CreateOrganisation";
import User from "./pages/User";
import Search from "./pages/Search";
import { backgroundFix } from "./overscrollColourFixer";
import Register from "./pages/Register";

if (
window.matchMedia &&
Expand Down Expand Up @@ -53,6 +54,12 @@ function App() {
path="/login"
component={() => <Login />}
/>
<PublicRoute
extract
unauthedOnly
path="/register"
component={() => <Register />}
/>
<PublicRoute
exact
unauthedOnly
Expand Down
41 changes: 28 additions & 13 deletions chartered-frontend/src/pages/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
SyntheticEvent,
MouseEventHandler,
} from "react";
import { useLocation } from "react-router-dom";
import { useLocation, Link } from "react-router-dom";

import { useAuth } from "../useAuth";
import { useUnauthenticatedRequest } from "../util";
Expand All @@ -17,18 +17,25 @@ import {
IconDefinition,
} from "@fortawesome/free-brands-svg-icons";
import { faSignInAlt } from "@fortawesome/free-solid-svg-icons";
import { PersonPlus } from "react-bootstrap-icons";

interface OAuthProviders {
providers: string[];
}

interface Prompt {
message: string;
kind: string;
}

export default function Login() {
const location = useLocation();
const auth = useAuth();

const [ackLocation, setAckLocation] = useState(false);
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [prompt, setPrompt] = useState<Prompt | null>(null);
const [loading, setLoading] = useState<string | null>(null);
const isMountedRef = useRef(null);

Expand All @@ -38,8 +45,9 @@ export default function Login() {
});

useEffect(() => {
if (location.state?.error) {
setError(location.state.error);
if (location.state?.prompt && !ackLocation) {
setPrompt(location.state.prompt);
setAckLocation(true);
}

isMountedRef.current = true;
Expand All @@ -51,13 +59,13 @@ export default function Login() {
const handleSubmit = async (evt: SyntheticEvent) => {
evt.preventDefault();

setError("");
setPrompt(null);
setLoading("password");

try {
await auth?.login(username, password);
} catch (e: any) {
setError(e.message);
setPrompt({ message: e.message, kind: "danger" });
} finally {
if (isMountedRef.current) {
setLoading(null);
Expand All @@ -66,13 +74,13 @@ export default function Login() {
};

const handleOAuthLogin = async (provider: string) => {
setError("");
setPrompt(null);
setLoading(provider);

try {
await auth?.oauthLogin(provider);
} catch (e: any) {
setError(e.message);
setPrompt({ message: e.message, kind: "danger" });
}
};

Expand All @@ -88,17 +96,17 @@ export default function Login() {
>
<div className="card-body">
<div
className="alert alert-danger alert-dismissible"
className={`alert alert-${prompt?.kind} alert-dismissible`}
role="alert"
style={{ display: error ? "block" : "none" }}
style={{ display: prompt ? "block" : "none" }}
>
{error}
{prompt?.message}

<button
type="button"
className="btn-close"
aria-label="Close"
onClick={() => setError("")}
onClick={() => setPrompt(null)}
/>
</div>

Expand Down Expand Up @@ -146,9 +154,16 @@ export default function Login() {
/>
</form>

<Link
to="/register"
className="btn btn-lg w-100 btn-outline-primary mt-2"
>
<PersonPlus /> Register
</Link>

{oauthProviders?.providers.length > 0 ? (
<>
<div className="side-lines mt-3">or</div>
<div className="side-lines mt-2">or</div>

{oauthProviders.providers.map((v, i) => (
<ButtonOrSpinner
Expand Down
Loading

0 comments on commit 5fd9217

Please sign in to comment.