Added session system

This commit is contained in:
xeovalyte 2024-06-05 14:14:57 +02:00
parent f1cb209217
commit 5f31e4bf22
No known key found for this signature in database
5 changed files with 241 additions and 238 deletions

35
Cargo.lock generated
View File

@ -317,6 +317,29 @@ dependencies = [
"tracing",
]
[[package]]
name = "axum-extra"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0be6ea09c9b96cb5076af0de2e383bd2bc0c18f827cf1967bdd353e0b910d733"
dependencies = [
"axum",
"axum-core",
"bytes",
"cookie",
"futures-util",
"http 1.1.0",
"http-body 1.0.0",
"http-body-util",
"mime",
"pin-project-lite",
"serde",
"tower",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "axum-macros"
version = "0.4.1"
@ -822,6 +845,17 @@ dependencies = [
"unicode-segmentation",
]
[[package]]
name = "cookie"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
dependencies = [
"percent-encoding",
"time",
"version_check",
]
[[package]]
name = "core-foundation"
version = "0.9.4"
@ -5556,6 +5590,7 @@ version = "0.1.0"
dependencies = [
"argon2",
"axum",
"axum-extra",
"dioxus",
"dioxus-logger",
"once_cell",

View File

@ -16,6 +16,7 @@ once_cell = "1.19.0"
tokio = { version = "1.38.0", features = [ "macros", "rt-multi-thread" ], optional = true }
argon2 = { version = "0.5.3", optional = true }
axum = { version = "0.7.5", optional = true }
axum-extra = { version = "0.9.3", features = [ "cookie" ], optional = true}
# Debug
tracing = "0.1.40"
@ -23,5 +24,5 @@ dioxus-logger = "0.5.0"
[features]
default = []
server = ["dioxus/axum", "axum", "surrealdb", "tokio", "argon2" ]
server = ["dioxus/axum", "axum", "axum-extra", "surrealdb", "tokio", "argon2" ]
web = ["dioxus/web"]

View File

@ -766,15 +766,6 @@ html {
color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));
}
.menu li > *:not(ul, .menu-title, details, .btn):active,
.menu li > *:not(ul, .menu-title, details, .btn).active,
.menu li > details > summary:active {
--tw-bg-opacity: 1;
background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));
--tw-text-opacity: 1;
color: var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity)));
}
.tab:hover {
--tw-text-opacity: 1;
}
@ -783,6 +774,12 @@ html {
--tw-text-opacity: 1;
color: var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)));
}
.table tr.hover:hover,
.table tr.hover:nth-child(even):hover {
--tw-bg-opacity: 1;
background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));
}
}
.btn {
@ -980,18 +977,6 @@ html {
}
}
:where(.menu li:not(.menu-title, .disabled) > *:not(ul, details, .menu-title)):not(.active, .btn):hover, :where(.menu li:not(.menu-title, .disabled) > details > summary:not(.menu-title)):not(.active, .btn):hover {
cursor: pointer;
outline: 2px solid transparent;
outline-offset: 2px;
}
@supports (color: oklch(0% 0 0)) {
:where(.menu li:not(.menu-title, .disabled) > *:not(ul, details, .menu-title)):not(.active, .btn):hover, :where(.menu li:not(.menu-title, .disabled) > details > summary:not(.menu-title)):not(.active, .btn):hover {
background-color: var(--fallback-bc,oklch(var(--bc)/0.1));
}
}
.tab[disabled],
.tab[disabled]:hover {
cursor: not-allowed;
@ -1066,12 +1051,6 @@ html {
margin-inline-end: -1rem;
}
.input-sm[type="number"]::-webkit-inner-spin-button {
margin-top: 0px;
margin-bottom: 0px;
margin-inline-end: -0px;
}
.link {
cursor: pointer;
text-decoration-line: underline;
@ -1081,59 +1060,6 @@ html {
text-decoration-line: none;
}
.menu {
display: flex;
flex-direction: column;
flex-wrap: wrap;
font-size: 0.875rem;
line-height: 1.25rem;
padding: 0.5rem;
}
.menu :where(li ul) {
position: relative;
white-space: nowrap;
margin-inline-start: 1rem;
padding-inline-start: 0.5rem;
}
.menu :where(li:not(.menu-title) > *:not(ul, details, .menu-title, .btn)), .menu :where(li:not(.menu-title) > details > summary:not(.menu-title)) {
display: grid;
grid-auto-flow: column;
align-content: flex-start;
align-items: center;
gap: 0.5rem;
grid-auto-columns: minmax(auto, max-content) auto max-content;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
.menu li.disabled {
cursor: not-allowed;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
color: var(--fallback-bc,oklch(var(--bc)/0.3));
}
.menu :where(li > .menu-dropdown:not(.menu-dropdown-show)) {
display: none;
}
:where(.menu li) {
position: relative;
display: flex;
flex-shrink: 0;
flex-direction: column;
flex-wrap: wrap;
align-items: stretch;
}
:where(.menu li) .badge {
justify-self: end;
}
.navbar {
display: flex;
align-items: center;
@ -1147,11 +1073,6 @@ html {
align-items: center;
}
.navbar-end {
width: 50%;
justify-content: flex-end;
}
.tabs {
display: grid;
align-items: flex-end;
@ -1216,6 +1137,39 @@ input.tab:checked + .tab-content,
display: block;
}
.table {
position: relative;
width: 100%;
border-radius: var(--rounded-box, 1rem);
text-align: left;
font-size: 0.875rem;
line-height: 1.25rem;
}
.table :where(.table-pin-rows thead tr) {
position: sticky;
top: 0px;
z-index: 1;
--tw-bg-opacity: 1;
background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));
}
.table :where(.table-pin-rows tfoot tr) {
position: sticky;
bottom: 0px;
z-index: 1;
--tw-bg-opacity: 1;
background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));
}
.table :where(.table-pin-cols tr th) {
position: sticky;
left: 0px;
right: 0px;
--tw-bg-opacity: 1;
background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));
}
.btm-nav > * .label {
font-size: 1rem;
line-height: 1.5rem;
@ -1483,88 +1437,6 @@ input.tab:checked + .tab-content,
outline-offset: 2px;
}
:where(.menu li:empty) {
--tw-bg-opacity: 1;
background-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));
opacity: 0.1;
margin: 0.5rem 1rem;
height: 1px;
}
.menu :where(li ul):before {
position: absolute;
bottom: 0.75rem;
inset-inline-start: 0px;
top: 0.75rem;
width: 1px;
--tw-bg-opacity: 1;
background-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));
opacity: 0.1;
content: "";
}
.menu :where(li:not(.menu-title) > *:not(ul, details, .menu-title, .btn)),
.menu :where(li:not(.menu-title) > details > summary:not(.menu-title)) {
border-radius: var(--rounded-btn, 0.5rem);
padding-left: 1rem;
padding-right: 1rem;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
text-align: start;
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
transition-duration: 200ms;
text-wrap: balance;
}
:where(.menu li:not(.menu-title, .disabled) > *:not(ul, details, .menu-title)):not(summary, .active, .btn).focus, :where(.menu li:not(.menu-title, .disabled) > *:not(ul, details, .menu-title)):not(summary, .active, .btn):focus, :where(.menu li:not(.menu-title, .disabled) > *:not(ul, details, .menu-title)):is(summary):not(.active, .btn):focus-visible, :where(.menu li:not(.menu-title, .disabled) > details > summary:not(.menu-title)):not(summary, .active, .btn).focus, :where(.menu li:not(.menu-title, .disabled) > details > summary:not(.menu-title)):not(summary, .active, .btn):focus, :where(.menu li:not(.menu-title, .disabled) > details > summary:not(.menu-title)):is(summary):not(.active, .btn):focus-visible {
cursor: pointer;
background-color: var(--fallback-bc,oklch(var(--bc)/0.1));
--tw-text-opacity: 1;
color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));
outline: 2px solid transparent;
outline-offset: 2px;
}
.menu li > *:not(ul, .menu-title, details, .btn):active,
.menu li > *:not(ul, .menu-title, details, .btn).active,
.menu li > details > summary:active {
--tw-bg-opacity: 1;
background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));
--tw-text-opacity: 1;
color: var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity)));
}
.menu :where(li > details > summary)::-webkit-details-marker {
display: none;
}
.menu :where(li > details > summary):after,
.menu :where(li > .menu-dropdown-toggle):after {
justify-self: end;
display: block;
margin-top: -0.5rem;
height: 0.5rem;
width: 0.5rem;
transform: rotate(45deg);
transition-property: transform, margin-top;
transition-duration: 0.3s;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
content: "";
transform-origin: 75% 75%;
box-shadow: 2px 2px;
pointer-events: none;
}
.menu :where(li > details[open] > summary):after,
.menu :where(li > .menu-dropdown-toggle.menu-dropdown-show):after {
transform: rotate(225deg);
margin-top: 0;
}
.mockup-browser .mockup-browser-toolbar .input {
position: relative;
margin-left: auto;
@ -1798,6 +1670,45 @@ input.tab:checked + .tab-content,
color: var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)));
}
:is([dir="rtl"] .table) {
text-align: right;
}
.table :where(th, td) {
padding-left: 1rem;
padding-right: 1rem;
padding-top: 0.75rem;
padding-bottom: 0.75rem;
vertical-align: middle;
}
.table tr.active,
.table tr.active:nth-child(even),
.table-zebra tbody tr:nth-child(even) {
--tw-bg-opacity: 1;
background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));
}
.table :where(thead tr, tbody tr:not(:last-child),tbody tr:first-child:last-child) {
border-bottom-width: 1px;
--tw-border-opacity: 1;
border-bottom-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));
}
.table :where(thead, tfoot) {
white-space: nowrap;
font-size: 0.75rem;
line-height: 1rem;
font-weight: 700;
color: var(--fallback-bc,oklch(var(--bc)/0.6));
}
.table :where(tfoot) {
border-top-width: 1px;
--tw-border-opacity: 1;
border-top-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));
}
@keyframes toast-pop {
0% {
transform: scale(0.9);
@ -1810,44 +1721,6 @@ input.tab:checked + .tab-content,
}
}
.btn-sm {
height: 2rem;
min-height: 2rem;
padding-left: 0.75rem;
padding-right: 0.75rem;
font-size: 0.875rem;
}
.btn-square:where(.btn-sm) {
height: 2rem;
width: 2rem;
padding: 0px;
}
.btn-circle:where(.btn-sm) {
height: 2rem;
width: 2rem;
border-radius: 9999px;
padding: 0px;
}
.input-sm {
height: 2rem;
padding-left: 0.75rem;
padding-right: 0.75rem;
font-size: 0.875rem;
line-height: 2rem;
}
.menu-horizontal {
display: inline-flex;
flex-direction: row;
}
.menu-horizontal > li:not(.menu-title) > details > ul {
position: absolute;
}
.tabs-md :where(.tab) {
height: 2rem;
font-size: 0.875rem;
@ -1890,27 +1763,6 @@ input.tab:checked + .tab-content,
line-height: 1.5rem;
}
.menu-horizontal > li:not(.menu-title) > details > ul {
margin-inline-start: 0px;
margin-top: 1rem;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
padding-inline-end: 0.5rem;
}
.menu-horizontal > li > details > ul:before {
content: none;
}
:where(.menu-horizontal > li:not(.menu-title) > details > ul) {
border-radius: var(--rounded-box, 1rem);
--tw-bg-opacity: 1;
background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));
--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.static {
position: static;
}
@ -1928,6 +1780,10 @@ input.tab:checked + .tab-content,
margin-top: 1.5rem;
}
.table {
display: table;
}
.w-full {
width: 100%;
}
@ -1963,16 +1819,16 @@ input.tab:checked + .tab-content,
padding-right: 0.25rem;
}
.py-6 {
padding-top: 1.5rem;
padding-bottom: 1.5rem;
}
.py-1 {
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
.py-6 {
padding-top: 1.5rem;
padding-bottom: 1.5rem;
}
.pt-40 {
padding-top: 10rem;
}

View File

@ -7,8 +7,9 @@ use surrealdb::sql::Thing;
#[derive(Debug, Serialize, Deserialize)]
pub struct User {
email: String,
password: String,
pub id: String,
pub email: String,
pub password: Option<String>,
}
#[cfg(feature = "server")]
@ -17,10 +18,92 @@ struct Record {
id: Thing,
}
#[derive(Debug, Deserialize)]
struct Session {
user_id: String,
id: String,
expires: i64,
token: String,
}
impl User {
#[cfg(feature = "server")]
async fn from_cookie() -> Result<Self, ServerFnError> {
use axum_extra::extract::CookieJar;
let jar: CookieJar = extract().await?;
let session_token = jar
.get("session_token")
.ok_or_else(|| ServerFnError::new("Session token cookie is not set"))?;
let session_token = session_token.value();
let mut res = DB
.query("SELECT type::string(user.id) as id, user.email as email FROM session WHERE token = $session_token")
.bind(("session_token", session_token))
.await?;
let user: Option<User> = res.take(0)?;
match user {
Some(u) => {
tracing::info!("Authorized session for {}", u.id);
Ok(u)
}
None => Err(ServerFnError::ServerError(
"Could not authorize session".to_string(),
)),
}
}
}
impl Session {
#[cfg(feature = "server")]
async fn create(user_id: &String) -> Result<Self, ServerFnError> {
// Create a session
let mut res = DB
.query("CREATE session SET user = type::thing($user_id) RETURN type::string(id) as id, type::string(id) as user_id, time::unix(expires) as expires, token;")
.bind(("user_id", user_id))
.await?;
let session: Option<Session> = res.take(0)?;
match session {
Some(s) => {
tracing::info!("Created new session for {}", user_id);
Ok(s)
}
None => Err(ServerFnError::ServerError(
"Could not generate session".to_string(),
)),
}
}
#[cfg(feature = "server")]
fn write(&self) -> Result<(), ServerFnError> {
use axum::http::{header, HeaderValue};
use axum_extra::extract::cookie::{Cookie, SameSite};
let mut cookie = Cookie::new("session_token", &self.token);
cookie.set_same_site(SameSite::Strict);
server_context()
.response_parts_mut()
.unwrap()
.headers
.insert(
header::SET_COOKIE,
HeaderValue::from_str(&cookie.to_string())?,
);
Ok(())
}
}
#[server(Register)]
pub async fn register(email: String, password: String) -> Result<String, ServerFnError> {
tracing::info!("Creating new user");
let mut res = DB
.query("CREATE user SET email = $email, password = crypto::argon2::generate($password)")
.bind(("email", email))
@ -32,6 +115,10 @@ pub async fn register(email: String, password: String) -> Result<String, ServerF
match user {
Some(Record { id }) => {
tracing::info!("Created new user ({id})");
let session = Session::create(&id.to_raw()).await?;
session.write()?;
Ok(id.to_string())
}
_ => Err(ServerFnError::ServerError("Could not get id".to_string())),
@ -40,8 +127,9 @@ pub async fn register(email: String, password: String) -> Result<String, ServerF
#[server(Signin)]
pub async fn signin(email: String, password: String) -> Result<User, ServerFnError> {
// Find the user with the correct email and password
let mut res = DB
.query("SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(password, $password)")
.query("SELECT type::string(id) as id, email, password FROM user WHERE email = $email AND crypto::argon2::compare(password, $password)")
.bind(("email", email))
.bind(("password", password))
.await?;
@ -51,8 +139,17 @@ pub async fn signin(email: String, password: String) -> Result<User, ServerFnErr
match user {
Some(u) => {
tracing::info!("User ({}) has signed in", u.email);
let session = Session::create(&u.id).await?;
session.write()?;
Ok(u)
}
_ => Err(ServerFnError::ServerError("Could not get id".to_string())),
}
}
#[server(GetUser)]
pub async fn get_current_user() -> Result<User, ServerFnError> {
User::from_cookie().await
}

View File

@ -15,16 +15,30 @@ pub async fn connect() -> surrealdb::Result<()> {
}
pub async fn define_schema() -> surrealdb::Result<()> {
// Define user table
let sql = "
DEFINE TABLE user SCHEMAFULL;
DEFINE FIELD email ON TABLE user TYPE string
ASSERT string::is::email($value);
ASSERT string::is::email($value) VALUE string::lowercase($value);
DEFINE FIELD password ON TABLE user TYPE string;
DEFINE INDEX userEmailIndex ON TABLE user COLUMNS email UNIQUE;
";
DB.query(sql).await?;
// Define session table
let sql = "
DEFINE TABLE session SCHEMAFULL;
DEFINE FIELD user ON TABLE session TYPE record;
DEFINE FIELD token ON TABLE session TYPE string VALUE rand::string(32);
DEFINE FIELD expires ON TABLE session TYPE datetime VALUE time::now() + 1w;
DEFINE INDEX sessionTokenIndex ON TABLE session COLUMNS token UNIQUE;
";
DB.query(sql).await?;
Ok(())
}