🏡 index : ~doyle/chartered.git

author Jordan Doyle <jordan@doyle.la> 2021-10-06 2:34:39.0 +01:00:00
committer Jordan Doyle <jordan@doyle.la> 2021-10-06 2:34:39.0 +01:00:00
commit
8a7676945cf10f61083566e9343c792871116eee [patch]
tree
8990ee3e0014c05736ffeabc727ecc935fa9b443
parent
7e6f0b519e0f41d9228c8ede68b94b1d9673e85b
download
8a7676945cf10f61083566e9343c792871116eee.tar.gz

Quick little user info page



Diff

 chartered-frontend/src/index.tsx                    |  6 ++++++
 chartered-frontend/src/pages/User.tsx               | 91 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 chartered-web/src/endpoints/web_api/mod.rs          |  4 ++--
 chartered-web/src/endpoints/web_api/search_users.rs | 54 ------------------------------------------------------
 chartered-web/src/endpoints/web_api/users/info.rs   | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 chartered-web/src/endpoints/web_api/users/mod.rs    | 25 +++++++++++++++++++++++++
 chartered-web/src/endpoints/web_api/users/search.rs | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 7 files changed, 237 insertions(+), 56 deletions(-)

diff --git a/chartered-frontend/src/index.tsx b/chartered-frontend/src/index.tsx
index 0e800f6..940f619 100644
--- a/chartered-frontend/src/index.tsx
+++ a/chartered-frontend/src/index.tsx
@@ -22,6 +22,7 @@
import ListOrganisations from "./pages/organisations/ListOrganisations";
import OrganisationView from "./pages/crate/OrganisationView";
import CreateOrganisation from "./pages/organisations/CreateOrganisation";
import User from "./pages/User";

