initial profile page work
This commit is contained in:
parent
779e4aa2ea
commit
2dbc6ad68a
|
@ -6,7 +6,9 @@ CREATE TABLE users (
|
||||||
host TEXT,
|
host TEXT,
|
||||||
display_name TEXT,
|
display_name TEXT,
|
||||||
email TEXT NOT NULL,
|
email TEXT NOT NULL,
|
||||||
password_hash TEXT NOT NULL
|
password_hash TEXT NOT NULL,
|
||||||
|
avatar_uri TEXT,
|
||||||
|
bio TEXT,
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE UNIQUE INDEX u_username_host ON users (username, host);
|
CREATE UNIQUE INDEX u_username_host ON users (username, host);
|
||||||
|
|
|
@ -17,17 +17,10 @@ impl Users {
|
||||||
|
|
||||||
pub async fn create_user(&self, u: User) -> Result<User, db::DBError> {
|
pub async fn create_user(&self, u: User) -> Result<User, db::DBError> {
|
||||||
let row = self.0.lock().await.query_one(
|
let row = self.0.lock().await.query_one(
|
||||||
"insert into users (id, username, host, display_name, password_hash, email) values ($1, $2, $3, $4, $5, $6) returning id",
|
"insert into users (id, username, host, display_name, password_hash, email) values ($1, $2, $3, $4, $5, $6) returning (id, username, host, display_name, password_hash, email, avatar_uri, bio)",
|
||||||
&[&sec::new_id(), &u.username, &u.host, &u.display_name, &u.password_hash, &u.email],
|
&[&sec::new_id(), &u.username, &u.host, &u.display_name, &u.password_hash, &u.email],
|
||||||
).await?;
|
).await?;
|
||||||
Ok(User {
|
Ok(User::from(&row))
|
||||||
id: row.get("id"),
|
|
||||||
username: u.username,
|
|
||||||
host: u.host,
|
|
||||||
display_name: u.display_name,
|
|
||||||
password_hash: u.password_hash,
|
|
||||||
email: u.email,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn user(&self, by: UserSelect) -> Result<User, db::DBError> {
|
pub async fn user(&self, by: UserSelect) -> Result<User, db::DBError> {
|
||||||
|
@ -52,7 +45,7 @@ impl Users {
|
||||||
.await
|
.await
|
||||||
.query(
|
.query(
|
||||||
format!(
|
format!(
|
||||||
"select id, username, host, display_name, password_hash, email from users where {}",
|
"select id, username, host, display_name, password_hash, email, avatar_uri, bio from users where {}",
|
||||||
where_clause
|
where_clause
|
||||||
)
|
)
|
||||||
.as_str(),
|
.as_str(),
|
||||||
|
@ -75,6 +68,8 @@ pub struct User {
|
||||||
pub display_name: Option<String>,
|
pub display_name: Option<String>,
|
||||||
pub password_hash: String,
|
pub password_hash: String,
|
||||||
pub email: String,
|
pub email: String,
|
||||||
|
pub avatar_uri: Option<String>,
|
||||||
|
pub bio: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&Row> for User {
|
impl From<&Row> for User {
|
||||||
|
@ -86,6 +81,8 @@ impl From<&Row> for User {
|
||||||
display_name: row.get("display_name"),
|
display_name: row.get("display_name"),
|
||||||
password_hash: row.get("password_hash"),
|
password_hash: row.get("password_hash"),
|
||||||
email: row.get("email"),
|
email: row.get("email"),
|
||||||
|
avatar_uri: row.get("avatar_uri"),
|
||||||
|
bio: row.get("bio"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,16 @@
|
||||||
use std::{collections::HashMap, str::FromStr};
|
use std;
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
body::Full,
|
body::Full,
|
||||||
extract::Path,
|
extract::Path,
|
||||||
handler::Handler,
|
|
||||||
http::{header, HeaderValue, StatusCode},
|
http::{header, HeaderValue, StatusCode},
|
||||||
response::{self, Html, IntoResponse, Response},
|
response::{self, IntoResponse, Response},
|
||||||
routing, Extension, Form, Json, Router,
|
routing, Extension, Form, Router,
|
||||||
};
|
};
|
||||||
use mime_guess::mime;
|
use mime_guess::mime;
|
||||||
use tower_cookies::{Cookie, Cookies};
|
use tower_cookies::{Cookie, Cookies};
|
||||||
|
|
||||||
use crate::svc::{
|
use crate::svc::auth::{AuthError, Claims};
|
||||||
auth::{AuthError, Claims},
|
|
||||||
profiles,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
servek::{Server, ServerError},
|
servek::{Server, ServerError},
|
||||||
|
@ -45,15 +41,26 @@ impl Server {
|
||||||
.fallback(routing::get(Self::handler_404))
|
.fallback(routing::get(Self::handler_404))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_cookie(&self, cookie: Option<Cookie>) -> Result<WithNav<Option<Claims>>, ServerError> {
|
fn from_cookies(&self, cookies: Cookies) -> Result<WithNav<Option<Claims>>, ServerError> {
|
||||||
if let Some(cookie) = cookie {
|
const logged_out: Result<WithNav<Option<Claims>>, ServerError> = Ok(WithNav {
|
||||||
let claims = self.auth.get_claims(cookie.value().to_owned())?;
|
obj: None,
|
||||||
|
nav_type: NavType::LoggedOut,
|
||||||
|
});
|
||||||
|
if let Some(cookie) = cookies.get(AUTH_COOKIE_NAME) {
|
||||||
|
let claims = match self.auth.get_claims(cookie.value().to_owned()) {
|
||||||
|
Ok(claims) => claims,
|
||||||
|
Err(e) => {
|
||||||
|
if e.clone().expired() {
|
||||||
|
cookies.remove(Cookie::new(AUTH_COOKIE_NAME, ""));
|
||||||
|
return logged_out;
|
||||||
|
} else {
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
Ok(WithNav::new(Some(claims.clone()), claims.into()))
|
Ok(WithNav::new(Some(claims.clone()), claims.into()))
|
||||||
} else {
|
} else {
|
||||||
Ok(WithNav {
|
logged_out
|
||||||
nav_type: NavType::LoggedOut,
|
|
||||||
obj: None,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,7 +68,7 @@ impl Server {
|
||||||
Extension(srv): Extension<Server>,
|
Extension(srv): Extension<Server>,
|
||||||
cookies: Cookies,
|
cookies: Cookies,
|
||||||
) -> Result<impl IntoResponse, ServerError> {
|
) -> Result<impl IntoResponse, ServerError> {
|
||||||
let user = srv.from_cookie(cookies.get(AUTH_COOKIE_NAME))?;
|
let user = srv.from_cookies(cookies)?;
|
||||||
Ok((
|
Ok((
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
response::Html(srv.hb.render("index", &user)?),
|
response::Html(srv.hb.render("index", &user)?),
|
||||||
|
@ -89,10 +96,7 @@ impl Server {
|
||||||
) -> Result<impl IntoResponse, ServerError> {
|
) -> Result<impl IntoResponse, ServerError> {
|
||||||
Ok((
|
Ok((
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
response::Html(
|
response::Html(srv.hb.render("404", &srv.from_cookies(cookies)?)?),
|
||||||
srv.hb
|
|
||||||
.render("404", &srv.from_cookie(cookies.get(AUTH_COOKIE_NAME))?)?,
|
|
||||||
),
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,7 +153,21 @@ impl Server {
|
||||||
)?,
|
)?,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let token = srv.auth.login(login.username, login.password).await?;
|
let token = match srv.auth.login(login.username, login.password).await {
|
||||||
|
Ok(token) => token,
|
||||||
|
Err(e) => match e {
|
||||||
|
AuthError::InvalidCredentials => {
|
||||||
|
return Ok((
|
||||||
|
StatusCode::UNAUTHORIZED,
|
||||||
|
srv.login_page_with(
|
||||||
|
"error-partial".to_owned(),
|
||||||
|
"invalid credentials".to_owned(),
|
||||||
|
)?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
e => return Err(e.into()),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
if cookies.get(AUTH_COOKIE_NAME).is_some() {
|
if cookies.get(AUTH_COOKIE_NAME).is_some() {
|
||||||
cookies.remove(Cookie::new(AUTH_COOKIE_NAME, ""));
|
cookies.remove(Cookie::new(AUTH_COOKIE_NAME, ""));
|
||||||
|
@ -178,7 +196,7 @@ impl Server {
|
||||||
"profile",
|
"profile",
|
||||||
&WithNav::new(
|
&WithNav::new(
|
||||||
srv.profiler.profile(username).await?,
|
srv.profiler.profile(username).await?,
|
||||||
srv.from_cookie(cookies.get(AUTH_COOKIE_NAME))?.nav_type,
|
srv.from_cookies(cookies)?.nav_type,
|
||||||
),
|
),
|
||||||
)?),
|
)?),
|
||||||
))
|
))
|
||||||
|
|
|
@ -105,6 +105,7 @@ impl From<AuthError> for ServerError {
|
||||||
ServerError::BadRequest("invalid credentials".to_owned())
|
ServerError::BadRequest("invalid credentials".to_owned())
|
||||||
}
|
}
|
||||||
AuthError::ServerError(err) => ServerError::Internal(err),
|
AuthError::ServerError(err) => ServerError::Internal(err),
|
||||||
|
AuthError::Expired => ServerError::BadRequest("expired token".to_owned()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,12 +95,19 @@ impl From<users::User> for Claims {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum AuthError {
|
pub enum AuthError {
|
||||||
InvalidCredentials,
|
InvalidCredentials,
|
||||||
|
Expired,
|
||||||
ServerError(String),
|
ServerError(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl AuthError {
|
||||||
|
pub fn expired(self) -> bool {
|
||||||
|
self == Self::Expired
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<DBError> for AuthError {
|
impl From<DBError> for AuthError {
|
||||||
fn from(_: DBError) -> Self {
|
fn from(_: DBError) -> Self {
|
||||||
Self::InvalidCredentials
|
Self::InvalidCredentials
|
||||||
|
@ -109,6 +116,9 @@ impl From<DBError> for AuthError {
|
||||||
|
|
||||||
impl From<jsonwebtoken::errors::Error> for AuthError {
|
impl From<jsonwebtoken::errors::Error> for AuthError {
|
||||||
fn from(e: jsonwebtoken::errors::Error) -> Self {
|
fn from(e: jsonwebtoken::errors::Error) -> Self {
|
||||||
Self::ServerError(e.to_string())
|
match e.kind() {
|
||||||
|
jsonwebtoken::errors::ErrorKind::ExpiredSignature => Self::Expired,
|
||||||
|
kind => Self::ServerError(e.to_string()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,6 +42,8 @@ impl Profiler {
|
||||||
display_name: None,
|
display_name: None,
|
||||||
password_hash: sec::hash(password),
|
password_hash: sec::hash(password),
|
||||||
email,
|
email,
|
||||||
|
avatar_uri: None,
|
||||||
|
bio: None,
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
@ -55,15 +57,26 @@ pub struct User {
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub display_name: Option<String>,
|
pub display_name: Option<String>,
|
||||||
pub host: Option<String>,
|
pub host: Option<String>,
|
||||||
|
pub avatar_uri: Option<String>,
|
||||||
|
pub bio: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<users::User> for User {
|
impl From<users::User> for User {
|
||||||
fn from(u: users::User) -> Self {
|
fn from(u: users::User) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: u.id,
|
id: u.id,
|
||||||
|
display_name: u.display_name.or(Some(format!(
|
||||||
|
"@{}{}",
|
||||||
|
u.username,
|
||||||
|
u.host
|
||||||
|
.clone()
|
||||||
|
.map(|h| format!("@{}", h))
|
||||||
|
.unwrap_or(String::new()),
|
||||||
|
))),
|
||||||
username: u.username,
|
username: u.username,
|
||||||
display_name: u.display_name,
|
|
||||||
host: u.host,
|
host: u.host,
|
||||||
|
avatar_uri: u.avatar_uri,
|
||||||
|
bio: u.bio,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@ body {
|
||||||
color: rebeccapurple;
|
color: rebeccapurple;
|
||||||
}
|
}
|
||||||
|
|
||||||
p,
|
|
||||||
h1,
|
h1,
|
||||||
h2,
|
h2,
|
||||||
label {
|
label {
|
||||||
|
@ -62,11 +61,58 @@ a {
|
||||||
color: rebeccapurple;
|
color: rebeccapurple;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
width: 85vw;
|
||||||
|
border: 5px solid rebeccapurple;
|
||||||
|
height: 85vh;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
background-color: rgba(13, 6, 19, 0.4);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 128px;
|
||||||
|
height: 128px;
|
||||||
|
border: 3px solid rebeccapurple;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
font-size: 110%;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile {
|
||||||
|
/* border: 3px solid rebeccapurple; */
|
||||||
|
padding: 5px;
|
||||||
|
display: flex;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header {
|
||||||
|
text-align: left;
|
||||||
|
width: 128px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-bio {
|
||||||
|
border: 3px solid rebeccapurple;
|
||||||
|
margin: auto;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
nav {
|
nav {
|
||||||
/* background-color: #333; */
|
|
||||||
margin: 0;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
border: 5px solid rgba(102, 51, 153, 0.5);
|
||||||
|
width: 85vw;
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
background-color: rgba(13, 6, 19, 0.4);
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav ul {
|
nav ul {
|
||||||
|
|
|
@ -2,14 +2,24 @@
|
||||||
<html>
|
<html>
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<title>@{{username}}</title>
|
<title>@{{obj.username}}</title>
|
||||||
<link rel="stylesheet" href="/static/style/main.css">
|
<link rel="stylesheet" href="/static/style/main.css">
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
{{> (lookup this "nav_type") }}
|
{{> (lookup this "nav_type") }}
|
||||||
<h1>hi {{obj.username}}, your id is {{obj.id}}</h1>
|
<div class="main">
|
||||||
|
<div class="profile">
|
||||||
|
<div class="profile-header">
|
||||||
|
<img class="avatar" src="{{obj.avatar_uri}}" alt="{{obj.username}}'s avatar" />
|
||||||
|
<p class="name">{{obj.display_name}}</p>
|
||||||
|
</div>
|
||||||
|
<div class="profile-bio">
|
||||||
|
<p>{{obj.bio}}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
Loading…
Reference in New Issue