Replies: 2 comments 1 reply
-
I would refer to the cookies example for doing that. rspc leaves handling auth up to the end developer. I do intend to release an open-source authentication library that pairs nicely with rspc but it will be a while until I have the time to do that. My recommendation is to use session cookies like those shown in the example linked above to manage the user's session and then use middleware to check the auth state. The syntax for defining middleware is going to significantly change within the next week. If you can hold off I would start developing other features and I can send a more detailed auth example once those changes land. Below is a snippet from a project of min. It is incomplete but it should show enough of how I am temporality doing things. This code will also stop working when I release the 0.0.6 update. This auth system isn't "production ready" as I am using it until I can get the authentication library I talked about earlier released. Depending on the timeframe and requirements of your project you could potentially do the same. The below code is NOT production ready so do not use it with production data unless you put more work into securing it! Codeuse std::ops::Add;
use argon2::{hash_encoded, verify_encoded, Config};
use prisma_client_rust::chrono::{Duration, Utc};
use rand::{distributions::Alphanumeric, Rng};
use rspc::{selection, ErrorCode, Type};
use serde::Deserialize;
use time::OffsetDateTime;
use tower_cookies::Cookie;
use tracing::{error, info};
use crate::prisma::{account, account_session};
use super::{Router, RouterBuilder};
const SESSION_COOKIE_NAME: &str = "session";
const SESSION_HINT_COOKIE_NAME: &str = "session_hint";
#[derive(Deserialize, Type)]
struct RegisterArgs {
email: String,
password: String,
}
#[derive(Deserialize, Type)]
struct LoginArgs {
email: String,
password: String,
}
pub(crate) fn mount() -> RouterBuilder {
// TODO: This is a super crude auth API. Refer to https://github.com/oscartbeaumont/Mattrax/issues/3
Router::new()
.mutation("register", |t| {
t(|ctx, req: RegisterArgs| async move {
if req.email.len() > 254 {
return Err(rspc::Error::new(
ErrorCode::BadRequest,
"Invalid email!".into(),
));
}
if req.password.len() < 8 {
return Err(rspc::Error::new(
ErrorCode::BadRequest,
"Password must be at least 8 characters long!".into(),
));
}
if req.password.len() > 254 {
return Err(rspc::Error::new(
ErrorCode::BadRequest,
"Password must be at most 254 characters long!".into(),
));
}
let salt: [u8; 32] = rand::thread_rng().gen();
let config = Config::default();
let hashed_password = hash_encoded(req.password.as_bytes(), &salt, &config)
.map_err(|err| {
error!("Error hashing password: {err}");
rspc::Error::new(
ErrorCode::InternalServerError,
"Internal server error".into(),
)
})?;
ctx.db
.account()
.create(
req.email.clone(),
vec![account::password::set(Some(hashed_password))],
)
.exec()
.await?;
info!("Registered account with email '{}'", req.email);
Ok(())
})
})
.mutation("login", |t| {
t(|ctx, req: LoginArgs| async move {
if req.email.len() > 254 {
return Err(rspc::Error::new(
ErrorCode::BadRequest,
"Invalid credentials!".into(),
));
}
if req.password.len() < 8 {
return Err(rspc::Error::new(
ErrorCode::BadRequest,
"Invalid credentials!".into(),
));
}
let account = ctx
.db
.account()
.find_unique(account::email::equals(req.email.clone()))
.exec()
.await?;
let account = match account {
Some(account) => account,
None => {
return Err(rspc::Error::new(
ErrorCode::BadRequest,
"Invalid credentials!".into(),
))
}
};
match account.password {
Some(password) => {
if !verify_encoded(&password, &req.password.into_bytes()).map_err(
|err| {
error!("Error verifying hashed password: {err}");
rspc::Error::new(
ErrorCode::InternalServerError,
"Internal server error".into(),
)
},
)? {
return Err(rspc::Error::new(
ErrorCode::BadRequest,
"Invalid credentials!".into(),
));
}
}
None => {
return Err(rspc::Error::new(
ErrorCode::BadRequest,
"Your account is disabled!".into(),
))
}
}
// TODO: Maybe set the Prisma column to fixed size of 15 chars?
let session_token: String = rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(15)
.map(char::from)
.collect();
let session = ctx
.db
.account_session()
.create(
session_token.clone(),
Utc::now().add(Duration::days(1)).into(),
account::id::equals(account.id),
vec![],
)
.exec()
.await?;
info!(
"Authenticated account '{}' and issued session '{}'",
req.email, session.id
);
let session_expiry = OffsetDateTime::now_utc().add(time::Duration::DAY);
let mut cookie = Cookie::new(SESSION_COOKIE_NAME, session_token);
cookie.set_expires(Some(session_expiry.clone()));
cookie.set_http_only(Some(true));
cookie.set_path("/");
ctx.cookies.add(cookie);
let mut cookie = Cookie::new(SESSION_HINT_COOKIE_NAME, "1");
cookie.set_expires(Some(session_expiry));
cookie.set_path("/");
ctx.cookies.add(cookie);
Ok(())
})
})
// .mutation("logout", |_, _: ()| "todo")
// .mutation("deleteMe", |_, _: ()| "todo")
// .mutation("editMe", |_, _: ()| "todo")
.query("me", |t| {
t(|ctx, _: ()| async move {
let session = match ctx.cookies.get(SESSION_COOKIE_NAME) {
Some(session) => {
// TODO: Don't fetch everything
ctx.db
.account_session()
.find_unique(account_session::session_token::equals(
session.value().to_string(),
))
.include(account_session::include!({
account: select {
first_name
last_name
email
}
}))
.exec()
.await?
}
None => {
// Note: this doesn't work over websockets
ctx.cookies.get(SESSION_HINT_COOKIE_NAME).map(|_| {
println!("HERE2");
ctx.cookies
.remove(Cookie::new(SESSION_HINT_COOKIE_NAME, ""))
});
None
}
};
let session = session.ok_or_else(|| {
rspc::Error::new(ErrorCode::Unauthorized, "Unauthorized".into())
})?;
let session = session.account.clone();
Ok(selection!(session, {
first_name,
last_name,
email
}))
})
})
} |
Beta Was this translation helpful? Give feedback.
-
i found this example if very good for authentification + token ...etc so i hope to integrate it as example with rspc |
Beta Was this translation helpful? Give feedback.
-
Any example for authentication and protect path ?
Beta Was this translation helpful? Give feedback.
All reactions