Show user profile pictures instead of placekittens
Diff
chartered-db/src/users.rs | 7 +++++++
chartered-frontend/src/index.tsx | 12 ++----------
chartered-frontend/src/useAuth.tsx | 39 ++++++++++++++++++++++++++-------------
chartered-frontend/src/util.tsx | 30 ++++++++++++++++++++++++++++++
chartered-frontend/src/pages/Login.tsx | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
chartered-frontend/src/pages/User.tsx | 218 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
chartered-frontend/src/pages/crate/CrateView.tsx | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++----------
chartered-frontend/src/pages/crate/Members.tsx | 36 ++++++++++++++++++++++--------------
chartered-frontend/src/pages/organisations/ListOrganisations.tsx | 43 ++++++++++++++++++++++++++-----------------
chartered-frontend/src/pages/ssh-keys/ListSshKeys.tsx | 143 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
chartered-web/src/endpoints/web_api/crates/info.rs | 15 +++++++++++++--
chartered-web/src/endpoints/web_api/crates/members.rs | 6 ++++--
chartered-web/src/endpoints/web_api/organisations/info.rs | 10 ++++++----
chartered-web/src/endpoints/web_api/users/info.rs | 2 +-
chartered-web/src/endpoints/web_api/users/mod.rs | 2 +-
15 files changed, 467 insertions(+), 249 deletions(-)
@@ -268,6 +268,13 @@
.unwrap_or_default()
.permissions)
}
pub fn display_name(&self) -> &str {
self.nick
.as_ref()
.or(self.name.as_ref())
.unwrap_or(&self.username)
}
}
#[derive(Identifiable, Queryable, Associations, PartialEq, Eq, Hash, Debug)]
@@ -62,13 +62,9 @@
path="/crates/:organisation/:crate/:subview?"
component={() => <CrateView />}
/>
<PrivateRoute exact path="/users/:uuid" component={() => <User />} />
<PrivateRoute
exact
path="/users/:uuid"
component={() => <User />}
/>
<PrivateRoute
exact
path="/ssh-keys"
component={() => <Redirect to="/ssh-keys/list" />}
/>
@@ -126,11 +122,7 @@
{...rest}
render={(props) => {
if (
!unauthedOnly ||
!auth ||
!auth?.getAuthKey()
) {
if (!unauthedOnly || !auth || !auth?.getAuthKey()) {
return <Component {...props} />;
} else {
return (
@@ -38,16 +38,20 @@
useEffect(async () => {
try {
let result = await fetch(unauthenticatedEndpoint(`login/oauth/complete${location.search}`));
let result = await fetch(
unauthenticatedEndpoint(`login/oauth/complete${location.search}`)
);
let json = await result.json();
auth.handleLoginResponse(json);
} catch (err) {
setResult(
<Redirect to={{
pathname: "/login",
state: { error: err.message }
}} />
<Redirect
to={{
pathname: "/login",
state: { error: err.message },
}}
/>
);
}
});
@@ -68,7 +72,11 @@
useEffect(() => {
localStorage.setItem(
"charteredAuthentication",
JSON.stringify({ userUuid: auth?.[0], authKey: auth?.[1], expires: auth?.[2] })
JSON.stringify({
userUuid: auth?.[0],
authKey: auth?.[1],
expires: auth?.[2],
})
);
}, [auth]);
@@ -78,7 +86,7 @@
}
setAuth([response.user_uuid, response.key, new Date(response.expires)]);
}
};
const login = async (username: string, password: string) => {
let res = await fetch(unauthenticatedEndpoint("login/password"), {
@@ -95,13 +103,16 @@
};
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 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) {
@@ -109,7 +120,7 @@
}
window.location.href = json.redirect_url;
}
};
const logout = async () => {
@@ -71,6 +71,36 @@
return { response, error };
}
export function ProfilePicture({
src,
height,
width,
className,
}: {
src: string;
height: string;
width: string;
className?: string;
}) {
if (src !== null) {
return (
<RoundedPicture
src={src}
height={height}
width={width}
className={className}
/>
);
} else {
return (
<div
className={`rounded-circle ${className}`}
style={{ width, height, background: "rgb(235, 235, 235)" }}
/>
);
}
}
export function RoundedPicture({
src,
height,
@@ -19,7 +19,10 @@
const [loading, setLoading] = useState<string | null>(null);
const isMountedRef = useRef(null);
const { response: oauthProviders } = useUnauthenticatedRequest<OAuthProviders>({ endpoint: "login/oauth/providers" });
const { response: oauthProviders } =
useUnauthenticatedRequest<OAuthProviders>({
endpoint: "login/oauth/providers",
});
useEffect(() => {
if (location.state?.error) {
@@ -56,7 +59,7 @@
} catch (e) {
setError(e.message);
}
}
};
return (
<div className="bg-primary p-4 text-white min-vh-100 d-flex justify-content-center align-items-center">
@@ -96,7 +99,9 @@
onChange={(e) => setUsername(e.target.value)}
/>
<label htmlFor="email" className="form-label">Username</label>
<label htmlFor="email" className="form-label">
Username
</label>
</div>
<div className="form-floating mt-2">
@@ -110,7 +115,9 @@
onChange={(e) => setPassword(e.target.value)}
/>
<label htmlFor="password" className="form-label">Password</label>
<label htmlFor="password" className="form-label">
Password
</label>
</div>
<ButtonOrSpinner
@@ -123,24 +130,28 @@
/>
</form>
{oauthProviders?.providers.length > 0 ? (<>
<div className="side-lines mt-3">or</div>
{oauthProviders.providers.map((v, i) => (
<ButtonOrSpinner
key={i}
type="button"
variant="dark"
disabled={!!loading}
showSpinner={loading === v}
text={`Login with ${v}`}
onClick={(evt) => {
evt.preventDefault();
handleOAuthLogin(v);
}}
/>
))}
</>): <></>}
{oauthProviders?.providers.length > 0 ? (
<>
<div className="side-lines mt-3">or</div>
{oauthProviders.providers.map((v, i) => (
<ButtonOrSpinner
key={i}
type="button"
variant="dark"
disabled={!!loading}
showSpinner={loading === v}
text={`Login with ${v}`}
onClick={(evt) => {
evt.preventDefault();
handleOAuthLogin(v);
}}
/>
))}
</>
) : (
<></>
)}
</div>
</div>
</div>
@@ -148,17 +159,27 @@
);
}
function ButtonOrSpinner({ type, variant, disabled, showSpinner, text, onClick }: {
type: "button" | "submit",
variant: string,
disabled: boolean,
showSpinner: boolean,
text: string,
onClick: (evt) => any,
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 (
<div className="spinner-border text-primary mt-3 m-auto d-block" role="status">
<div
className="spinner-border text-primary mt-3 m-auto d-block"
role="status"
>
<span className="visually-hidden">Logging in...</span>
</div>
);
@@ -166,9 +187,14 @@
if (type) {
return (
<button type={type} disabled={disabled} onClick={onClick} className={`btn btn-lg mt-2 btn-${variant} w-100`}>
<button
type={type}
disabled={disabled}
onClick={onClick}
className={`btn btn-lg mt-2 btn-${variant} w-100`}
>
{text}
</button>
);
}
}
}
@@ -1,91 +1,165 @@
import React = require("react");
import { useParams } from "react-router-dom";
import { useAuth } from "../useAuth";
import { RoundedPicture, useAuthenticatedRequest } from "../util";
import {
ProfilePicture,
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;
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>
const auth = useAuth();
const { uuid } = useParams();
const { response: user, error } = useAuthenticatedRequest<Response>({
auth,
endpoint: "users/info/" + uuid,
});
if (error) {
return <ErrorPage message={error} />;
}
<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>
const ready = !!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">
<ProfilePicture
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="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 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">
<h5>Links</h5>
{ready && !user?.external_profile_url && !user?.email
? `${
user?.nick || user?.name || user?.username
} hasn't added any links to their profile yet.`
: ""}
{!ready || user?.external_profile_url ? (
<div>
<HouseDoor />
<ReactPlaceholder
showLoadingAnimation
type="text"
rows={1}
ready={ready}
className="position-relative d-inline-block"
style={{ width: "12rem", top: "4px" }}
>
<a href={user?.external_profile_url} target="_blank">
{user?.external_profile_url}
</a>
</ReactPlaceholder>
</div>
) : (
<></>
)}
{!ready || user?.email ? (
<div>
<Envelope />
<ReactPlaceholder
showLoadingAnimation
type="text"
rows={1}
ready={ready}
className="position-relative d-inline-block"
style={{ width: "12rem", top: "4px" }}
>
<a href={`mailto:${user?.email}`}>{user?.email}</a>
</ReactPlaceholder>
</div>
) : (
<></>
)}
</div>
</div>
</div>
</div>
</div>
);
}
</div>
);
}
@@ -21,6 +21,7 @@
import { useParams, NavLink, Redirect, Link } from "react-router-dom";
import {
authenticatedEndpoint,
ProfilePicture,
RoundedPicture,
RoundedPicture,
useAuthenticatedRequest,
@@ -45,12 +46,18 @@
versions: CrateInfoVersion[];
}
export interface CrateInfoVersionUploader {
display_name: string;
picture_url?: string;
uuid: string;
}
export interface CrateInfoVersion {
vers: string;
deps: CrateInfoVersionDependency[];
features: any[];
size: number;
uploader: string;
uploader: CrateInfoVersionUploader;
created_at: string;
}
@@ -69,10 +76,13 @@
return <Redirect to={`/crates/${organisation}/${crate}/readme`} />;
}
const { response: crateInfo, error } = useAuthenticatedRequest<CrateInfo>({
auth,
endpoint: `crates/${organisation}/${crate}`,
}, [organisation, crate]);
const { response: crateInfo, error } = useAuthenticatedRequest<CrateInfo>(
{
auth,
endpoint: `crates/${organisation}/${crate}`,
},
[organisation, crate]
);
if (error) {
return <ErrorPage message={error} />;
@@ -224,7 +234,13 @@
) : (
<></>
)}
{crateVersion.deps.map((dep) => <Dependency key={`${dep.name}-${dep.version_req}`} organisation={organisation} dep={dep} /> )}
{crateVersion.deps.map((dep) => (
<Dependency
key={`${dep.name}-${dep.version_req}`}
organisation={organisation}
dep={dep}
/>
))}
</ul>
</div>
</div>
@@ -241,19 +257,33 @@
interface Member {
uuid: string;
username: string;
display_name: string;
permissions: string[];
}
function Dependency({ organisation, dep }: { organisation: string, dep: CrateInfoVersionDependency }) {
function Dependency({
organisation,
dep,
}: {
organisation: string;
dep: CrateInfoVersionDependency;
}) {
let link = <>{dep.name}</>;
if (dep.registry === null) {
link = <a target="_blank" href={`/crates/${organisation}/${dep.name}`}>{link}</a>;
link = (
<a target="_blank" href={`/crates/${organisation}/${dep.name}`}>
{link}
</a>
);
} else if (dep.registry === "https://github.com/rust-lang/crates.io-index") {
link = <a target="_blank" href={`https://crates.io/crates/${dep.name}`}>{link}</a>;
link = (
<a target="_blank" href={`https://crates.io/crates/${dep.name}`}>
{link}
</a>
);
} else if (dep.registry.indexOf("ssh://") === 0) {
const parts = dep.registry.split('/');
const parts = dep.registry.split("/");
const org = parts[parts.length - 1];
if (org) {
link = <Link to={`/crates/${org}/${dep.name}`}>{link}</Link>;
@@ -383,13 +413,18 @@
<div>
<div className="d-inline-block">
By
<RoundedPicture
src="http://placekitten.com/22/22"
<ProfilePicture
src={version.uploader.picture_url}
height="22px"
width="22px"
className="ms-1 me-1"
/>
{version.uploader}
<Link
to={`/users/${version.uploader.uuid}`}
className="link-light"
>
{version.uploader.display_name}
</Link>
</div>
<div className="ms-3 d-inline-block">
@@ -10,6 +10,7 @@
} from "react-bootstrap-icons";
import {
authenticatedEndpoint,
ProfilePicture,
RoundedPicture,
useAuthenticatedRequest,
} from "../../util";
@@ -23,7 +24,8 @@
interface Member {
uuid: string;
permissions?: string[];
username: string;
display_name: string;
picture_url?: string;
}
export default function Members({
@@ -203,26 +205,30 @@
show={deleting === true}
onCancel={() => setDeleting(false)}
onConfirm={() => doDelete()}
username={member.username}
username={member.display_name}
/>
<ErrorModal error={error} onClose={() => setError(null)} />
<tr>
<td className="align-middle fit">
<RoundedPicture
src="http://placekitten.com/48/48"
height="48px"
width="48px"
/>
<ProfilePicture src={member.picture_url} height="48px" width="48px" />
</td>
<td className="align-middle">
<strong><Link to={`/users/${member.uuid}`} class="text-decoration-none">{member.username}</Link></strong>
{auth.getUserUuid() === member.uuid ? <>
<br />
<em>(that's you!)</em>
</> : <></>}
<strong>
<Link to={`/users/${member.uuid}`} className="text-decoration-none">
{member.display_name}
</Link>
</strong>
{auth.getUserUuid() === member.uuid ? (
<>
<br />
<em>(that's you!)</em>
</>
) : (
<></>
)}
</td>
{possiblePermissions && member.permissions ? (
@@ -323,13 +329,13 @@
ref={searchRef}
renderMenuItemChildren={(option, props) => (
<>
<RoundedPicture
src="http://placekitten.com/24/24"
<ProfilePicture
src={option.picture_url}
height="24px"
width="24px"
className="me-2"
/>
<span>{option.username}</span>
<span>{option.display_name}</span>
</>
)}
/>
@@ -37,40 +37,47 @@
<h1>Your Organisations</h1>
<div className="card border-0 shadow-sm text-black">
{!list ? <LoadingSpinner /> : <>
{list.organisations.length === 0 ? (
{!list ? (
<LoadingSpinner />
) : (
<>
{list.organisations.length === 0 ? (
<div className="card-body">
You don't belong to any organisations yet.
You don't belong to any organisations yet.
</div>
) : (
) : (
<table className="table table-striped">
<tbody>
<tbody>
{list.organisations.map((v, i) => (
<tr key={i}>
<tr key={i}>
<td className="align-middle fit">
<RoundedPicture
<RoundedPicture
src="http://placekitten.com/48/48"
height="48px"
width="48px"
/>
/>
</td>
<td className="align-middle" style={{ lineHeight: "1.1" }}>
<div>
<td
className="align-middle"
style={{ lineHeight: "1.1" }}
>
<div>
<Link to={`/crates/${v.name}`}>{v.name}</Link>
</div>
<div>
</div>
<div>
<small style={{ fontSize: "0.75rem" }}>
{v.description}
{v.description}
</small>
</div>
</div>
</td>
</tr>
</tr>
))}
</tbody>
</tbody>
</table>
)}
</>}
)}
</>
)}
</div>
<Link
@@ -97,82 +97,97 @@
</div>
<div className="card border-0 shadow-sm text-black">
{!sshKeys ? <LoadingSpinner /> : <>
{sshKeys.keys.length == 0 ? (
<div className="card-body">You haven't added any SSH keys yet.</div>
) : (
<div className="table-responsive">
<table className="table table-striped">
<tbody>
{sshKeys.keys.map((key) => (
<tr key={key.uuid}>
<td className="align-middle">
<h6 className="m-0 lh-sm">{key.name}</h6>
<pre className="m-0">{key.fingerprint}</pre>
<div className="lh-sm" style={{ fontSize: ".75rem" }}>
<div className="text-muted d-inline-block me-3">
Added{" "}
<OverlayTrigger
overlay={
<Tooltip id={`${key.uuid}-created-at`}>
{new Date(key.created_at).toLocaleString()}
</Tooltip>
}
>
<span className="text-decoration-underline-dotted">
<HumanTime
time={new Date(key.created_at).getTime()}
/>
</span>
</OverlayTrigger>
</div>
<span
className={`text-${
key.last_used_at
? new Date(key.last_used_at) > dateMonthAgo
? "success"
: "danger"
: "muted"
}`}
{!sshKeys ? (
<LoadingSpinner />
) : (
<>
{sshKeys.keys.length == 0 ? (
<div className="card-body">
You haven't added any SSH keys yet.
</div>
) : (
<div className="table-responsive">
<table className="table table-striped">
<tbody>
{sshKeys.keys.map((key) => (
<tr key={key.uuid}>
<td className="align-middle">
<h6 className="m-0 lh-sm">{key.name}</h6>
<pre className="m-0">{key.fingerprint}</pre>
<div
className="lh-sm"
style={{ fontSize: ".75rem" }}
>
Last used{" "}
{key.last_used_at ? (
<div className="text-muted d-inline-block me-3">
Added{" "}
<OverlayTrigger
overlay={
<Tooltip id={`${key.uuid}-last-used`}>
{new Date(key.last_used_at).toLocaleString()}
<Tooltip id={`${key.uuid}-created-at`}>
{new Date(
key.created_at
).toLocaleString()}
</Tooltip>
}
>
<span className="text-decoration-underline-dotted">
<HumanTime
time={new Date(key.last_used_at).getTime()}
time={new Date(key.created_at).getTime()}
/>
</span>
</OverlayTrigger>
) : (
<>never</>
)}
</span>
</div>
</td>
</div>
<span
className={`text-${
key.last_used_at
? new Date(key.last_used_at) > dateMonthAgo
? "success"
: "danger"
: "muted"
}`}
>
Last used{" "}
{key.last_used_at ? (
<OverlayTrigger
overlay={
<Tooltip id={`${key.uuid}-last-used`}>
{new Date(
key.last_used_at
).toLocaleString()}
</Tooltip>
}
>
<span className="text-decoration-underline-dotted">
<HumanTime
time={new Date(
key.last_used_at
).getTime()}
/>
</span>
</OverlayTrigger>
) : (
<>never</>
)}
</span>
</div>
</td>
<td className="align-middle fit">
<button
type="button"
className="btn text-danger"
onClick={() => setDeleting(key)}
>
<Trash />
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</>}
<td className="align-middle fit">
<button
type="button"
className="btn text-danger"
onClick={() => setDeleting(key)}
>
<Trash />
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</>
)}
</div>
<Link
@@ -49,7 +49,11 @@
size: v.size,
created_at: chrono::Utc.from_local_datetime(&v.created_at).unwrap(),
inner: v.into_cargo_format(&crate_with_permissions.crate_),
uploader: user.username,
uploader: ResponseVersionUploader {
uuid: user.uuid.0,
display_name: user.display_name().to_string(),
picture_url: user.picture_url,
},
})
.collect(),
})
@@ -69,7 +73,14 @@
inner: CrateVersion<'a>,
size: i32,
created_at: chrono::DateTime<chrono::Utc>,
uploader: String,
uploader: ResponseVersionUploader,
}
#[derive(Serialize)]
pub struct ResponseVersionUploader {
uuid: chartered_db::uuid::Uuid,
display_name: String,
picture_url: Option<String>,
}
#[derive(Serialize)]
@@ -17,7 +17,8 @@
#[derive(Deserialize, Serialize)]
pub struct GetResponseMember {
uuid: Uuid,
username: String,
display_name: String,
picture_url: Option<String>,
permissions: UserPermission,
}
@@ -35,7 +36,8 @@
.into_iter()
.map(|(user, permissions)| GetResponseMember {
uuid: user.uuid.0,
username: user.username,
display_name: user.display_name().to_string(),
picture_url: user.picture_url,
permissions,
})
.collect();
@@ -52,8 +52,9 @@
members: users
.into_iter()
.map(|(user, perms)| ResponseUser {
uuid: user.uuid.to_string(),
username: user.username,
uuid: user.uuid.0,
display_name: user.display_name().to_string(),
picture_url: user.picture_url,
permissions: can_manage_users.then(|| perms),
})
.collect(),
@@ -76,7 +77,8 @@
#[derive(Serialize)]
pub struct ResponseUser {
uuid: String,
username: String,
uuid: chartered_db::uuid::Uuid,
display_name: String,
picture_url: Option<String>,
permissions: Option<UserPermission>,
}
@@ -1,6 +1,6 @@
use axum::{extract, Json};
use chartered_db::{users::User, ConnectionPool};
use serde::{Deserialize, Serialize};
use serde::Serialize;
use thiserror::Error;
#[derive(Serialize)]
@@ -1,5 +1,5 @@
mod search;
mod info;
mod search;
use axum::{
body::{Body, BoxBody},