mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-13 18:12:27 +00:00
Some refactoring and app token management
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -3164,6 +3164,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"toml",
|
||||
"tracing",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -32,6 +32,7 @@ docker run \
|
||||
-p 4000:4000 \
|
||||
-v YOUR_DATA_DIR:/var/lib/rustical/ \
|
||||
-v YOUR_CONFIG_TOML:/etc/rustical/config.toml \
|
||||
-v YOUR_PRINCIPALS_TOML:/etc/rustical/principals.toml \
|
||||
ghcr.io/lennart-k/rustical
|
||||
```
|
||||
|
||||
@@ -77,7 +78,7 @@ id = "user"
|
||||
displayname = "User"
|
||||
password = "$argon2id$......."
|
||||
app_tokens = [
|
||||
{name = "Token", token = "$pbkdf2-sha256$........"},
|
||||
{id = "1", name = "Token", token = "$pbkdf2-sha256$........"},
|
||||
]
|
||||
memberships = ["group:amazing_group"]
|
||||
|
||||
@@ -107,8 +108,9 @@ docker run -it --rm ghcr.io/lennart-k/rustical rustical pwhash
|
||||
The password is meant as a password you use to log in to the frontend.
|
||||
Since it's sensitive information,
|
||||
the secure but slow hash algorithm `argon2` is chosen.
|
||||
If you've configured OpenID Connect you can also completely omit the password.
|
||||
|
||||
I recommend to generate random app tokens for each CalDAV/CardDAV client.
|
||||
I recommend to generate random app tokens for each CalDAV/CardDAV client (which can also be done through the frontend).
|
||||
These can use the faster `pbkdf2` algorithm.
|
||||
|
||||
### WebDAV Push
|
||||
|
||||
@@ -5,8 +5,30 @@
|
||||
|
||||
{% block content %}
|
||||
{% let name = calendar.displayname.to_owned().unwrap_or(calendar.id.to_owned()) %}
|
||||
<h1>{{ name }}</h1>
|
||||
<h1>{{ calendar.principal }}/{{ name }}</h1>
|
||||
{% if let Some(description) = calendar.description %}<p>{{ description }}</p>{% endif%}
|
||||
|
||||
{% if let Some(subscription_url) = calendar.subscription_url %}
|
||||
<h2>Subscription URL</h2>
|
||||
<a href="{{ subscription_url }}">{{ subscription_url }}</a>
|
||||
{% endif %}
|
||||
|
||||
<h2>Components</h2>
|
||||
<ul>
|
||||
{% for comp in calendar.components %}
|
||||
<li>{{ comp.as_str() }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<h2>Timezone</h2>
|
||||
|
||||
{% if let Some(timezone_id) = calendar.timezone_id %}
|
||||
<p>{{ timezone_id }}</p>
|
||||
{% endif %}
|
||||
{% if let Some(timezone) = calendar.timezone %}
|
||||
<pre>{{ timezone }}</pre>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<pre>{{ calendar|json }}</pre>
|
||||
{% endblock %}
|
||||
{%endblock %}
|
||||
|
||||
@@ -72,24 +72,47 @@ li.collection-list-item {
|
||||
|
||||
<h3>App tokens</h3>
|
||||
|
||||
<ul>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Created at</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
{% for app_token in user.app_tokens %}
|
||||
<li>
|
||||
{{ app_token.name }}
|
||||
{% if let Some(created_at) = app_token.created_at %}
|
||||
{{ created_at.to_rfc3339() }}
|
||||
{% endif %}
|
||||
</li>
|
||||
<tr>
|
||||
<td>{{ app_token.name }}</td>
|
||||
<td>
|
||||
{% if let Some(created_at) = app_token.created_at %}
|
||||
{{ created_at.to_rfc3339() }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<form action="/frontend/user/{{ user.id }}/app_token/{{ app_token.id }}/delete" method="POST">
|
||||
<button type="submit">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<tr>
|
||||
<form action="/frontend/user/{{ user.id }}/app_token" method="POST">
|
||||
<td>
|
||||
<label for="generate_app_token_name">App name</label>
|
||||
<input type="text" name="name" id="generate_app_token_name" />
|
||||
</td>
|
||||
<td></td>
|
||||
<td>
|
||||
<button type="submit">Generate</button>
|
||||
</td>
|
||||
</form>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h2>Calendars</h2>
|
||||
<ul>
|
||||
{% for calendar in calendars %}
|
||||
{% let color = calendar.color.to_owned().unwrap_or("red".to_owned()) %}
|
||||
<li class="collection-list-item" style="--color: {{ color }}">
|
||||
<a href="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}">
|
||||
<a href="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id }}">
|
||||
<span class="title">{{ calendar.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }}</span>
|
||||
<span class="description">
|
||||
{% if let Some(description) = calendar.description %}{{ description }}{% endif %}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use actix_session::{
|
||||
SessionMiddleware, config::CookieContentSecurity, storage::CookieSessionStore,
|
||||
SessionMiddleware,
|
||||
config::CookieContentSecurity,
|
||||
storage::{CookieSessionStore, SessionStore},
|
||||
};
|
||||
use actix_web::{
|
||||
HttpRequest, HttpResponse, Responder,
|
||||
@@ -7,12 +9,13 @@ use actix_web::{
|
||||
dev::ServiceResponse,
|
||||
http::{Method, StatusCode},
|
||||
middleware::{ErrorHandlerResponse, ErrorHandlers},
|
||||
web::{self, Data, Path, Redirect},
|
||||
web::{self, Data, Form, Path, Redirect},
|
||||
};
|
||||
use askama::Template;
|
||||
use askama_web::WebTemplate;
|
||||
use assets::{Assets, EmbedService};
|
||||
use oidc::{route_get_oidc_callback, route_post_oidc};
|
||||
use rand::{Rng, distributions::Alphanumeric};
|
||||
use routes::{
|
||||
addressbook::{route_addressbook, route_addressbook_restore},
|
||||
calendar::{route_calendar, route_calendar_restore},
|
||||
@@ -22,6 +25,7 @@ use rustical_store::{
|
||||
Addressbook, AddressbookStore, Calendar, CalendarStore,
|
||||
auth::{AuthenticationMiddleware, AuthenticationProvider, User},
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
|
||||
mod assets;
|
||||
@@ -32,6 +36,14 @@ mod routes;
|
||||
|
||||
pub use config::FrontendConfig;
|
||||
|
||||
pub fn generate_app_token() -> String {
|
||||
rand::thread_rng()
|
||||
.sample_iter(Alphanumeric)
|
||||
.map(char::from)
|
||||
.take(64)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Template, WebTemplate)]
|
||||
#[template(path = "pages/user.html")]
|
||||
struct UserPage {
|
||||
@@ -105,6 +117,36 @@ async fn route_root(user: Option<User>, req: HttpRequest) -> impl Responder {
|
||||
web::Redirect::to(redirect_url.to_string()).permanent()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub(crate) struct PostAppTokenForm {
|
||||
name: String,
|
||||
}
|
||||
|
||||
async fn route_post_app_token<AP: AuthenticationProvider>(
|
||||
user: User,
|
||||
auth_provider: Data<AP>,
|
||||
path: Path<String>,
|
||||
Form(PostAppTokenForm { name }): Form<PostAppTokenForm>,
|
||||
) -> Result<impl Responder, rustical_store::Error> {
|
||||
assert_eq!(path.into_inner(), user.id);
|
||||
let token = generate_app_token();
|
||||
auth_provider
|
||||
.add_app_token(&user.id, name, token.clone())
|
||||
.await?;
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
async fn route_delete_app_token<AP: AuthenticationProvider>(
|
||||
user: User,
|
||||
auth_provider: Data<AP>,
|
||||
path: Path<(String, String)>,
|
||||
) -> Result<Redirect, rustical_store::Error> {
|
||||
let (path_user, token_id) = path.into_inner();
|
||||
assert_eq!(path_user, user.id);
|
||||
auth_provider.remove_app_token(&user.id, &token_id).await?;
|
||||
Ok(Redirect::to("/frontend/user").see_other())
|
||||
}
|
||||
|
||||
pub(crate) fn unauthorized_handler<B>(
|
||||
res: ServiceResponse<B>,
|
||||
) -> actix_web::Result<ErrorHandlerResponse<B>> {
|
||||
@@ -136,6 +178,14 @@ pub(crate) fn unauthorized_handler<B>(
|
||||
Ok(ErrorHandlerResponse::Response(res))
|
||||
}
|
||||
|
||||
pub fn session_middleware(frontend_secret: [u8; 64]) -> SessionMiddleware<impl SessionStore> {
|
||||
SessionMiddleware::builder(CookieSessionStore::default(), Key::from(&frontend_secret))
|
||||
.cookie_secure(true)
|
||||
.cookie_same_site(SameSite::Strict)
|
||||
.cookie_content_security(CookieContentSecurity::Private)
|
||||
.build()
|
||||
}
|
||||
|
||||
pub fn configure_frontend<AP: AuthenticationProvider, CS: CalendarStore, AS: AddressbookStore>(
|
||||
cfg: &mut web::ServiceConfig,
|
||||
auth_provider: Arc<AP>,
|
||||
@@ -147,16 +197,7 @@ pub fn configure_frontend<AP: AuthenticationProvider, CS: CalendarStore, AS: Add
|
||||
web::scope("")
|
||||
.wrap(ErrorHandlers::new().handler(StatusCode::UNAUTHORIZED, unauthorized_handler))
|
||||
.wrap(AuthenticationMiddleware::new(auth_provider.clone()))
|
||||
.wrap(
|
||||
SessionMiddleware::builder(
|
||||
CookieSessionStore::default(),
|
||||
Key::from(&frontend_config.secret_key),
|
||||
)
|
||||
.cookie_secure(true)
|
||||
.cookie_same_site(SameSite::Strict)
|
||||
.cookie_content_security(CookieContentSecurity::Private)
|
||||
.build(),
|
||||
)
|
||||
.wrap(session_middleware(frontend_config.secret_key))
|
||||
.app_data(Data::from(auth_provider))
|
||||
.app_data(Data::from(cal_store.clone()))
|
||||
.app_data(Data::from(addr_store.clone()))
|
||||
@@ -173,6 +214,14 @@ pub fn configure_frontend<AP: AuthenticationProvider, CS: CalendarStore, AS: Add
|
||||
.route(web::method(Method::GET).to(route_user_named::<CS, AS>))
|
||||
.name("frontend_user_named"),
|
||||
)
|
||||
.service(
|
||||
web::resource("/user/{user}/app_token")
|
||||
.route(web::method(Method::POST).to(route_post_app_token::<AP>)),
|
||||
)
|
||||
.service(
|
||||
web::resource("/user/{user}/app_token/{id}/delete")
|
||||
.route(web::method(Method::POST).to(route_delete_app_token::<AP>)),
|
||||
)
|
||||
.service(
|
||||
web::resource("/user/{user}/calendar/{calendar}")
|
||||
.route(web::method(Method::GET).to(route_calendar::<CS>)),
|
||||
|
||||
78
crates/frontend/src/nextcloud_login/mod.rs
Normal file
78
crates/frontend/src/nextcloud_login/mod.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
use actix_web::{
|
||||
http::StatusCode,
|
||||
middleware::ErrorHandlers,
|
||||
web::{self, Data, ServiceConfig},
|
||||
};
|
||||
use chrono::{DateTime, Utc};
|
||||
use routes::{get_nextcloud_flow, post_nextcloud_flow, post_nextcloud_login, post_nextcloud_poll};
|
||||
use rustical_store::auth::{AuthenticationMiddleware, AuthenticationProvider};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
mod routes;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct NextcloudFlow {
|
||||
app_name: String,
|
||||
created_at: DateTime<Utc>,
|
||||
token: String,
|
||||
response: Option<NextcloudSuccessResponse>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct NextcloudSuccessResponse {
|
||||
server: String,
|
||||
login_name: String,
|
||||
app_password: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct NextcloudLoginResponse {
|
||||
poll: NextcloudLoginPoll,
|
||||
login: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct NextcloudLoginPoll {
|
||||
token: String,
|
||||
endpoint: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct NextcloudFlows {
|
||||
flows: RwLock<HashMap<String, NextcloudFlow>>,
|
||||
}
|
||||
|
||||
use crate::{session_middleware, unauthorized_handler};
|
||||
|
||||
pub fn configure_nextcloud_login<AP: AuthenticationProvider>(
|
||||
cfg: &mut ServiceConfig,
|
||||
nextcloud_flows_state: Arc<NextcloudFlows>,
|
||||
auth_provider: Arc<AP>,
|
||||
frontend_secret: [u8; 64],
|
||||
) {
|
||||
cfg.service(
|
||||
web::scope("/index.php/login/v2")
|
||||
.wrap(ErrorHandlers::new().handler(StatusCode::UNAUTHORIZED, unauthorized_handler))
|
||||
.wrap(AuthenticationMiddleware::new(auth_provider.clone()))
|
||||
.wrap(session_middleware(frontend_secret))
|
||||
.app_data(Data::from(nextcloud_flows_state))
|
||||
.app_data(Data::from(auth_provider.clone()))
|
||||
.service(web::resource("").post(post_nextcloud_login))
|
||||
.service(
|
||||
web::resource("/poll/{flow}")
|
||||
.name("nc_login_poll")
|
||||
.post(post_nextcloud_poll::<AP>),
|
||||
)
|
||||
.service(
|
||||
web::resource("/flow/{flow}")
|
||||
.name("nc_login_flow")
|
||||
.get(get_nextcloud_flow)
|
||||
.post(post_nextcloud_flow),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1,55 +1,21 @@
|
||||
use actix_session::{
|
||||
SessionMiddleware, config::CookieContentSecurity, storage::CookieSessionStore,
|
||||
use crate::generate_app_token;
|
||||
|
||||
use super::{
|
||||
NextcloudFlow, NextcloudFlows, NextcloudLoginPoll, NextcloudLoginResponse,
|
||||
NextcloudSuccessResponse,
|
||||
};
|
||||
use actix_web::{
|
||||
HttpRequest, HttpResponse, Responder,
|
||||
cookie::{Key, SameSite},
|
||||
http::{
|
||||
StatusCode,
|
||||
header::{self},
|
||||
},
|
||||
middleware::ErrorHandlers,
|
||||
web::{self, Data, Form, Html, Json, Path, ServiceConfig},
|
||||
http::header::{self},
|
||||
web::{Data, Form, Html, Json, Path},
|
||||
};
|
||||
use askama::Template;
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use rand::{Rng, distributions::Alphanumeric};
|
||||
use rustical_store::auth::{AuthenticationMiddleware, AuthenticationProvider, User};
|
||||
use chrono::{Duration, Utc};
|
||||
use rustical_store::auth::{AuthenticationProvider, User};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::unauthorized_handler;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct NextcloudFlow {
|
||||
app_name: String,
|
||||
created_at: DateTime<Utc>,
|
||||
token: String,
|
||||
response: Option<NextcloudSuccessResponse>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct NextcloudFlows {
|
||||
flows: RwLock<HashMap<String, NextcloudFlow>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct NextcloudLoginPoll {
|
||||
token: String,
|
||||
endpoint: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct NextcloudLoginResponse {
|
||||
poll: NextcloudLoginPoll,
|
||||
login: String,
|
||||
}
|
||||
|
||||
async fn post_nextcloud_login(
|
||||
pub(crate) async fn post_nextcloud_login(
|
||||
req: HttpRequest,
|
||||
state: Data<NextcloudFlows>,
|
||||
) -> Json<NextcloudLoginResponse> {
|
||||
@@ -95,19 +61,11 @@ async fn post_nextcloud_login(
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct NextcloudSuccessResponse {
|
||||
server: String,
|
||||
login_name: String,
|
||||
app_password: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct NextcloudPollForm {
|
||||
pub(crate) struct NextcloudPollForm {
|
||||
token: String,
|
||||
}
|
||||
|
||||
async fn post_nextcloud_poll<AP: AuthenticationProvider>(
|
||||
pub(crate) async fn post_nextcloud_poll<AP: AuthenticationProvider>(
|
||||
state: Data<NextcloudFlows>,
|
||||
form: Form<NextcloudPollForm>,
|
||||
path: Path<String>,
|
||||
@@ -143,14 +101,6 @@ async fn post_nextcloud_poll<AP: AuthenticationProvider>(
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_app_token() -> String {
|
||||
rand::thread_rng()
|
||||
.sample_iter(Alphanumeric)
|
||||
.map(char::from)
|
||||
.take(64)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "pages/nextcloud_login/form.html")]
|
||||
struct NextcloudLoginPage {
|
||||
@@ -159,7 +109,7 @@ struct NextcloudLoginPage {
|
||||
}
|
||||
|
||||
#[instrument(skip(state, req))]
|
||||
async fn get_nextcloud_flow(
|
||||
pub(crate) async fn get_nextcloud_flow(
|
||||
user: User,
|
||||
state: Data<NextcloudFlows>,
|
||||
path: Path<String>,
|
||||
@@ -183,7 +133,7 @@ async fn get_nextcloud_flow(
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
struct NextcloudAuthorizeForm {
|
||||
pub(crate) struct NextcloudAuthorizeForm {
|
||||
app_name: String,
|
||||
}
|
||||
|
||||
@@ -194,7 +144,7 @@ struct NextcloudLoginSuccessPage {
|
||||
}
|
||||
|
||||
#[instrument(skip(state, req))]
|
||||
async fn post_nextcloud_flow(
|
||||
pub(crate) async fn post_nextcloud_flow(
|
||||
user: User,
|
||||
state: Data<NextcloudFlows>,
|
||||
path: Path<String>,
|
||||
@@ -222,40 +172,3 @@ async fn post_nextcloud_flow(
|
||||
Ok(HttpResponse::NotFound().body("Login flow not found"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn configure_nextcloud_login<AP: AuthenticationProvider>(
|
||||
cfg: &mut ServiceConfig,
|
||||
nextcloud_flows_state: Arc<NextcloudFlows>,
|
||||
auth_provider: Arc<AP>,
|
||||
frontend_secret: [u8; 64],
|
||||
) {
|
||||
cfg.service(
|
||||
web::scope("/index.php/login/v2")
|
||||
.wrap(ErrorHandlers::new().handler(StatusCode::UNAUTHORIZED, unauthorized_handler))
|
||||
.wrap(AuthenticationMiddleware::new(auth_provider.clone()))
|
||||
.wrap(
|
||||
SessionMiddleware::builder(
|
||||
CookieSessionStore::default(),
|
||||
Key::from(&frontend_secret),
|
||||
)
|
||||
.cookie_secure(true)
|
||||
.cookie_same_site(SameSite::Strict)
|
||||
.cookie_content_security(CookieContentSecurity::Private)
|
||||
.build(),
|
||||
)
|
||||
.app_data(Data::from(nextcloud_flows_state))
|
||||
.app_data(Data::from(auth_provider.clone()))
|
||||
.service(web::resource("").post(post_nextcloud_login))
|
||||
.service(
|
||||
web::resource("/poll/{flow}")
|
||||
.name("nc_login_poll")
|
||||
.post(post_nextcloud_poll::<AP>),
|
||||
)
|
||||
.service(
|
||||
web::resource("/flow/{flow}")
|
||||
.name("nc_login_flow")
|
||||
.get(get_nextcloud_flow)
|
||||
.post(post_nextcloud_flow),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -28,6 +28,7 @@ rustical_xml.workspace = true
|
||||
toml.workspace = true
|
||||
tokio.workspace = true
|
||||
rand.workspace = true
|
||||
uuid.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
rstest = { workspace = true }
|
||||
|
||||
@@ -9,7 +9,14 @@ pub trait AuthenticationProvider: 'static {
|
||||
async fn get_principal(&self, id: &str) -> Result<Option<User>, crate::Error>;
|
||||
async fn insert_principal(&self, user: User) -> Result<(), crate::Error>;
|
||||
async fn validate_user_token(&self, user_id: &str, token: &str) -> Result<Option<User>, Error>;
|
||||
async fn add_app_token(&self, user_id: &str, name: String, token: String) -> Result<(), Error>;
|
||||
/// Returns a token identifier
|
||||
async fn add_app_token(
|
||||
&self,
|
||||
user_id: &str,
|
||||
name: String,
|
||||
token: String,
|
||||
) -> Result<String, Error>;
|
||||
async fn remove_app_token(&self, user_id: &str, token_id: &str) -> Result<(), Error>;
|
||||
}
|
||||
|
||||
pub use middleware::AuthenticationMiddleware;
|
||||
|
||||
@@ -101,9 +101,16 @@ impl AuthenticationProvider for TomlPrincipalStore {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
async fn add_app_token(&self, user_id: &str, name: String, token: String) -> Result<(), Error> {
|
||||
/// Returns an identifier for the new app token
|
||||
async fn add_app_token(
|
||||
&self,
|
||||
user_id: &str,
|
||||
name: String,
|
||||
token: String,
|
||||
) -> Result<String, Error> {
|
||||
let mut principals = self.principals.write().await;
|
||||
if let Some(principal) = principals.get_mut(user_id) {
|
||||
let id = uuid::Uuid::new_v4().to_string();
|
||||
let salt = SaltString::generate(OsRng);
|
||||
let token_hash = pbkdf2::Pbkdf2
|
||||
.hash_password_customized(
|
||||
@@ -122,11 +129,23 @@ impl AuthenticationProvider for TomlPrincipalStore {
|
||||
name,
|
||||
token: token_hash,
|
||||
created_at: Some(chrono::Utc::now()),
|
||||
id: id.clone(),
|
||||
});
|
||||
self.save(principals.deref())?;
|
||||
Ok(())
|
||||
Ok(id)
|
||||
} else {
|
||||
Err(Error::NotFound)
|
||||
}
|
||||
}
|
||||
|
||||
async fn remove_app_token(&self, user_id: &str, token_id: &str) -> Result<(), Error> {
|
||||
let mut principals = self.principals.write().await;
|
||||
if let Some(principal) = principals.get_mut(user_id) {
|
||||
principal
|
||||
.app_tokens
|
||||
.retain(|AppToken { id, .. }| token_id != id);
|
||||
self.save(principals.deref())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ impl ValueSerialize for PrincipalType {
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct AppToken {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub token: String,
|
||||
pub created_at: Option<DateTime<Utc>>,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::{CalDateTime, EventObject, JournalObject, TodoObject};
|
||||
use crate::Error;
|
||||
use ical::parser::{ical::component::IcalTimeZone, Component};
|
||||
use ical::parser::{Component, ical::component::IcalTimeZone};
|
||||
use serde::Serialize;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::{collections::HashMap, io::BufReader};
|
||||
@@ -13,14 +13,19 @@ pub enum CalendarObjectType {
|
||||
Journal = 2,
|
||||
}
|
||||
|
||||
impl rustical_xml::ValueSerialize for CalendarObjectType {
|
||||
fn serialize(&self) -> String {
|
||||
impl CalendarObjectType {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
CalendarObjectType::Event => "VEVENT",
|
||||
CalendarObjectType::Todo => "VTODO",
|
||||
CalendarObjectType::Journal => "VJOURNAL",
|
||||
}
|
||||
.to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
impl rustical_xml::ValueSerialize for CalendarObjectType {
|
||||
fn serialize(&self) -> String {
|
||||
self.as_str().to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user