-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add a store for user preferences
Closes #926
- Loading branch information
Showing
17 changed files
with
772 additions
and
7 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
use sea_orm::entity::prelude::*; | ||
|
||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] | ||
#[sea_orm(table_name = "user_preferences")] | ||
pub struct Model { | ||
#[sea_orm(primary_key)] | ||
pub user_id: String, | ||
#[sea_orm(primary_key)] | ||
pub key: String, | ||
|
||
pub revision: Uuid, | ||
pub data: serde_json::Value, | ||
} | ||
|
||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] | ||
pub enum Relation {} | ||
|
||
impl ActiveModelBehavior for ActiveModel {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
use sea_orm_migration::prelude::*; | ||
|
||
#[derive(DeriveMigrationName)] | ||
pub struct Migration; | ||
|
||
#[async_trait::async_trait] | ||
impl MigrationTrait for Migration { | ||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { | ||
manager | ||
.create_table( | ||
Table::create() | ||
.table(UserPreferences::Table) | ||
.col( | ||
ColumnDef::new(UserPreferences::UserId) | ||
.string() | ||
.not_null() | ||
.to_owned(), | ||
) | ||
.col( | ||
ColumnDef::new(UserPreferences::Key) | ||
.string() | ||
.not_null() | ||
.to_owned(), | ||
) | ||
.col( | ||
ColumnDef::new(UserPreferences::Revision) | ||
.uuid() | ||
.not_null() | ||
.to_owned(), | ||
) | ||
.col( | ||
ColumnDef::new(UserPreferences::Data) | ||
.json_binary() | ||
.not_null() | ||
.to_owned(), | ||
) | ||
.primary_key( | ||
Index::create() | ||
.col(UserPreferences::UserId) | ||
.col(UserPreferences::Key), | ||
) | ||
.to_owned(), | ||
) | ||
.await?; | ||
|
||
Ok(()) | ||
} | ||
|
||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { | ||
manager | ||
.drop_table(Table::drop().table(UserPreferences::Table).to_owned()) | ||
.await?; | ||
|
||
Ok(()) | ||
} | ||
} | ||
|
||
#[derive(DeriveIden)] | ||
enum UserPreferences { | ||
Table, | ||
UserId, | ||
Key, | ||
Revision, | ||
Data, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
[package] | ||
name = "trustify-module-user" | ||
version.workspace = true | ||
edition.workspace = true | ||
publish.workspace = true | ||
license.workspace = true | ||
|
||
[dependencies] | ||
trustify-auth = { workspace = true } | ||
trustify-common = { workspace = true } | ||
trustify-entity = { workspace = true } | ||
|
||
actix-web = { workspace = true } | ||
anyhow = { workspace = true } | ||
sea-orm = { workspace = true, features = ["sea-query-binder", "sqlx-postgres", "runtime-tokio-rustls", "macros", "debug-print"] } | ||
sea-query = { workspace = true } | ||
serde_json = { workspace = true } | ||
thiserror = { workspace = true } | ||
utoipa = { workspace = true, features = ["actix_extras", "time", "url"] } | ||
utoipa-actix-web = { workspace = true } | ||
|
||
[dev-dependencies] | ||
trustify-test-context = { workspace = true } | ||
|
||
actix-http = { workspace = true } | ||
test-context = { workspace = true } | ||
test-log = { workspace = true, features = ["log", "trace"] } | ||
tokio = { workspace = true, features = ["full"] } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
# User management | ||
|
||
## Store user preferences | ||
|
||
This will create or replace the existing value. | ||
|
||
```bash | ||
http PUT localhost:8080/api/v1/userPreferences/foo key=value | ||
``` | ||
|
||
## Get user preferences | ||
|
||
```bash | ||
http GET localhost:8080/api/v1/userPreferences/foo | ||
``` | ||
|
||
## Delete user preferences | ||
|
||
```bash | ||
http DELETE localhost:8080/api/v1/userPreferences/foo | ||
``` | ||
|
||
## Conditionally update user preferences | ||
|
||
It is possible to update the value only if the expected precondition matches. Meaning that you can provide an | ||
etag value as received from a `GET` or `PUT`, and the update will only take place if the revision/etag value hasn't | ||
been changed yet by another update. If it did, the request will fail with a `412 Precondition failed` status and | ||
appropriate action would be to re-fetch the data, apply the update again, and request another store operation. This | ||
is also called "optimistic locking." | ||
|
||
```bash | ||
http PUT localhost:8080/api/v1/userPreferences/foo key=value 'If-Match:"<etag from get>"' | ||
``` | ||
|
||
**NOTE:** Be sure that the `If-Match` value is actually quoted, e.g. `If-Match:"79e5d346-876f-42b6-b0d0-51ee1be73a4c"` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
use crate::service::{Error, UserPreferenceService}; | ||
use actix_web::{ | ||
delete, get, | ||
http::header::{self, ETag, EntityTag, IfMatch}, | ||
put, web, HttpResponse, Responder, | ||
}; | ||
use trustify_auth::authenticator::user::UserDetails; | ||
use trustify_common::{db::Database, model::Revisioned}; | ||
|
||
/// mount the "user" module | ||
pub fn configure(svc: &mut utoipa_actix_web::service_config::ServiceConfig, db: Database) { | ||
svc.app_data(web::Data::new(UserPreferenceService::new(db))) | ||
.service(set) | ||
.service(get) | ||
.service(delete); | ||
} | ||
|
||
#[utoipa::path( | ||
tag = "userPreferences", | ||
operation_id = "getUserPreferences", | ||
params( | ||
("key", Path, description = "The key to the user preferences"), | ||
), | ||
responses( | ||
( | ||
status = 200, | ||
description = "User preference stored under this key", | ||
body = serde_json::Value, | ||
headers( | ||
("etag" = String, description = "Revision ID") | ||
) | ||
), | ||
(status = 404, description = "Unknown user preference key"), | ||
) | ||
)] | ||
#[get("/v1/userPreference/{key}")] | ||
/// Get user preferences | ||
async fn get( | ||
service: web::Data<UserPreferenceService>, | ||
key: web::Path<String>, | ||
user: UserDetails, | ||
) -> Result<impl Responder, Error> { | ||
Ok(match service.get(user.id, key.into_inner()).await? { | ||
Some(Revisioned { value, revision }) => HttpResponse::Ok() | ||
.append_header((header::ETAG, ETag(EntityTag::new_strong(revision)))) | ||
.json(value), | ||
None => HttpResponse::NotFound().finish(), | ||
}) | ||
} | ||
|
||
#[utoipa::path( | ||
tag = "userPreferences", | ||
operation_id = "setUserPreferences", | ||
request_body = serde_json::Value, | ||
params( | ||
("key", Path, description = "The key to the user preferences"), | ||
("if-match" = Option<String>, Header, description = "The revision to update"), | ||
), | ||
responses( | ||
( | ||
status = 200, | ||
description = "User preference stored under this key", | ||
headers( | ||
("etag" = String, description = "Revision ID") | ||
) | ||
), | ||
(status = 412, description = "The provided If-Match revision did not match the actual revision") | ||
) | ||
)] | ||
#[put("/v1/userPreference/{key}")] | ||
/// Set user preferences | ||
async fn set( | ||
service: web::Data<UserPreferenceService>, | ||
key: web::Path<String>, | ||
user: UserDetails, | ||
web::Header(if_match): web::Header<IfMatch>, | ||
web::Json(data): web::Json<serde_json::Value>, | ||
) -> Result<impl Responder, Error> { | ||
let revision = match &if_match { | ||
IfMatch::Any => None, | ||
IfMatch::Items(items) => items.first().map(|etag| etag.tag()), | ||
}; | ||
|
||
let Revisioned { | ||
value: (), | ||
revision, | ||
} = service | ||
.set(user.id, key.into_inner(), revision, data) | ||
.await?; | ||
|
||
Ok(HttpResponse::NoContent() | ||
.append_header((header::ETAG, ETag(EntityTag::new_strong(revision)))) | ||
.finish()) | ||
} | ||
|
||
#[utoipa::path( | ||
tag = "userPreferences", | ||
operation_id = "deleteUserPreferences", | ||
request_body = serde_json::Value, | ||
params( | ||
("key", Path, description = "The key to the user preferences"), | ||
("if-match" = Option<String>, Header, description = "The revision to delete"), | ||
), | ||
responses( | ||
(status = 201, description = "User preferences are deleted"), | ||
(status = 412, description = "The provided If-Match revision did not match the actual revision") | ||
) | ||
)] | ||
#[delete("/v1/userPreference/{key}")] | ||
/// Delete user preferences | ||
async fn delete( | ||
service: web::Data<UserPreferenceService>, | ||
key: web::Path<String>, | ||
user: UserDetails, | ||
web::Header(if_match): web::Header<IfMatch>, | ||
) -> Result<impl Responder, Error> { | ||
let revision = match &if_match { | ||
IfMatch::Any => None, | ||
IfMatch::Items(items) => items.first().map(|etag| etag.tag()), | ||
}; | ||
|
||
service.delete(user.id, key.into_inner(), revision).await?; | ||
Ok(HttpResponse::NoContent().finish()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
#![recursion_limit = "256"] | ||
|
||
pub mod endpoints; | ||
pub mod service; | ||
pub mod test; |
Oops, something went wrong.