initial profile page work

This commit is contained in:
emilis 2022-09-13 17:51:23 +01:00
parent 779e4aa2ea
commit 2dbc6ad68a
8 changed files with 138 additions and 41 deletions

View File

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

View File

@ -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"),
} }
} }
} }

View File

@ -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,
), ),
)?), )?),
)) ))

View File

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

View File

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

View File

@ -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,
} }
} }
} }

View File

@ -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 {

View File

@ -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>