function App() {
  return (
@@ -60,6 +61,11 @@
            exact
            path="/crates/:organisation/:crate/:subview?"
            component={() => <CrateView />}
          />
          <PrivateRoute
            exact
            path="/users/:uuid"
            component={() => <User />}
          />
          <PrivateRoute
            exact
diff --git a/chartered-frontend/src/pages/User.tsx b/chartered-frontend/src/pages/User.tsx
new file mode 100644
index 0000000..1f07a29 100644
--- /dev/null
+++ a/chartered-frontend/src/pages/User.tsx
@@ -1,0 +1,91 @@
import React = require("react");
import { useParams } from "react-router-dom";
import { useAuth } from "../useAuth";
import { RoundedPicture, useAuthenticatedRequest } from "../util";
import Nav from "../sections/Nav";
import ErrorPage from "./ErrorPage";
import ReactPlaceholder from "react-placeholder/lib";
import { Envelope, HouseDoor } from "react-bootstrap-icons";

interface Response {
    uuid: string;
    username: string;
    name?: string;
    nick?: string;
    email?: string;
    external_profile_url?: string;
    picture_url?: string;
}

export default function User() {
    const auth = useAuth();
    const { uuid } = useParams();
  
    const { response: user, error } = useAuthenticatedRequest<Response>({
      auth,
      endpoint: "users/info/" + uuid,
    });
  
    if (error) {
      return <ErrorPage message={error} />;
    }

    const ready = !!user;

    console.log(user);
  
    return (
      <div className="text-white">
        <Nav />
  
        <div className="container mt-4 pb-4">
            <div className="row align-items-stretch">
                <div className="col-12 col-md-6 mb-3">
                    <div className="card border-0 shadow-sm text-black h-100">
                    <div className="card-body">
                        <div className="d-flex flex-row align-items-center">
                        <RoundedPicture
                            src={user?.picture_url}
                            height="96px"
                            width="96px"
                        />

                        <div className="px-2">
                            <h1 className="text-primary my-0">
                                <ReactPlaceholder showLoadingAnimation type="text" rows={1} ready={ready} style={{ width: "12rem" }}>
                                    {user?.nick || user?.name || user?.username}
                                </ReactPlaceholder>
                            </h1>
                        </div>
                        </div>
                    </div>
                    </div>
                </div>

                <div className="col-12 col-md-6 mb-3">
                    <div className="card border-0 shadow-sm text-black h-100">
                        <div className="card-body">
                            <h5>Aliases</h5>

                            {user?.nick ? <>{user?.nick}<br /></> : <></>}
                            {user?.name ? <>{user?.name}<br /></> : <></>}
                            {user?.username ? <>{user?.username}<br /></> : <></>}
                        </div>
                    </div>
                </div>
            </div>

            <div className="row align-items-stretch">
                <div className="col-12">
                    <div className="card border-0 shadow-sm text-black h-100">
                        <div className="card-body">
                            <div><HouseDoor /> <a href={user?.external_profile_url} target="_blank">{user?.external_profile_url}</a></div>
                            <div><Envelope /> <a href={`mailto:${user?.email}`}>{user?.email}</a></div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
      </div>
    );
  }
diff --git a/chartered-web/src/endpoints/web_api/mod.rs b/chartered-web/src/endpoints/web_api/mod.rs
index 5e80e62..407d039 100644
--- a/chartered-web/src/endpoints/web_api/mod.rs
+++ a/chartered-web/src/endpoints/web_api/mod.rs
@@ -1,8 +1,8 @@
mod auth;
mod crates;
mod organisations;
mod search_users;
mod ssh_key;
mod users;

use axum::{
    body::{Body, BoxBody},
@@ -25,7 +25,7 @@
    crate::axum_box_after_every_route!(Router::new()
        .nest("/organisations", organisations::routes())
        .nest("/crates", crates::routes())
        .route("/users/search", get(search_users::handle))
        .nest("/users", users::routes())
        .route("/ssh-key", get(ssh_key::handle_get))
        .route("/ssh-key", put(ssh_key::handle_put))
        .route("/ssh-key/:id", delete(ssh_key::handle_delete)))
diff --git a/chartered-web/src/endpoints/web_api/search_users.rs b/chartered-web/src/endpoints/web_api/search_users.rs
deleted file mode 100644
index 9278150..0000000 100644
--- a/chartered-web/src/endpoints/web_api/search_users.rs
+++ /dev/null
@@ -1,54 +1,0 @@
use axum::{extract, Json};
use chartered_db::{users::User, ConnectionPool};
use serde::{Deserialize, Serialize};
use thiserror::Error;

#[derive(Deserialize)]
pub struct RequestParams {
    q: String,
}

#[derive(Serialize)]
pub struct Response {
    users: Vec<ResponseUser>,
}

#[derive(Serialize)]
pub struct ResponseUser {
    user_uuid: chartered_db::uuid::Uuid,
    username: String,
}

pub async fn handle(
    extract::Extension(db): extract::Extension<ConnectionPool>,
    extract::Query(req): extract::Query<RequestParams>,
) -> Result<Json<Response>, Error> {
    let users = User::search(db, req.q, 5)
        .await?
        .into_iter()
        .map(|user| ResponseUser {
            user_uuid: user.uuid.0,
            username: user.username,
        })
        .collect();

    Ok(Json(Response { users }))
}

#[derive(Error, Debug)]
pub enum Error {
    #[error("Failed to query database")]
    Database(#[from] chartered_db::Error),
}

impl Error {
    pub fn status_code(&self) -> axum::http::StatusCode {
        use axum::http::StatusCode;

        match self {
            Self::Database(_) => StatusCode::INTERNAL_SERVER_ERROR,
        }
    }
}

define_error_response!(Error);
diff --git a/chartered-web/src/endpoints/web_api/users/info.rs b/chartered-web/src/endpoints/web_api/users/info.rs
new file mode 100644
index 0000000..c8870bb 100644
--- /dev/null
+++ a/chartered-web/src/endpoints/web_api/users/info.rs
@@ -1,0 +1,59 @@
use axum::{extract, Json};
use chartered_db::{users::User, ConnectionPool};
use serde::{Deserialize, Serialize};
use thiserror::Error;

#[derive(Serialize)]
pub struct Response {
    uuid: chartered_db::uuid::Uuid,
    username: String,
    name: Option<String>,
    nick: Option<String>,
    email: Option<String>,
    external_profile_url: Option<String>,
    picture_url: Option<String>,
}

impl From<chartered_db::users::User> for Response {
    fn from(user: chartered_db::users::User) -> Self {
        Self {
            uuid: user.uuid.0,
            username: user.username,
            name: user.name,
            nick: user.nick,
            email: user.email,
            external_profile_url: user.external_profile_url,
            picture_url: user.picture_url,
        }
    }
}

pub async fn handle(
    extract::Path((_session_key, uuid)): extract::Path<(String, chartered_db::uuid::Uuid)>,
    extract::Extension(db): extract::Extension<ConnectionPool>,
) -> Result<Json<Response>, Error> {
    let user = User::find_by_uuid(db, uuid).await?.ok_or(Error::NotFound)?;

    Ok(Json(user.into()))
}

#[derive(Error, Debug)]
pub enum Error {
    #[error("Failed to query database")]
    Database(#[from] chartered_db::Error),
    #[error("User doesn't exist")]
    NotFound,
}

impl Error {
    pub fn status_code(&self) -> axum::http::StatusCode {
        use axum::http::StatusCode;

        match self {
            Self::Database(_) => StatusCode::INTERNAL_SERVER_ERROR,
            Self::NotFound => StatusCode::NOT_FOUND,
        }
    }
}

define_error_response!(Error);
diff --git a/chartered-web/src/endpoints/web_api/users/mod.rs b/chartered-web/src/endpoints/web_api/users/mod.rs
new file mode 100644
index 0000000..256bbb5 100644
--- /dev/null
+++ a/chartered-web/src/endpoints/web_api/users/mod.rs
@@ -1,0 +1,25 @@
mod search;
mod info;

use axum::{
    body::{Body, BoxBody},
    handler::get,
    http::{Request, Response},
    Router,
};
use futures::future::Future;
use std::convert::Infallible;

pub fn routes() -> Router<
    impl tower::Service<
            Request<Body>,
            Response = Response<BoxBody>,
            Error = Infallible,
            Future = impl Future<Output = Result<Response<BoxBody>, Infallible>> + Send,
        > + Clone
        + Send,
> {
    crate::axum_box_after_every_route!(Router::new()
        .route("/search", get(search::handle))
        .route("/info/:uuid", get(info::handle)))
}
diff --git a/chartered-web/src/endpoints/web_api/users/search.rs b/chartered-web/src/endpoints/web_api/users/search.rs
new file mode 100644
index 0000000..9278150 100644
--- /dev/null
+++ a/chartered-web/src/endpoints/web_api/users/search.rs
@@ -1,0 +1,54 @@
use axum::{extract, Json};
use chartered_db::{users::User, ConnectionPool};
use serde::{Deserialize, Serialize};
use thiserror::Error;

#[derive(Deserialize)]
pub struct RequestParams {
    q: String,
}

#[derive(Serialize)]
pub struct Response {
    users: Vec<ResponseUser>,
}

#[derive(Serialize)]
pub struct ResponseUser {
    user_uuid: chartered_db::uuid::Uuid,
    username: String,
}

pub async fn handle(
    extract::Extension(db): extract::Extension<ConnectionPool>,
    extract::Query(req): extract::Query<RequestParams>,
) -> Result<Json<Response>, Error> {
    let users = User::search(db, req.q, 5)
        .await?
        .into_iter()
        .map(|user| ResponseUser {
            user_uuid: user.uuid.0,
            username: user.username,
        })
        .collect();

    Ok(Json(Response { users }))
}

#[derive(Error, Debug)]
pub enum Error {
    #[error("Failed to query database")]
    Database(#[from] chartered_db::Error),
}

impl Error {
    pub fn status_code(&self) -> axum::http::StatusCode {
        use axum::http::StatusCode;

        match self {
            Self::Database(_) => StatusCode::INTERNAL_SERVER_ERROR,
        }
    }
}

define_error_response!(Error);