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,
display_name TEXT,
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);

View File

@ -17,17 +17,10 @@ impl Users {
pub async fn create_user(&self, u: User) -> Result<User, db::DBError> {
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],
).await?;
Ok(User {
id: row.get("id"),
username: u.username,
host: u.host,
display_name: u.display_name,
password_hash: u.password_hash,
email: u.email,
})
Ok(User::from(&row))
}
pub async fn user(&self, by: UserSelect) -> Result<User, db::DBError> {
@ -52,7 +45,7 @@ impl Users {
.await
.query(
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
)
.as_str(),
@ -75,6 +68,8 @@ pub struct User {
pub display_name: Option<String>,
pub password_hash: String,
pub email: String,
pub avatar_uri: Option<String>,
pub bio: Option<String>,
}
impl From<&Row> for User {
@ -86,6 +81,8 @@ impl From<&Row> for User {
display_name: row.get("display_name"),
password_hash: row.get("password_hash"),
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::{
body::Full,
extract::Path,
handler::Handler,
http::{header, HeaderValue, StatusCode},
response::{self, Html, IntoResponse, Response},
routing, Extension, Form, Json, Router,
response::{self, IntoResponse, Response},
routing, Extension, Form, Router,
};
use mime_guess::mime;
use tower_cookies::{Cookie, Cookies};
use crate::svc::{
auth::{AuthError, Claims},
profiles,
};
use crate::svc::auth::{AuthError, Claims};
use super::{
servek::{Server, ServerError},
@ -45,15 +41,26 @@ impl Server {
.fallback(routing::get(Self::handler_404))
}
fn from_cookie(&self, cookie: Option<Cookie>) -> Result<WithNav<Option<Claims>>, ServerError> {
if let Some(cookie) = cookie {
let claims = self.auth.get_claims(cookie.value().to_owned())?;
fn from_cookies(&self, cookies: Cookies) -> Result<WithNav<Option<Claims>>, ServerError> {
const logged_out: Result<WithNav<Option<Claims>>, ServerError> = Ok(WithNav {
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()))
} else {
Ok(WithNav {
nav_type: NavType::LoggedOut,
obj: None,
})
logged_out
}
}
@ -61,7 +68,7 @@ impl Server {
Extension(srv): Extension<Server>,
cookies: Cookies,
) -> Result<impl IntoResponse, ServerError> {
let user = srv.from_cookie(cookies.get(AUTH_COOKIE_NAME))?;
let user = srv.from_cookies(cookies)?;
Ok((
StatusCode::OK,
response::Html(srv.hb.render("index", &user)?),
@ -89,10 +96,7 @@ impl Server {
) -> Result<impl IntoResponse, ServerError> {
Ok((
StatusCode::OK,
response::Html(
srv.hb
.render("404", &srv.from_cookie(cookies.get(AUTH_COOKIE_NAME))?)?,
),
response::Html(srv.hb.render("404", &srv.from_cookies(cookies)?)?),
))
}
@ -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() {
cookies.remove(Cookie::new(AUTH_COOKIE_NAME, ""));
@ -178,7 +196,7 @@ impl Server {
"profile",
&WithNav::new(
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())
}
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 {
InvalidCredentials,
Expired,
ServerError(String),
}
impl AuthError {
pub fn expired(self) -> bool {
self == Self::Expired
}
}
impl From<DBError> for AuthError {
fn from(_: DBError) -> Self {
Self::InvalidCredentials
@ -109,6 +116,9 @@ impl From<DBError> for AuthError {
impl From<jsonwebtoken::errors::Error> for AuthError {
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,
password_hash: sec::hash(password),
email,
avatar_uri: None,
bio: None,
})
.await?;
@ -55,15 +57,26 @@ pub struct User {
pub username: String,
pub display_name: Option<String>,
pub host: Option<String>,
pub avatar_uri: Option<String>,
pub bio: Option<String>,
}
impl From<users::User> for User {
fn from(u: users::User) -> Self {
Self {
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,
display_name: u.display_name,
host: u.host,
avatar_uri: u.avatar_uri,
bio: u.bio,
}
}
}

View File

@ -3,7 +3,6 @@ body {
color: rebeccapurple;
}
p,
h1,
h2,
label {
@ -62,11 +61,58 @@ a {
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 {
/* background-color: #333; */
margin: 0;
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 {

View File

@ -2,14 +2,24 @@
<html>
<head>
<title>@{{username}}</title>
<title>@{{obj.username}}</title>
<link rel="stylesheet" href="/static/style/main.css">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
</head>
<body>
{{> (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>
</html>