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(-)
@@ -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
@@ -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>
);
}
@@ -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)))
@@ -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);
@@ -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);
@@ -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)))
}
@@ -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);