multiple changes
This commit is contained in:
parent
dc5c92c1e8
commit
86ab1a451b
10
.yaak/yaak.fl_1twfKS3Z0A.yaml
Normal file
10
.yaak/yaak.fl_1twfKS3Z0A.yaml
Normal file
@ -0,0 +1,10 @@
|
||||
type: folder
|
||||
model: folder
|
||||
id: fl_1twfKS3Z0A
|
||||
createdAt: 2025-01-04T16:33:57
|
||||
updatedAt: 2025-01-12T08:36:42
|
||||
workspaceId: wk_SlydsyH2WI
|
||||
folderId: null
|
||||
name: API Test
|
||||
description: ''
|
||||
sortPriority: 1000.0
|
40
.yaak/yaak.rq_1YHYKIkG8x.yaml
Normal file
40
.yaak/yaak.rq_1YHYKIkG8x.yaml
Normal file
@ -0,0 +1,40 @@
|
||||
type: http_request
|
||||
model: http_request
|
||||
id: rq_1YHYKIkG8x
|
||||
createdAt: 2025-01-06T11:35:07
|
||||
updatedAt: 2025-03-31T17:29:24.409065
|
||||
workspaceId: wk_SlydsyH2WI
|
||||
folderId: fl_1twfKS3Z0A
|
||||
authentication:
|
||||
token: ${[ response.body.raw(request='rq_Eb5hJAiaKG') ]}
|
||||
authenticationType: bearer
|
||||
body:
|
||||
text: |-
|
||||
{
|
||||
"signature": "untrusted comment: {\"timestamp\":\"2025-01-07T21:23:48.632759\",\"user_id\":\"TestUser\"}\nRUSvcn8DqG4zxAhjcXsihWr/GlBX5bqPs64sPuU2PsFWgALzwkv+dRUF2on5Xr2RFMAl+U0WRwz1jR65FbEHGnzHP6/Lq48IcgM=\ntrusted comment: timestamp:1736284801\nLHdY9CWqzzhFtkGGUXuaYRiFrpg6CId8rX2zRJJigmWJxCPx7+d3ZtWPel4TmHUB21Ce6C84ObFCZRwWE8v6Bw==\n",
|
||||
"signed_from": ""
|
||||
}
|
||||
bodyType: application/json
|
||||
description: ''
|
||||
headers:
|
||||
- enabled: true
|
||||
name: Content-Type
|
||||
value: application/json
|
||||
id: null
|
||||
- enabled: true
|
||||
name: x-api-key
|
||||
value: ${[ ApiKey ]}
|
||||
id: null
|
||||
- enabled: false
|
||||
name: ''
|
||||
value: ''
|
||||
id: null
|
||||
- enabled: true
|
||||
name: ''
|
||||
value: ''
|
||||
id: null
|
||||
method: GET
|
||||
name: Verify Something
|
||||
sortPriority: 6000.0
|
||||
url: http://localhost:8080/api/signature/verify
|
||||
urlParameters: []
|
48
.yaak/yaak.rq_A81dnTkhL5.yaml
Normal file
48
.yaak/yaak.rq_A81dnTkhL5.yaml
Normal file
@ -0,0 +1,48 @@
|
||||
type: http_request
|
||||
model: http_request
|
||||
id: rq_A81dnTkhL5
|
||||
createdAt: 2024-12-31T10:05:25
|
||||
updatedAt: 2025-03-31T17:28:59.911928
|
||||
workspaceId: wk_SlydsyH2WI
|
||||
folderId: fl_1twfKS3Z0A
|
||||
authentication:
|
||||
token: ${[ response.body.raw(request='rq_Eb5hJAiaKG') ]}
|
||||
authenticationType: bearer
|
||||
body:
|
||||
text: |-
|
||||
{
|
||||
"user_id": "",
|
||||
"surname": "",
|
||||
"status_flag": "Deleted",
|
||||
"name": "",
|
||||
"permissions": [
|
||||
"Test"
|
||||
],
|
||||
"active_directory_auth": true,
|
||||
"email": "",
|
||||
"group_permissions": []
|
||||
}
|
||||
bodyType: application/json
|
||||
description: ''
|
||||
headers:
|
||||
- enabled: true
|
||||
name: Content-Type
|
||||
value: application/json
|
||||
id: null
|
||||
- enabled: true
|
||||
name: x-api-key
|
||||
value: ${[ ApiKey ]}
|
||||
id: null
|
||||
- enabled: false
|
||||
name: ''
|
||||
value: ''
|
||||
id: null
|
||||
- enabled: true
|
||||
name: ''
|
||||
value: ''
|
||||
id: null
|
||||
method: GET
|
||||
name: Users
|
||||
sortPriority: 2000.0
|
||||
url: http://localhost:8080/api/users
|
||||
urlParameters: []
|
35
.yaak/yaak.rq_Eb5hJAiaKG.yaml
Normal file
35
.yaak/yaak.rq_Eb5hJAiaKG.yaml
Normal file
@ -0,0 +1,35 @@
|
||||
type: http_request
|
||||
model: http_request
|
||||
id: rq_Eb5hJAiaKG
|
||||
createdAt: 2024-12-01T08:39:12
|
||||
updatedAt: 2025-03-02T07:37:25.323717
|
||||
workspaceId: wk_SlydsyH2WI
|
||||
folderId: fl_1twfKS3Z0A
|
||||
authentication: {}
|
||||
authenticationType: null
|
||||
body:
|
||||
text: |-
|
||||
{
|
||||
"id": "TestUser",
|
||||
"password": "test"
|
||||
}
|
||||
bodyType: application/json
|
||||
description: ''
|
||||
headers:
|
||||
- enabled: true
|
||||
name: Content-Type
|
||||
value: application/json
|
||||
id: null
|
||||
- enabled: true
|
||||
name: x-api-key
|
||||
value: ${[ ApiKey ]}
|
||||
id: null
|
||||
- enabled: true
|
||||
name: ''
|
||||
value: ''
|
||||
id: null
|
||||
method: POST
|
||||
name: Auth
|
||||
sortPriority: 0.0
|
||||
url: http://localhost:8080/api/login
|
||||
urlParameters: []
|
39
.yaak/yaak.rq_FJT5RHtZ2B.yaml
Normal file
39
.yaak/yaak.rq_FJT5RHtZ2B.yaml
Normal file
@ -0,0 +1,39 @@
|
||||
type: http_request
|
||||
model: http_request
|
||||
id: rq_FJT5RHtZ2B
|
||||
createdAt: 2025-04-05T16:17:53.461290
|
||||
updatedAt: 2025-04-05T16:52:50.453961
|
||||
workspaceId: wk_SlydsyH2WI
|
||||
folderId: fl_1twfKS3Z0A
|
||||
authentication:
|
||||
token: ${[ response.body.raw(request='rq_Eb5hJAiaKG') ]}
|
||||
authenticationType: bearer
|
||||
body:
|
||||
text: |-
|
||||
{
|
||||
"draft_id": "2015-09-05 23:56:04"
|
||||
}
|
||||
bodyType: application/json
|
||||
description: ''
|
||||
headers:
|
||||
- enabled: true
|
||||
name: x-api-key
|
||||
value: ${[ ApiKey ]}
|
||||
id: null
|
||||
- enabled: false
|
||||
name: ''
|
||||
value: ''
|
||||
id: null
|
||||
- enabled: true
|
||||
name: ''
|
||||
value: ''
|
||||
id: null
|
||||
- enabled: true
|
||||
name: Content-Type
|
||||
value: application/json
|
||||
id: FWIGbxmTAH
|
||||
method: GET
|
||||
name: Get attached Draft Files
|
||||
sortPriority: 6000.002
|
||||
url: http://localhost:8080/api/files/draft
|
||||
urlParameters: []
|
40
.yaak/yaak.rq_FmsM38yvHM.yaml
Normal file
40
.yaak/yaak.rq_FmsM38yvHM.yaml
Normal file
@ -0,0 +1,40 @@
|
||||
type: http_request
|
||||
model: http_request
|
||||
id: rq_FmsM38yvHM
|
||||
createdAt: 2025-01-06T10:53:14
|
||||
updatedAt: 2025-03-31T17:29:12.196535
|
||||
workspaceId: wk_SlydsyH2WI
|
||||
folderId: fl_1twfKS3Z0A
|
||||
authentication:
|
||||
token: ${[ response.body.raw(request='rq_Eb5hJAiaKG') ]}
|
||||
authenticationType: bearer
|
||||
body:
|
||||
text: |-
|
||||
{
|
||||
"secret": "EinTestSecret",
|
||||
"ttl": 30
|
||||
}
|
||||
bodyType: application/json
|
||||
description: ''
|
||||
headers:
|
||||
- enabled: true
|
||||
name: Content-Type
|
||||
value: application/json
|
||||
id: null
|
||||
- enabled: true
|
||||
name: x-api-key
|
||||
value: ${[ ApiKey ]}
|
||||
id: null
|
||||
- enabled: false
|
||||
name: ''
|
||||
value: ''
|
||||
id: null
|
||||
- enabled: true
|
||||
name: ''
|
||||
value: ''
|
||||
id: null
|
||||
method: PUT
|
||||
name: Verify Signing Key
|
||||
sortPriority: 4000.0
|
||||
url: http://localhost:8080/api/signature/key
|
||||
urlParameters: []
|
48
.yaak/yaak.rq_G6OxWcR5j2.yaml
Normal file
48
.yaak/yaak.rq_G6OxWcR5j2.yaml
Normal file
@ -0,0 +1,48 @@
|
||||
type: http_request
|
||||
model: http_request
|
||||
id: rq_G6OxWcR5j2
|
||||
createdAt: 2025-01-06T10:50:59
|
||||
updatedAt: 2025-03-31T17:29:16.792639
|
||||
workspaceId: wk_SlydsyH2WI
|
||||
folderId: fl_1twfKS3Z0A
|
||||
authentication:
|
||||
token: ${[ response.body.raw(request='rq_Eb5hJAiaKG') ]}
|
||||
authenticationType: bearer
|
||||
body:
|
||||
text: |-
|
||||
{
|
||||
"user_id": "",
|
||||
"surname": "",
|
||||
"status_flag": "Deleted",
|
||||
"name": "",
|
||||
"permissions": [
|
||||
"Test"
|
||||
],
|
||||
"active_directory_auth": true,
|
||||
"email": "",
|
||||
"group_permissions": []
|
||||
}
|
||||
bodyType: application/json
|
||||
description: ''
|
||||
headers:
|
||||
- enabled: true
|
||||
name: Content-Type
|
||||
value: application/json
|
||||
id: null
|
||||
- enabled: true
|
||||
name: x-api-key
|
||||
value: ${[ ApiKey ]}
|
||||
id: null
|
||||
- enabled: false
|
||||
name: ''
|
||||
value: ''
|
||||
id: null
|
||||
- enabled: true
|
||||
name: ''
|
||||
value: ''
|
||||
id: null
|
||||
method: GET
|
||||
name: Verify Signing Key
|
||||
sortPriority: 4500.0
|
||||
url: http://localhost:8080/api/signature/key
|
||||
urlParameters: []
|
39
.yaak/yaak.rq_SFA4nX8S0k.yaml
Normal file
39
.yaak/yaak.rq_SFA4nX8S0k.yaml
Normal file
@ -0,0 +1,39 @@
|
||||
type: http_request
|
||||
model: http_request
|
||||
id: rq_SFA4nX8S0k
|
||||
createdAt: 2025-01-06T10:55:51
|
||||
updatedAt: 2025-03-31T17:29:07.550911
|
||||
workspaceId: wk_SlydsyH2WI
|
||||
folderId: fl_1twfKS3Z0A
|
||||
authentication:
|
||||
token: ${[ response.body.raw(request='rq_Eb5hJAiaKG') ]}
|
||||
authenticationType: bearer
|
||||
body:
|
||||
text: |-
|
||||
{
|
||||
"secret": "EinTestSecret"
|
||||
}
|
||||
bodyType: application/json
|
||||
description: ''
|
||||
headers:
|
||||
- enabled: true
|
||||
name: Content-Type
|
||||
value: application/json
|
||||
id: null
|
||||
- enabled: true
|
||||
name: x-api-key
|
||||
value: ${[ ApiKey ]}
|
||||
id: null
|
||||
- enabled: false
|
||||
name: ''
|
||||
value: ''
|
||||
id: null
|
||||
- enabled: true
|
||||
name: ''
|
||||
value: ''
|
||||
id: null
|
||||
method: POST
|
||||
name: Create Signing Key
|
||||
sortPriority: 3000.0
|
||||
url: http://localhost:8080/api/signature/key
|
||||
urlParameters: []
|
40
.yaak/yaak.rq_eFkZk6Z6QS.yaml
Normal file
40
.yaak/yaak.rq_eFkZk6Z6QS.yaml
Normal file
@ -0,0 +1,40 @@
|
||||
type: http_request
|
||||
model: http_request
|
||||
id: rq_eFkZk6Z6QS
|
||||
createdAt: 2025-04-05T16:52:35.932090
|
||||
updatedAt: 2025-04-05T16:55:08.986210
|
||||
workspaceId: wk_SlydsyH2WI
|
||||
folderId: fl_1twfKS3Z0A
|
||||
authentication:
|
||||
token: ${[ response.body.raw(request='rq_Eb5hJAiaKG') ]}
|
||||
authenticationType: bearer
|
||||
body:
|
||||
text: |-
|
||||
{
|
||||
"draft_id": "2015-09-05 23:56:04",
|
||||
"hash": "9CE71B15548BEAC9745F464531B70BEE30F71F310908D3A0A86E73E07F61ED61"
|
||||
}
|
||||
bodyType: application/json
|
||||
description: ''
|
||||
headers:
|
||||
- enabled: true
|
||||
name: x-api-key
|
||||
value: ${[ ApiKey ]}
|
||||
id: null
|
||||
- enabled: false
|
||||
name: ''
|
||||
value: ''
|
||||
id: null
|
||||
- enabled: true
|
||||
name: ''
|
||||
value: ''
|
||||
id: null
|
||||
- enabled: true
|
||||
name: Content-Type
|
||||
value: application/json
|
||||
id: FWIGbxmTAH
|
||||
method: GET
|
||||
name: Download Draft File
|
||||
sortPriority: 6000.003
|
||||
url: http://localhost:8080/api/files/draft/file
|
||||
urlParameters: []
|
48
.yaak/yaak.rq_gU10vzCIxt.yaml
Normal file
48
.yaak/yaak.rq_gU10vzCIxt.yaml
Normal file
@ -0,0 +1,48 @@
|
||||
type: http_request
|
||||
model: http_request
|
||||
id: rq_gU10vzCIxt
|
||||
createdAt: 2025-01-04T18:07:52
|
||||
updatedAt: 2025-03-31T17:28:46.667681
|
||||
workspaceId: wk_SlydsyH2WI
|
||||
folderId: fl_1twfKS3Z0A
|
||||
authentication:
|
||||
token: ${[ response.body.raw(request='rq_Eb5hJAiaKG') ]}
|
||||
authenticationType: bearer
|
||||
body:
|
||||
text: |-
|
||||
{
|
||||
"user_id": "",
|
||||
"surname": "",
|
||||
"status_flag": "Deleted",
|
||||
"name": "",
|
||||
"permissions": [
|
||||
"Test"
|
||||
],
|
||||
"active_directory_auth": true,
|
||||
"email": "",
|
||||
"group_permissions": []
|
||||
}
|
||||
bodyType: application/json
|
||||
description: ''
|
||||
headers:
|
||||
- enabled: true
|
||||
name: Content-Type
|
||||
value: application/json
|
||||
id: null
|
||||
- enabled: true
|
||||
name: x-api-key
|
||||
value: ${[ ApiKey ]}
|
||||
id: null
|
||||
- enabled: false
|
||||
name: ''
|
||||
value: ''
|
||||
id: null
|
||||
- enabled: true
|
||||
name: ''
|
||||
value: ''
|
||||
id: null
|
||||
method: GET
|
||||
name: Api Keys
|
||||
sortPriority: 1000.0
|
||||
url: http://localhost:8080/api/apikeys
|
||||
urlParameters: []
|
39
.yaak/yaak.rq_lGuHoFSNGa.yaml
Normal file
39
.yaak/yaak.rq_lGuHoFSNGa.yaml
Normal file
@ -0,0 +1,39 @@
|
||||
type: http_request
|
||||
model: http_request
|
||||
id: rq_lGuHoFSNGa
|
||||
createdAt: 2025-01-06T10:56:17
|
||||
updatedAt: 2025-03-31T17:29:20.133348
|
||||
workspaceId: wk_SlydsyH2WI
|
||||
folderId: fl_1twfKS3Z0A
|
||||
authentication:
|
||||
token: ${[ response.body.raw(request='rq_Eb5hJAiaKG') ]}
|
||||
authenticationType: bearer
|
||||
body:
|
||||
text: |-
|
||||
{
|
||||
"secret": "EinTestSecret"
|
||||
}
|
||||
bodyType: application/json
|
||||
description: ''
|
||||
headers:
|
||||
- enabled: true
|
||||
name: Content-Type
|
||||
value: application/json
|
||||
id: null
|
||||
- enabled: true
|
||||
name: x-api-key
|
||||
value: ${[ ApiKey ]}
|
||||
id: null
|
||||
- enabled: false
|
||||
name: ''
|
||||
value: ''
|
||||
id: null
|
||||
- enabled: true
|
||||
name: ''
|
||||
value: ''
|
||||
id: null
|
||||
method: POST
|
||||
name: Sign Something
|
||||
sortPriority: 5000.0
|
||||
url: http://localhost:8080/api/signature/sign
|
||||
urlParameters: []
|
52
.yaak/yaak.rq_t5k4XC8Poe.yaml
Normal file
52
.yaak/yaak.rq_t5k4XC8Poe.yaml
Normal file
@ -0,0 +1,52 @@
|
||||
type: http_request
|
||||
model: http_request
|
||||
id: rq_t5k4XC8Poe
|
||||
createdAt: 2025-04-02T16:41:11.369047
|
||||
updatedAt: 2025-04-05T17:00:29.492513
|
||||
workspaceId: wk_SlydsyH2WI
|
||||
folderId: fl_1twfKS3Z0A
|
||||
authentication:
|
||||
token: ${[ response.body.raw(request='rq_Eb5hJAiaKG') ]}
|
||||
authenticationType: bearer
|
||||
body:
|
||||
form:
|
||||
- enabled: true
|
||||
file: ''
|
||||
id: u6yikyzRTH
|
||||
name: file
|
||||
- enabled: true
|
||||
id: njtBaKefpN
|
||||
name: name
|
||||
value: '"TestValue"'
|
||||
- enabled: true
|
||||
id: Iav8TimHS8
|
||||
name: draft_id
|
||||
value: 2015-09-05 23:56:04
|
||||
- enabled: true
|
||||
id: d57ZzimswD
|
||||
name: ''
|
||||
value: ''
|
||||
bodyType: multipart/form-data
|
||||
description: ''
|
||||
headers:
|
||||
- enabled: true
|
||||
name: x-api-key
|
||||
value: ${[ ApiKey ]}
|
||||
id: null
|
||||
- enabled: false
|
||||
name: ''
|
||||
value: ''
|
||||
id: null
|
||||
- enabled: true
|
||||
name: ''
|
||||
value: ''
|
||||
id: null
|
||||
- enabled: true
|
||||
name: Content-Type
|
||||
value: multipart/form-data
|
||||
id: rf64n05TdT
|
||||
method: POST
|
||||
name: Upload Draft File
|
||||
sortPriority: 6000.001
|
||||
url: http://localhost:8080/api/files/draft
|
||||
urlParameters: []
|
10
.yaak/yaak.wk_SlydsyH2WI.yaml
Normal file
10
.yaak/yaak.wk_SlydsyH2WI.yaml
Normal file
@ -0,0 +1,10 @@
|
||||
type: workspace
|
||||
model: workspace
|
||||
id: wk_SlydsyH2WI
|
||||
createdAt: 2024-12-01T08:38:45
|
||||
updatedAt: 2024-12-01T08:38:45
|
||||
name: Yaak
|
||||
description: ''
|
||||
settingValidateCertificates: true
|
||||
settingFollowRedirects: true
|
||||
settingRequestTimeout: 0
|
@ -1,5 +1,6 @@
|
||||
use axum_jwt_login::{AuthBackend, UserPermissions};
|
||||
use ldap::LDAPBackend;
|
||||
use private_key_cache::PrivateKeyCache;
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
@ -11,11 +12,13 @@ use crate::{
|
||||
use super::routes::{auth::models::Credentials, users::models::User};
|
||||
|
||||
pub mod ldap;
|
||||
pub mod private_key_cache;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ApiBackend {
|
||||
pool: PgPool,
|
||||
config: Configuration,
|
||||
private_key_cache: PrivateKeyCache,
|
||||
}
|
||||
|
||||
impl ApiBackend {
|
||||
@ -23,6 +26,7 @@ impl ApiBackend {
|
||||
Self {
|
||||
pool,
|
||||
config: config.clone(),
|
||||
private_key_cache: PrivateKeyCache::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,6 +37,10 @@ impl ApiBackend {
|
||||
pub fn config(&self) -> &Configuration {
|
||||
&self.config
|
||||
}
|
||||
|
||||
pub fn private_key_cache(&self) -> PrivateKeyCache {
|
||||
self.private_key_cache.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl AuthBackend<User> for ApiBackend {
|
||||
|
77
src/api/backend/private_key_cache.rs
Normal file
77
src/api/backend/private_key_cache.rs
Normal file
@ -0,0 +1,77 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use minisign::SecretKey;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::debug;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PrivateKeyCache {
|
||||
cache: Arc<Mutex<HashMap<String, PrivateKeyEntry>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PrivateKeyEntry {
|
||||
key: SecretKey,
|
||||
ttl: Duration,
|
||||
creation: Instant,
|
||||
clean_token: CancellationToken,
|
||||
}
|
||||
|
||||
impl PrivateKeyEntry {
|
||||
pub fn secret_key(&self) -> SecretKey {
|
||||
self.key.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl PrivateKeyCache {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
cache: Arc::new(Mutex::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn add_key(&self, user: &str, key: &SecretKey, ttl: Duration) {
|
||||
// create token to abort cleaning task
|
||||
let clean_token = CancellationToken::new();
|
||||
// create private key entry
|
||||
let entry = PrivateKeyEntry {
|
||||
key: key.clone(),
|
||||
ttl,
|
||||
creation: Instant::now(),
|
||||
clean_token: clean_token.clone(),
|
||||
};
|
||||
// lock cache
|
||||
let mut cache = self.cache.lock().await;
|
||||
// add private key to cache
|
||||
if let Some(old_entry) = cache.insert(user.to_string(), entry) {
|
||||
// abort cleaning task of old entry
|
||||
old_entry.clean_token.cancel();
|
||||
}
|
||||
|
||||
// spawn task to remove private key from cache after storage expiration
|
||||
let cache = self.cache.clone();
|
||||
let user = user.to_string();
|
||||
tokio::spawn(async move {
|
||||
tokio::select! {
|
||||
_ = tokio::time::sleep(ttl) => {
|
||||
let mut lock = cache.lock().await;
|
||||
lock.remove(&user);
|
||||
debug!("Deleted cached Secret Key after TTL expiration")
|
||||
},
|
||||
_ = clean_token.cancelled() => {
|
||||
debug!("Cancelled automated cache clearance after TTL renewal")
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn get_key(&self, user_id: &str) -> Option<PrivateKeyEntry> {
|
||||
let lock = self.cache.lock().await;
|
||||
lock.get(user_id).cloned()
|
||||
}
|
||||
}
|
@ -8,12 +8,14 @@ pub const USERS_TAG: &str = "Users";
|
||||
pub const ORDER_TAG: &str = "order";
|
||||
pub const API_KEY_TAG: &str = "API Keys";
|
||||
pub const SIGNATURE_TAG: &str = "Signature";
|
||||
pub const FILE_TAG: &str = "Files";
|
||||
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
modifiers(&SecurityAddon),
|
||||
tags(
|
||||
(name = AUTH_TAG, description = "API Authentication endpoints"),
|
||||
(name = FILE_TAG, description = "Upload and Download Files"),
|
||||
(name = ORDER_TAG, description = "Order API endpoints")
|
||||
),
|
||||
)]
|
||||
|
35
src/api/routes/api_keys/handlers/apikeys_delete.rs
Normal file
35
src/api/routes/api_keys/handlers/apikeys_delete.rs
Normal file
@ -0,0 +1,35 @@
|
||||
use axum::{debug_handler, extract::Query, Extension};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::{
|
||||
api::{backend::ApiBackend, description::API_KEY_TAG, routes::api_keys::sql},
|
||||
errors::ApiError,
|
||||
};
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct DeleteQueryParameters {
|
||||
key_id: String,
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/apikeys",
|
||||
summary = "Delete API Key",
|
||||
description = "Delete an API Key.",
|
||||
params(DeleteQueryParameters),
|
||||
responses(
|
||||
(status = OK, description = "API Key successfully deleted"),
|
||||
),
|
||||
security(
|
||||
("user_auth" = ["write:apikeys",]),
|
||||
),
|
||||
tag = API_KEY_TAG)]
|
||||
pub async fn delete_api_key(
|
||||
Extension(backend): Extension<ApiBackend>,
|
||||
Query(params): Query<DeleteQueryParameters>,
|
||||
) -> Result<(), ApiError> {
|
||||
sql::delete_api_key(backend.pool(), ¶ms.key_id).await?;
|
||||
Ok(())
|
||||
}
|
29
src/api/routes/api_keys/handlers/apikeys_get.rs
Normal file
29
src/api/routes/api_keys/handlers/apikeys_get.rs
Normal file
@ -0,0 +1,29 @@
|
||||
use axum::{debug_handler, Extension, Json};
|
||||
|
||||
use crate::{
|
||||
api::{
|
||||
backend::ApiBackend,
|
||||
description::API_KEY_TAG,
|
||||
routes::api_keys::{models::ApiKey, sql},
|
||||
},
|
||||
errors::ApiError,
|
||||
};
|
||||
|
||||
#[debug_handler]
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/apikeys",
|
||||
summary = "Get all API Keys",
|
||||
description = "Get a list of all configured API Keys.",
|
||||
responses(
|
||||
(status = OK, body = Vec<ApiKey>, description = "List of API Keys"),
|
||||
),
|
||||
security(
|
||||
("user_auth" = ["read:apikeys",]),
|
||||
),
|
||||
tag = API_KEY_TAG)]
|
||||
pub async fn get_api_keys(
|
||||
Extension(backend): Extension<ApiBackend>,
|
||||
) -> Result<Json<Vec<ApiKey>>, ApiError> {
|
||||
Ok(Json(sql::get_api_keys(backend.pool()).await?))
|
||||
}
|
50
src/api/routes/api_keys/handlers/apikeys_post.rs
Normal file
50
src/api/routes/api_keys/handlers/apikeys_post.rs
Normal file
@ -0,0 +1,50 @@
|
||||
use axum::{debug_handler, Json};
|
||||
|
||||
use crate::{
|
||||
api::{
|
||||
description::API_KEY_TAG,
|
||||
routes::{
|
||||
api_keys::{models::ApiKey, sql},
|
||||
AuthBackendType,
|
||||
},
|
||||
},
|
||||
errors::ApiError,
|
||||
};
|
||||
|
||||
#[debug_handler]
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/apikeys",
|
||||
summary = "Create new API Key",
|
||||
description = "Create a new API Key.",
|
||||
request_body(content = ApiKey, description = "API Key details", content_type = "application/json"),
|
||||
responses(
|
||||
(status = OK, description = "API Key successfully created (API Key Secret in Body)", body = String),
|
||||
),
|
||||
security(
|
||||
("user_auth" = ["write:apikeys",]),
|
||||
),
|
||||
tag = API_KEY_TAG)]
|
||||
pub async fn create_api_key(
|
||||
auth_session: AuthBackendType,
|
||||
Json(api_key): Json<ApiKey>,
|
||||
) -> Result<String, ApiError> {
|
||||
let backend = auth_session.backend();
|
||||
|
||||
// create new API key
|
||||
let (key_secret, key) = ApiKey::create(
|
||||
&api_key.name,
|
||||
api_key.auth_required,
|
||||
api_key.permissions.0,
|
||||
backend.config(),
|
||||
)?;
|
||||
|
||||
// insert API Key into database
|
||||
sql::create_api_key(backend.pool(), &key).await?;
|
||||
|
||||
// add API Key to session
|
||||
auth_session.add_api_key(key).await;
|
||||
|
||||
// Return key secret in response
|
||||
Ok(key_secret)
|
||||
}
|
32
src/api/routes/api_keys/handlers/apikeys_put.rs
Normal file
32
src/api/routes/api_keys/handlers/apikeys_put.rs
Normal file
@ -0,0 +1,32 @@
|
||||
use axum::{debug_handler, Extension, Json};
|
||||
|
||||
use crate::{
|
||||
api::{
|
||||
backend::ApiBackend,
|
||||
description::API_KEY_TAG,
|
||||
routes::api_keys::{models::ApiKey, sql},
|
||||
},
|
||||
errors::ApiError,
|
||||
};
|
||||
|
||||
#[debug_handler]
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/apikeys",
|
||||
summary = "Update API Key",
|
||||
description = "Update an API Key.",
|
||||
request_body(content = ApiKey, description = "API Key details", content_type = "application/json"),
|
||||
responses(
|
||||
(status = OK, description = "API Key successfully updated"),
|
||||
),
|
||||
security(
|
||||
("user_auth" = ["write:apikeys",]),
|
||||
),
|
||||
tag = API_KEY_TAG)]
|
||||
pub async fn update_api_key(
|
||||
Extension(backend): Extension<ApiBackend>,
|
||||
Json(api_key): Json<ApiKey>,
|
||||
) -> Result<(), ApiError> {
|
||||
sql::update_api_key(backend.pool(), &api_key).await?;
|
||||
Ok(())
|
||||
}
|
4
src/api/routes/api_keys/handlers/mod.rs
Normal file
4
src/api/routes/api_keys/handlers/mod.rs
Normal file
@ -0,0 +1,4 @@
|
||||
pub mod apikeys_delete;
|
||||
pub mod apikeys_get;
|
||||
pub mod apikeys_post;
|
||||
pub mod apikeys_put;
|
@ -1,24 +1,15 @@
|
||||
use axum::{debug_handler, extract::Query, Extension, Json};
|
||||
use models::ApiKey;
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
use utoipa_axum::{router::OpenApiRouter, routes};
|
||||
|
||||
use crate::{
|
||||
api::{
|
||||
backend::ApiBackend,
|
||||
description::API_KEY_TAG,
|
||||
routes::{
|
||||
users::permissions::{Permission, PermissionDetail},
|
||||
User,
|
||||
},
|
||||
},
|
||||
errors::ApiError,
|
||||
api::routes::users::permissions::{Permission, PermissionDetail},
|
||||
permission_required,
|
||||
};
|
||||
use handlers::apikeys_delete::{__path_delete_api_key, delete_api_key};
|
||||
use handlers::apikeys_get::{__path_get_api_keys, get_api_keys};
|
||||
use handlers::apikeys_post::{__path_create_api_key, create_api_key};
|
||||
use handlers::apikeys_put::{__path_update_api_key, update_api_key};
|
||||
|
||||
use super::AuthBackendType;
|
||||
|
||||
mod handlers;
|
||||
pub mod models;
|
||||
pub mod sql;
|
||||
|
||||
@ -37,109 +28,3 @@ pub fn router() -> OpenApiRouter {
|
||||
|
||||
OpenApiRouter::new().merge(read).merge(write)
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/apikeys",
|
||||
summary = "Get all API Keys",
|
||||
description = "Get a list of all configured API Keys.",
|
||||
responses(
|
||||
(status = OK, body = Vec<ApiKey>, description = "List of API Keys"),
|
||||
),
|
||||
security(
|
||||
("user_auth" = ["read:apikeys",]),
|
||||
),
|
||||
tag = API_KEY_TAG)]
|
||||
pub async fn get_api_keys(
|
||||
Extension(backend): Extension<ApiBackend>,
|
||||
) -> Result<Json<Vec<ApiKey>>, ApiError> {
|
||||
Ok(Json(sql::get_api_keys(backend.pool()).await?))
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/apikeys",
|
||||
summary = "Create new API Key",
|
||||
description = "Create a new API Key.",
|
||||
request_body(content = ApiKey, description = "API Key details", content_type = "application/json"),
|
||||
responses(
|
||||
(status = OK, description = "API Key successfully created (API Key Secret in Body)", body = String),
|
||||
),
|
||||
security(
|
||||
("user_auth" = ["write:apikeys",]),
|
||||
),
|
||||
tag = API_KEY_TAG)]
|
||||
pub async fn create_api_key(
|
||||
auth_session: AuthBackendType,
|
||||
Json(api_key): Json<ApiKey>,
|
||||
) -> Result<String, ApiError> {
|
||||
let backend = auth_session.backend();
|
||||
|
||||
// create new API key
|
||||
let (key_secret, key) = ApiKey::create(
|
||||
&api_key.name,
|
||||
api_key.auth_required,
|
||||
api_key.permissions.0,
|
||||
backend.config(),
|
||||
)?;
|
||||
|
||||
// insert API Key into database
|
||||
sql::create_api_key(backend.pool(), &key).await?;
|
||||
|
||||
// add API Key to session
|
||||
auth_session.add_api_key(key).await;
|
||||
|
||||
// Return key secret in response
|
||||
Ok(key_secret)
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/apikeys",
|
||||
summary = "Update API Key",
|
||||
description = "Update an API Key.",
|
||||
request_body(content = ApiKey, description = "API Key details", content_type = "application/json"),
|
||||
responses(
|
||||
(status = OK, description = "API Key successfully updated"),
|
||||
),
|
||||
security(
|
||||
("user_auth" = ["write:apikeys",]),
|
||||
),
|
||||
tag = API_KEY_TAG)]
|
||||
pub async fn update_api_key(
|
||||
Extension(backend): Extension<ApiBackend>,
|
||||
Json(api_key): Json<ApiKey>,
|
||||
) -> Result<(), ApiError> {
|
||||
sql::update_api_key(backend.pool(), &api_key).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct DeleteQueryParameters {
|
||||
key_id: String,
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/apikeys",
|
||||
summary = "Delete API Key",
|
||||
description = "Delete an API Key.",
|
||||
params(DeleteQueryParameters),
|
||||
responses(
|
||||
(status = OK, description = "API Key successfully deleted"),
|
||||
),
|
||||
security(
|
||||
("user_auth" = ["write:apikeys",]),
|
||||
),
|
||||
tag = API_KEY_TAG)]
|
||||
pub async fn delete_api_key(
|
||||
Extension(backend): Extension<ApiBackend>,
|
||||
Query(params): Query<DeleteQueryParameters>,
|
||||
) -> Result<(), ApiError> {
|
||||
sql::delete_api_key(backend.pool(), ¶ms.key_id).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
40
src/api/routes/auth/handlers/login_post.rs
Normal file
40
src/api/routes/auth/handlers/login_post.rs
Normal file
@ -0,0 +1,40 @@
|
||||
use axum::{debug_handler, Json};
|
||||
|
||||
use crate::{
|
||||
api::{
|
||||
description::AUTH_TAG,
|
||||
routes::{auth::models::Credentials, AuthBackendType},
|
||||
},
|
||||
errors::ApiError,
|
||||
};
|
||||
|
||||
#[debug_handler]
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/login",
|
||||
summary = "Authenticate as user",
|
||||
description = "Authenticate as user and receive a JWT auth token.",
|
||||
request_body(content = Credentials, description = "User credentials", content_type = "application/json"),
|
||||
responses(
|
||||
(status = OK, body = String, description = "Successfully logged in (JWT Token in body)"),
|
||||
(status = UNAUTHORIZED, description = "Invalid credentials or unauthorized user")
|
||||
),
|
||||
tag = AUTH_TAG)]
|
||||
pub async fn authorize(
|
||||
mut auth_session: AuthBackendType,
|
||||
Json(credentials): Json<Credentials>,
|
||||
) -> Result<String, ApiError> {
|
||||
let token = match auth_session.authenticate(credentials).await {
|
||||
Ok(Some(_user)) => {
|
||||
if let Some(token) = auth_session.get_auth_token() {
|
||||
token
|
||||
} else {
|
||||
return Err(ApiError::InvalidCredentials);
|
||||
}
|
||||
}
|
||||
Ok(None) => return Err(ApiError::InvalidCredentials),
|
||||
Err(_) => return Err(ApiError::InvalidCredentials),
|
||||
};
|
||||
|
||||
Ok(token)
|
||||
}
|
21
src/api/routes/auth/handlers/logout_post.rs
Normal file
21
src/api/routes/auth/handlers/logout_post.rs
Normal file
@ -0,0 +1,21 @@
|
||||
use axum::debug_handler;
|
||||
|
||||
use crate::{
|
||||
api::{description::AUTH_TAG, routes::AuthBackendType},
|
||||
errors::ApiError,
|
||||
};
|
||||
|
||||
#[debug_handler]
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/logout",
|
||||
summary = "Logout",
|
||||
description = "Log the currently logged in user out.",
|
||||
responses(
|
||||
(status = OK, description = "Logout successful")
|
||||
),
|
||||
tag = AUTH_TAG)]
|
||||
pub async fn logout(mut auth_session: AuthBackendType) -> Result<(), ApiError> {
|
||||
auth_session.logout().await?;
|
||||
Ok(())
|
||||
}
|
2
src/api/routes/auth/handlers/mod.rs
Normal file
2
src/api/routes/auth/handlers/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod login_post;
|
||||
pub mod logout_post;
|
@ -1,11 +1,9 @@
|
||||
use axum::{debug_handler, Json};
|
||||
use models::Credentials;
|
||||
use utoipa_axum::{router::OpenApiRouter, routes};
|
||||
|
||||
use crate::{api::description::AUTH_TAG, errors::ApiError};
|
||||
|
||||
use super::AuthBackendType;
|
||||
use crate::api::routes::auth::handlers::login_post::{__path_authorize, authorize};
|
||||
use crate::api::routes::auth::handlers::logout_post::{__path_logout, logout};
|
||||
|
||||
mod handlers;
|
||||
pub mod models;
|
||||
|
||||
// expose the OpenAPI to parent module
|
||||
@ -14,49 +12,3 @@ pub fn router() -> OpenApiRouter {
|
||||
.routes(routes!(authorize))
|
||||
.routes(routes!(logout))
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/login",
|
||||
summary = "Authenticate as user",
|
||||
description = "Authenticate as user and receive a JWT auth token.",
|
||||
request_body(content = Credentials, description = "User credentials", content_type = "application/json"),
|
||||
responses(
|
||||
(status = OK, body = String, description = "Successfully logged in (JWT Token in body)"),
|
||||
(status = UNAUTHORIZED, description = "Invalid credentials or unauthorized user")
|
||||
),
|
||||
tag = AUTH_TAG)]
|
||||
pub async fn authorize(
|
||||
mut auth_session: AuthBackendType,
|
||||
Json(credentials): Json<Credentials>,
|
||||
) -> Result<String, ApiError> {
|
||||
let token = match auth_session.authenticate(credentials).await {
|
||||
Ok(Some(_user)) => {
|
||||
if let Some(token) = auth_session.get_auth_token() {
|
||||
token
|
||||
} else {
|
||||
return Err(ApiError::InvalidCredentials);
|
||||
}
|
||||
}
|
||||
Ok(None) => return Err(ApiError::InvalidCredentials),
|
||||
Err(_) => return Err(ApiError::InvalidCredentials),
|
||||
};
|
||||
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/logout",
|
||||
summary = "Logout",
|
||||
description = "Log the currently logged in user out.",
|
||||
responses(
|
||||
(status = OK, description = "Logout successful")
|
||||
),
|
||||
tag = AUTH_TAG)]
|
||||
pub async fn logout(mut auth_session: AuthBackendType) -> Result<(), ApiError> {
|
||||
auth_session.logout().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
64
src/api/routes/files/handlers/files_draft_file_get.rs
Normal file
64
src/api/routes/files/handlers/files_draft_file_get.rs
Normal file
@ -0,0 +1,64 @@
|
||||
use axum::{
|
||||
body::Body,
|
||||
debug_handler,
|
||||
http::header,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use chrono::NaiveDateTime;
|
||||
use serde::Deserialize;
|
||||
use ts_rs::TS;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::{
|
||||
api::{
|
||||
description::FILE_TAG,
|
||||
routes::{files::sql, AuthBackendType},
|
||||
},
|
||||
errors::ApiError,
|
||||
};
|
||||
|
||||
#[derive(Debug, Deserialize, TS, ToSchema)]
|
||||
#[ts(export)]
|
||||
pub struct GetFileRequest {
|
||||
draft_id: String,
|
||||
hash: String,
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/files/draft/file",
|
||||
summary = "Download specified file",
|
||||
description = "Download specified file.",
|
||||
request_body(content = GetFileRequest, description = "Request to download specified file", content_type = "application/json"),
|
||||
responses(
|
||||
(status = OK, description = "File Data", content_type = "application/octet-stream"),
|
||||
),
|
||||
tag = FILE_TAG)]
|
||||
pub async fn get_specified_draft_file(
|
||||
auth_session: AuthBackendType,
|
||||
Json(request): Json<GetFileRequest>,
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
let backend = auth_session.backend();
|
||||
let user = auth_session
|
||||
.is_authenticated()
|
||||
.ok_or(ApiError::AccessDenied)?;
|
||||
|
||||
let draft_id =
|
||||
NaiveDateTime::parse_from_str(&request.draft_id, "%Y-%m-%d %H:%M:%S").map_err(|_| {
|
||||
ApiError::InvalidRequest("Could not parse NaiveDateTime from draft id".to_string())
|
||||
})?;
|
||||
|
||||
match sql::get_specified_draft_file(backend.pool(), user, draft_id, request.hash).await? {
|
||||
Some(file) => Ok(Response::builder()
|
||||
.header(header::CONTENT_TYPE, file.content_type)
|
||||
.header(
|
||||
header::CONTENT_DISPOSITION,
|
||||
format!("attachment; filename=\"{}\"", file.name),
|
||||
)
|
||||
.body(Body::from(file.data))
|
||||
.unwrap_or_default()),
|
||||
None => Err(ApiError::FileNotFound),
|
||||
}
|
||||
}
|
51
src/api/routes/files/handlers/files_draft_get.rs
Normal file
51
src/api/routes/files/handlers/files_draft_get.rs
Normal file
@ -0,0 +1,51 @@
|
||||
use axum::{debug_handler, Json};
|
||||
use chrono::NaiveDateTime;
|
||||
use serde::Deserialize;
|
||||
use ts_rs::TS;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::{
|
||||
api::{
|
||||
description::FILE_TAG,
|
||||
routes::{
|
||||
files::{models::AttachedFile, sql},
|
||||
AuthBackendType,
|
||||
},
|
||||
},
|
||||
errors::ApiError,
|
||||
};
|
||||
|
||||
#[derive(Debug, Deserialize, TS, ToSchema)]
|
||||
#[ts(export)]
|
||||
pub struct GetAttachedFilesRequest {
|
||||
draft_id: String,
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/files/draft",
|
||||
summary = "Get Files attached to Draft",
|
||||
description = "Get list of all files that are attached to a specified draft.",
|
||||
request_body(content = GetAttachedFilesRequest, description = "Request for attached files", content_type = "application/json"),
|
||||
responses(
|
||||
(status = OK, body = AttachedFile, description = "Attached Files List", content_type = "application/json"),
|
||||
),
|
||||
tag = FILE_TAG)]
|
||||
pub async fn get_attached_draft_files(
|
||||
auth_session: AuthBackendType,
|
||||
Json(request): Json<GetAttachedFilesRequest>,
|
||||
) -> Result<Json<Vec<AttachedFile>>, ApiError> {
|
||||
let backend = auth_session.backend();
|
||||
let user = auth_session
|
||||
.is_authenticated()
|
||||
.ok_or(ApiError::AccessDenied)?;
|
||||
|
||||
let draft_id =
|
||||
NaiveDateTime::parse_from_str(&request.draft_id, "%Y-%m-%d %H:%M:%S").map_err(|_| {
|
||||
ApiError::InvalidRequest("Could not parse NaiveDateTime from draft id".to_string())
|
||||
})?;
|
||||
Ok(Json(
|
||||
sql::get_draft_attached_files(backend.pool(), user, draft_id).await?,
|
||||
))
|
||||
}
|
112
src/api/routes/files/handlers/files_draft_post.rs
Normal file
112
src/api/routes/files/handlers/files_draft_post.rs
Normal file
@ -0,0 +1,112 @@
|
||||
use axum::{debug_handler, extract::Multipart, http::StatusCode};
|
||||
use chrono::NaiveDateTime;
|
||||
use serde::Deserialize;
|
||||
use tokio_util::bytes::Bytes;
|
||||
use ts_rs::TS;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::{
|
||||
api::{
|
||||
description::FILE_TAG,
|
||||
routes::{
|
||||
files::{models::File, sql},
|
||||
AuthBackendType,
|
||||
},
|
||||
},
|
||||
errors::ApiError,
|
||||
};
|
||||
|
||||
use super::super::FILE_SIZE_LIMIT_MB;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, TS, ToSchema)]
|
||||
#[ts(export)]
|
||||
#[allow(unused)]
|
||||
pub struct FileUploadRequest {
|
||||
name: Option<String>,
|
||||
#[schema(value_type = String)]
|
||||
draft_id: NaiveDateTime,
|
||||
#[schema(format = Binary, content_media_type = "application/octet-stream")]
|
||||
file: String,
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/files/draft",
|
||||
summary = "Draft File Upload",
|
||||
description = "Upload a file as draft file.",
|
||||
request_body(content = FileUploadRequest, description = "File Data", content_type = "multipart/form-data"),
|
||||
responses(
|
||||
(status = OK, body = String, description = "Successfully uploaded and stored file"),
|
||||
(status = 413, description = format!("The size of the uploaded file is too large (max {FILE_SIZE_LIMIT_MB} MB)"))
|
||||
),
|
||||
tag = FILE_TAG)]
|
||||
pub async fn upload_draft_file(
|
||||
auth_session: AuthBackendType,
|
||||
mut multipart: Multipart,
|
||||
) -> Result<(), ApiError> {
|
||||
let backend = auth_session.backend();
|
||||
let user = auth_session
|
||||
.is_authenticated()
|
||||
.ok_or(ApiError::AccessDenied)?;
|
||||
|
||||
let mut name: Option<String> = None;
|
||||
let mut draft_id: Option<NaiveDateTime> = None;
|
||||
|
||||
let mut content_type: Option<String> = None;
|
||||
let mut file_name: Option<String> = None;
|
||||
let mut bytes: Option<Bytes> = None;
|
||||
let mut size: Option<i32> = None;
|
||||
|
||||
while let Some(field) = multipart.next_field().await.unwrap() {
|
||||
let field_name = field.name();
|
||||
|
||||
match &field_name {
|
||||
Some("name") => name = Some(field.text().await?),
|
||||
Some("draft_id") => {
|
||||
draft_id = Some(
|
||||
NaiveDateTime::parse_from_str(&field.text().await?, "%Y-%m-%d %H:%M:%S")
|
||||
.map_err(|_| {
|
||||
ApiError::InvalidRequest(
|
||||
"Could not parse NaiveDateTime from draft id".to_string(),
|
||||
)
|
||||
})?,
|
||||
)
|
||||
}
|
||||
Some("file") => {
|
||||
file_name = field.file_name().map(ToString::to_string);
|
||||
content_type = field.content_type().map(ToString::to_string);
|
||||
let _bytes = field.bytes().await?;
|
||||
size = Some(_bytes.len() as i32);
|
||||
bytes = Some(_bytes);
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
}
|
||||
|
||||
// store file in database
|
||||
if let (Some(data), Some(draft_id), Some(content_type), Some(filename)) =
|
||||
(bytes, draft_id, content_type, file_name)
|
||||
{
|
||||
sql::insert_new_draft_file(
|
||||
backend.pool(),
|
||||
user,
|
||||
File {
|
||||
name: match name {
|
||||
Some(name) => name,
|
||||
None => filename,
|
||||
},
|
||||
draft_id,
|
||||
content_type,
|
||||
data,
|
||||
size: size.unwrap_or_default(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
Err(ApiError::MultipartForm(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Missing fields in request".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
3
src/api/routes/files/handlers/mod.rs
Normal file
3
src/api/routes/files/handlers/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub mod files_draft_file_get;
|
||||
pub mod files_draft_get;
|
||||
pub mod files_draft_post;
|
26
src/api/routes/files/mod.rs
Normal file
26
src/api/routes/files/mod.rs
Normal file
@ -0,0 +1,26 @@
|
||||
mod handlers;
|
||||
mod models;
|
||||
mod sql;
|
||||
|
||||
use axum::extract::DefaultBodyLimit;
|
||||
use utoipa_axum::{router::OpenApiRouter, routes};
|
||||
|
||||
use crate::{
|
||||
api::routes::users::permissions::{Permission, PermissionDetail},
|
||||
permission_required,
|
||||
};
|
||||
|
||||
use handlers::{files_draft_file_get::*, files_draft_get::*, files_draft_post::*};
|
||||
|
||||
const FILE_SIZE_LIMIT_MB: usize = 20;
|
||||
|
||||
// expose the OpenAPI to parent module
|
||||
pub fn router() -> OpenApiRouter {
|
||||
OpenApiRouter::new()
|
||||
.routes(routes!(get_specified_draft_file))
|
||||
.routes(routes!(upload_draft_file, get_attached_draft_files))
|
||||
.layer(DefaultBodyLimit::max(FILE_SIZE_LIMIT_MB * 1000 * 1000))
|
||||
// .route_layer(permission_required!(Permission::Write(
|
||||
// PermissionDetail::Users // TODO adjust permissions
|
||||
// )))
|
||||
}
|
33
src/api/routes/files/models.rs
Normal file
33
src/api/routes/files/models.rs
Normal file
@ -0,0 +1,33 @@
|
||||
use chrono::NaiveDateTime;
|
||||
use serde::Serialize;
|
||||
use sha2::{Digest, Sha256};
|
||||
use tokio_util::bytes::Bytes;
|
||||
use ts_rs::TS;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, TS, ToSchema)]
|
||||
#[ts(export)]
|
||||
pub struct AttachedFile {
|
||||
pub name: String,
|
||||
pub hash: String,
|
||||
pub content_type: String,
|
||||
pub size: i32,
|
||||
}
|
||||
|
||||
pub struct File {
|
||||
pub name: String,
|
||||
pub draft_id: NaiveDateTime,
|
||||
pub content_type: String,
|
||||
pub data: Bytes,
|
||||
pub size: i32,
|
||||
}
|
||||
|
||||
impl File {
|
||||
pub fn hash(&self) -> String {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&self.data);
|
||||
let hash = hasher.finalize();
|
||||
|
||||
format!("{hash:X}")
|
||||
}
|
||||
}
|
84
src/api/routes/files/sql.rs
Normal file
84
src/api/routes/files/sql.rs
Normal file
@ -0,0 +1,84 @@
|
||||
use chrono::NaiveDateTime;
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::{api::routes::users::models::User, errors::ApiError};
|
||||
|
||||
use super::models::{AttachedFile, File};
|
||||
|
||||
pub async fn insert_new_draft_file(pool: &PgPool, user: &User, file: File) -> Result<(), ApiError> {
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
let affected_rows = sqlx::query!(
|
||||
r#"INSERT INTO draftfiles
|
||||
("UserID", "DraftID", "Hash", "ContentType", "Name", "Data", "Size")
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT DO NOTHING"#,
|
||||
user.user_id,
|
||||
file.draft_id,
|
||||
file.hash(),
|
||||
file.content_type,
|
||||
file.name,
|
||||
&file.data.to_vec(),
|
||||
file.size
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?
|
||||
.rows_affected();
|
||||
|
||||
// commit transaction
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_draft_attached_files(
|
||||
pool: &PgPool,
|
||||
user: &User,
|
||||
draft_id: NaiveDateTime,
|
||||
) -> Result<Vec<AttachedFile>, ApiError> {
|
||||
Ok(sqlx::query_as!(
|
||||
AttachedFile,
|
||||
r#"SELECT
|
||||
"Hash" as hash,
|
||||
"ContentType" as content_type,
|
||||
"Name" as name,
|
||||
"Size" as size
|
||||
FROM
|
||||
"draftfiles"
|
||||
WHERE
|
||||
"UserID" = $1 AND "DraftID" = $2
|
||||
ORDER BY "Name" ASC"#,
|
||||
user.user_id,
|
||||
draft_id
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn get_specified_draft_file(
|
||||
pool: &PgPool,
|
||||
user: &User,
|
||||
draft_id: NaiveDateTime,
|
||||
hash: String,
|
||||
) -> Result<Option<File>, ApiError> {
|
||||
let file = sqlx::query_as!(
|
||||
File,
|
||||
r#"SELECT
|
||||
"ContentType" as content_type,
|
||||
"Name" as name,
|
||||
"Data" as data,
|
||||
"DraftID" as draft_id,
|
||||
"Size" as size
|
||||
FROM
|
||||
"draftfiles"
|
||||
WHERE
|
||||
"UserID" = $1 AND "DraftID" = $2 AND "Hash" = $3
|
||||
ORDER BY "Name" ASC"#,
|
||||
user.user_id,
|
||||
draft_id,
|
||||
hash
|
||||
)
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
|
||||
Ok(file)
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
pub mod api_keys;
|
||||
pub mod auth;
|
||||
mod files;
|
||||
mod signature;
|
||||
pub mod users;
|
||||
|
||||
@ -44,6 +45,7 @@ pub fn create_routes(session: AuthBackendType) -> Router {
|
||||
.nest(API_BASE, users::router())
|
||||
.nest(API_BASE, api_keys::router())
|
||||
.nest(API_BASE, signature::router())
|
||||
.nest(API_BASE, files::router())
|
||||
// .nest(
|
||||
// "/api/order",
|
||||
// // order::router().route_layer(crate::login_required!(AuthenticationBackend<ApiKey>)),
|
||||
|
5
src/api/routes/signature/handlers/mod.rs
Normal file
5
src/api/routes/signature/handlers/mod.rs
Normal file
@ -0,0 +1,5 @@
|
||||
pub mod signature_key_get;
|
||||
pub mod signature_key_post;
|
||||
pub mod signature_key_put;
|
||||
pub mod signature_sign_post;
|
||||
pub mod signature_verify_get;
|
36
src/api/routes/signature/handlers/signature_key_get.rs
Normal file
36
src/api/routes/signature/handlers/signature_key_get.rs
Normal file
@ -0,0 +1,36 @@
|
||||
use super::super::{models::SigningKeyStatus, sql};
|
||||
use axum::{debug_handler, Json};
|
||||
|
||||
use crate::{
|
||||
api::{description::SIGNATURE_TAG, routes::AuthBackendType},
|
||||
errors::ApiError,
|
||||
};
|
||||
|
||||
#[debug_handler]
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/signature/key",
|
||||
summary = "Check for signing key",
|
||||
description = "Check if the logged in user has configured a signing key.",
|
||||
responses(
|
||||
(status = OK, body = SigningKeyStatus, description = "Signature Status"),
|
||||
),
|
||||
security(
|
||||
("user_auth" = ["write:signature",]),
|
||||
),
|
||||
tag = SIGNATURE_TAG)]
|
||||
pub async fn check_signing_key(
|
||||
auth_session: AuthBackendType,
|
||||
) -> Result<Json<SigningKeyStatus>, ApiError> {
|
||||
let backend = auth_session.backend();
|
||||
let user = auth_session
|
||||
.is_authenticated()
|
||||
.ok_or(ApiError::AccessDenied)?;
|
||||
|
||||
let private_key = sql::get_private_key(backend.pool(), &user.user_id).await?;
|
||||
|
||||
Ok(Json(SigningKeyStatus {
|
||||
configured: private_key.key.is_some(),
|
||||
verified: None,
|
||||
}))
|
||||
}
|
60
src/api/routes/signature/handlers/signature_key_post.rs
Normal file
60
src/api/routes/signature/handlers/signature_key_post.rs
Normal file
@ -0,0 +1,60 @@
|
||||
use super::super::sql;
|
||||
use axum::{debug_handler, Json};
|
||||
use chrono::Utc;
|
||||
use minisign::KeyPair;
|
||||
use serde::Deserialize;
|
||||
use ts_rs::TS;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::{
|
||||
api::{description::SIGNATURE_TAG, routes::AuthBackendType},
|
||||
errors::ApiError,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, TS, ToSchema)]
|
||||
#[ts(export)]
|
||||
pub struct CreateSigningKeyRequest {
|
||||
secret: String,
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/signature/key",
|
||||
summary = "Create signing key",
|
||||
description = "Create signing key for the currently logged in user.",
|
||||
request_body(content = CreateSigningKeyRequest, description = "Signing Key Creation Request", content_type = "application/json"),
|
||||
responses(
|
||||
(status = OK, description = "Successfully created signing key"),
|
||||
),
|
||||
security(
|
||||
("user_auth" = ["write:signature",]),
|
||||
),
|
||||
tag = SIGNATURE_TAG)]
|
||||
pub async fn create_signing_key(
|
||||
auth_session: AuthBackendType,
|
||||
Json(key_request): Json<CreateSigningKeyRequest>,
|
||||
) -> Result<(), ApiError> {
|
||||
let backend = auth_session.backend();
|
||||
let user = auth_session
|
||||
.is_authenticated()
|
||||
.ok_or(ApiError::AccessDenied)?;
|
||||
|
||||
// create Key Pair
|
||||
let KeyPair { pk, sk } = KeyPair::generate_encrypted_keypair(Some(key_request.secret))?;
|
||||
let public_key = pk.to_box()?.to_string();
|
||||
let private_key = sk
|
||||
.to_box(Some(&format!(
|
||||
"{} {} - {}",
|
||||
user.name,
|
||||
user.surname,
|
||||
Utc::now()
|
||||
)))? // Optional comment about the key
|
||||
.to_string();
|
||||
|
||||
// store keys in database
|
||||
sql::update_private_key(backend.pool(), &user.user_id, private_key).await?;
|
||||
sql::store_public_key(backend.pool(), &user.user_id, public_key).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
48
src/api/routes/signature/handlers/signature_key_put.rs
Normal file
48
src/api/routes/signature/handlers/signature_key_put.rs
Normal file
@ -0,0 +1,48 @@
|
||||
use super::super::models::{SigningKeyRequest, SigningKeyStatus};
|
||||
use axum::{debug_handler, Json};
|
||||
|
||||
use crate::{
|
||||
api::{description::SIGNATURE_TAG, routes::AuthBackendType},
|
||||
errors::ApiError,
|
||||
};
|
||||
|
||||
#[debug_handler]
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/signature/key",
|
||||
summary = "Verify signing key",
|
||||
description = "Verify signing key for the currently logged in user.",
|
||||
request_body(content = SigningKeyRequest, description = "Signing Key Request", content_type = "application/json"),
|
||||
responses(
|
||||
(status = OK, body = SigningKeyStatus, description = "Signature Status"),
|
||||
),
|
||||
security(
|
||||
("user_auth" = ["write:signature",]),
|
||||
),
|
||||
tag = SIGNATURE_TAG)]
|
||||
pub async fn verify_signing_key(
|
||||
auth_session: AuthBackendType,
|
||||
Json(key_request): Json<SigningKeyRequest>,
|
||||
) -> Result<Json<SigningKeyStatus>, ApiError> {
|
||||
let backend = auth_session.backend();
|
||||
let user = auth_session
|
||||
.is_authenticated()
|
||||
.ok_or(ApiError::AccessDenied)?;
|
||||
|
||||
// get private key
|
||||
let status = match key_request
|
||||
.to_private_key(backend.pool(), &user.user_id, backend.private_key_cache())
|
||||
.await?
|
||||
{
|
||||
Some(_key) => SigningKeyStatus {
|
||||
configured: true,
|
||||
verified: Some(true),
|
||||
},
|
||||
None => SigningKeyStatus {
|
||||
configured: false,
|
||||
verified: None,
|
||||
},
|
||||
};
|
||||
|
||||
Ok(Json(status))
|
||||
}
|
87
src/api/routes/signature/handlers/signature_sign_post.rs
Normal file
87
src/api/routes/signature/handlers/signature_sign_post.rs
Normal file
@ -0,0 +1,87 @@
|
||||
use std::io::Cursor;
|
||||
|
||||
use super::super::{
|
||||
models::{SignatureTest, SigningKeyRequest, VerificationRequest},
|
||||
sql,
|
||||
};
|
||||
use axum::{debug_handler, Json};
|
||||
|
||||
use crate::{
|
||||
api::{description::SIGNATURE_TAG, routes::AuthBackendType},
|
||||
errors::ApiError,
|
||||
};
|
||||
|
||||
#[debug_handler]
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/signature/sign",
|
||||
summary = "Sign something with your signing key",
|
||||
description = "Sign something as the currently logged in user.",
|
||||
request_body(content = SigningKeyRequest, description = "Signing Key Request", content_type = "application/json"),
|
||||
responses(
|
||||
(status = OK, description = "Successfully signed"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 500, description = "Internal Server Error")
|
||||
),
|
||||
security(
|
||||
("user_auth" = ["write:signature",]),
|
||||
),
|
||||
tag = SIGNATURE_TAG)]
|
||||
pub async fn sign(
|
||||
auth_session: AuthBackendType,
|
||||
Json(key_request): Json<SigningKeyRequest>,
|
||||
) -> Result<Json<VerificationRequest>, ApiError> {
|
||||
let backend = auth_session.backend();
|
||||
let user = auth_session
|
||||
.is_authenticated()
|
||||
.ok_or(ApiError::AccessDenied)?;
|
||||
|
||||
// get private key
|
||||
let private_key = key_request
|
||||
.to_private_key(backend.pool(), &user.user_id, backend.private_key_cache())
|
||||
.await?
|
||||
.ok_or(ApiError::AccessDenied)?;
|
||||
|
||||
// get current public key
|
||||
let public_key_id = sql::get_current_public_key(backend.pool(), &user.user_id)
|
||||
.await?
|
||||
.ok_or(ApiError::InternalError(
|
||||
"No corresponding public key found".to_string(),
|
||||
))?;
|
||||
|
||||
let data = SignatureTest {
|
||||
value1: "blfjdljfa".to_string(),
|
||||
value2: false,
|
||||
value3: vec!["bladfa".to_string(), "noch bla".to_string()],
|
||||
};
|
||||
|
||||
let data_reader = Cursor::new(data.calculate_hash().to_be_bytes());
|
||||
let signature_box = minisign::sign(
|
||||
None,
|
||||
&private_key,
|
||||
data_reader,
|
||||
None,
|
||||
Some(&serde_json::to_string(&public_key_id)?),
|
||||
)?;
|
||||
|
||||
// We have a signature! Let's inspect it a little bit.
|
||||
println!(
|
||||
"Untrusted comment: [{}]",
|
||||
signature_box.untrusted_comment().unwrap()
|
||||
);
|
||||
println!(
|
||||
"Trusted comment: [{}]",
|
||||
signature_box.trusted_comment().unwrap()
|
||||
);
|
||||
|
||||
// store signature
|
||||
// Converting the signature box to a string in order to save it is easy.
|
||||
let signature = signature_box.into_string();
|
||||
println!("BLA");
|
||||
println!("{signature}");
|
||||
|
||||
Ok(Json(VerificationRequest {
|
||||
signature,
|
||||
signed_from: "".to_string(),
|
||||
}))
|
||||
}
|
66
src/api/routes/signature/handlers/signature_verify_get.rs
Normal file
66
src/api/routes/signature/handlers/signature_verify_get.rs
Normal file
@ -0,0 +1,66 @@
|
||||
use std::io::Cursor;
|
||||
|
||||
use super::super::{
|
||||
models::{SignatureTest, SigningKeyRequest, VerificationRequest},
|
||||
sql,
|
||||
};
|
||||
use axum::{debug_handler, Json};
|
||||
use minisign::{PublicKeyBox, SignatureBox};
|
||||
|
||||
use crate::{
|
||||
api::{
|
||||
description::SIGNATURE_TAG,
|
||||
routes::{signature::models::PublicKeyID, AuthBackendType},
|
||||
},
|
||||
errors::ApiError,
|
||||
};
|
||||
|
||||
#[debug_handler]
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/signature/verify",
|
||||
summary = "Verify a signature",
|
||||
description = "Verify the signature.",
|
||||
request_body(content = SigningKeyRequest, description = "Signing Key Request", content_type = "application/json"),
|
||||
responses(
|
||||
(status = OK, description = "Successfully signed"),
|
||||
),
|
||||
tag = SIGNATURE_TAG)]
|
||||
pub async fn verify(
|
||||
auth_session: AuthBackendType,
|
||||
Json(verification_request): Json<VerificationRequest>,
|
||||
) -> Result<(), ApiError> {
|
||||
let backend = auth_session.backend();
|
||||
|
||||
// Now, let's verify the signature.
|
||||
// Assuming we just loaded it into `signature_box_str`, get the box back.
|
||||
let signature_box = SignatureBox::from_string(&verification_request.signature)?;
|
||||
|
||||
// get public key id from signature
|
||||
let public_key_id: PublicKeyID = serde_json::from_str(&signature_box.untrusted_comment()?)?;
|
||||
|
||||
// Load the public key of the user that signed.
|
||||
let public_key_str = sql::get_public_key_by_id(backend.pool(), public_key_id)
|
||||
.await?
|
||||
.key
|
||||
.ok_or(ApiError::InternalError(
|
||||
"No corresponding Public Key was found.".to_string(),
|
||||
))?;
|
||||
let public_key_box = PublicKeyBox::from_string(&public_key_str)?;
|
||||
let public_key = public_key_box.into_public_key()?;
|
||||
|
||||
// And verify the data.
|
||||
let data = SignatureTest {
|
||||
value1: "blfjdljfa".to_string(),
|
||||
value2: false,
|
||||
value3: vec!["bladfa".to_string(), "noch bla".to_string()],
|
||||
};
|
||||
let data_reader = Cursor::new(data.calculate_hash().to_be_bytes());
|
||||
let verified = minisign::verify(&public_key, &signature_box, data_reader, true, false, false);
|
||||
match verified {
|
||||
Ok(()) => println!("Success!"),
|
||||
Err(_) => println!("Verification failed"),
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
@ -1,24 +1,17 @@
|
||||
use std::io::Cursor;
|
||||
|
||||
use axum::{debug_handler, Json};
|
||||
use chrono::Utc;
|
||||
use minisign::{KeyPair, PublicKeyBox, SecretKeyBox, SignatureBox};
|
||||
use models::{
|
||||
PublicKeyID, SignatureTest, SigningKeyRequest, SigningKeyStatus, VerificationRequest,
|
||||
};
|
||||
use utoipa_axum::{router::OpenApiRouter, routes};
|
||||
|
||||
use crate::{
|
||||
api::{
|
||||
description::SIGNATURE_TAG,
|
||||
routes::users::permissions::{Permission, PermissionDetail},
|
||||
},
|
||||
errors::ApiError,
|
||||
api::routes::users::permissions::{Permission, PermissionDetail},
|
||||
permission_required,
|
||||
};
|
||||
|
||||
use super::AuthBackendType;
|
||||
use handlers::signature_key_get::*;
|
||||
use handlers::signature_key_post::*;
|
||||
use handlers::signature_key_put::*;
|
||||
use handlers::signature_sign_post::*;
|
||||
use handlers::signature_verify_get::*;
|
||||
|
||||
mod handlers;
|
||||
pub mod models;
|
||||
pub mod sql;
|
||||
|
||||
@ -38,238 +31,3 @@ pub fn router() -> OpenApiRouter {
|
||||
|
||||
OpenApiRouter::new().merge(write).merge(verify)
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/signature/key",
|
||||
summary = "Check for signing key",
|
||||
description = "Check if the logged in user has configured a signing key.",
|
||||
responses(
|
||||
(status = OK, body = SigningKeyStatus, description = "Signature Status"),
|
||||
),
|
||||
security(
|
||||
("user_auth" = ["write:signature",]),
|
||||
),
|
||||
tag = SIGNATURE_TAG)]
|
||||
pub async fn check_signing_key(
|
||||
auth_session: AuthBackendType,
|
||||
) -> Result<Json<SigningKeyStatus>, ApiError> {
|
||||
let backend = auth_session.backend();
|
||||
let user = auth_session
|
||||
.is_authenticated()
|
||||
.ok_or(ApiError::AccessDenied)?;
|
||||
|
||||
let private_key = sql::get_private_key(backend.pool(), &user.user_id).await?;
|
||||
|
||||
Ok(Json(SigningKeyStatus {
|
||||
configured: private_key.key.is_some(),
|
||||
verified: None,
|
||||
}))
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/signature/key",
|
||||
summary = "Create signing key",
|
||||
description = "Create signing key for the currently logged in user.",
|
||||
request_body(content = SigningKeyRequest, description = "Signing Key Request", content_type = "application/json"),
|
||||
responses(
|
||||
(status = OK, description = "Successfully created signing key"),
|
||||
),
|
||||
security(
|
||||
("user_auth" = ["write:signature",]),
|
||||
),
|
||||
tag = SIGNATURE_TAG)]
|
||||
pub async fn create_signing_key(
|
||||
auth_session: AuthBackendType,
|
||||
Json(key_request): Json<SigningKeyRequest>,
|
||||
) -> Result<(), ApiError> {
|
||||
let backend = auth_session.backend();
|
||||
let user = auth_session
|
||||
.is_authenticated()
|
||||
.ok_or(ApiError::AccessDenied)?;
|
||||
|
||||
// create Key Pair
|
||||
let KeyPair { pk, sk } = KeyPair::generate_encrypted_keypair(Some(key_request.secret))?;
|
||||
let public_key = pk.to_box()?.to_string();
|
||||
let private_key = sk
|
||||
.to_box(Some(&format!(
|
||||
"{} {} - {}",
|
||||
user.name,
|
||||
user.surname,
|
||||
Utc::now()
|
||||
)))? // Optional comment about the key
|
||||
.to_string();
|
||||
|
||||
// store keys in database
|
||||
sql::update_private_key(backend.pool(), &user.user_id, private_key).await?;
|
||||
sql::store_public_key(backend.pool(), &user.user_id, public_key).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/signature/key",
|
||||
summary = "Verify signing key",
|
||||
description = "Verify signing key for the currently logged in user.",
|
||||
request_body(content = SigningKeyRequest, description = "Signing Key Request", content_type = "application/json"),
|
||||
responses(
|
||||
(status = OK, body = SigningKeyStatus, description = "Signature Status"),
|
||||
),
|
||||
security(
|
||||
("user_auth" = ["write:signature",]),
|
||||
),
|
||||
tag = SIGNATURE_TAG)]
|
||||
pub async fn verify_signing_key(
|
||||
auth_session: AuthBackendType,
|
||||
Json(key_request): Json<SigningKeyRequest>,
|
||||
) -> Result<Json<SigningKeyStatus>, ApiError> {
|
||||
let backend = auth_session.backend();
|
||||
let user = auth_session
|
||||
.is_authenticated()
|
||||
.ok_or(ApiError::AccessDenied)?;
|
||||
|
||||
// get private key
|
||||
let status = match key_request
|
||||
.to_private_key(backend.pool(), &user.user_id)
|
||||
.await?
|
||||
{
|
||||
Some(_key) => SigningKeyStatus {
|
||||
configured: true,
|
||||
verified: Some(true),
|
||||
},
|
||||
None => SigningKeyStatus {
|
||||
configured: false,
|
||||
verified: None,
|
||||
},
|
||||
};
|
||||
|
||||
Ok(Json(status))
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/signature/sign",
|
||||
summary = "Sign something with your signing key",
|
||||
description = "Sign something as the currently logged in user.",
|
||||
request_body(content = SigningKeyRequest, description = "Signing Key Request", content_type = "application/json"),
|
||||
responses(
|
||||
(status = OK, description = "Successfully signed"),
|
||||
),
|
||||
security(
|
||||
("user_auth" = ["write:signature",]),
|
||||
),
|
||||
tag = SIGNATURE_TAG)]
|
||||
pub async fn sign(
|
||||
auth_session: AuthBackendType,
|
||||
Json(key_request): Json<SigningKeyRequest>,
|
||||
) -> Result<Json<VerificationRequest>, ApiError> {
|
||||
let backend = auth_session.backend();
|
||||
let user = auth_session
|
||||
.is_authenticated()
|
||||
.ok_or(ApiError::AccessDenied)?;
|
||||
|
||||
// get private key
|
||||
let private_key = key_request
|
||||
.to_private_key(backend.pool(), &user.user_id)
|
||||
.await?
|
||||
.ok_or(ApiError::AccessDenied)?;
|
||||
|
||||
// get current public key
|
||||
let public_key_id = sql::get_current_public_key(backend.pool(), &user.user_id)
|
||||
.await?
|
||||
.ok_or(ApiError::InternalError(
|
||||
"No corresponding public key found".to_string(),
|
||||
))?;
|
||||
|
||||
let data = SignatureTest {
|
||||
value1: "blfjdljfa".to_string(),
|
||||
value2: false,
|
||||
value3: vec!["bladfa".to_string(), "noch bla".to_string()],
|
||||
};
|
||||
|
||||
let data_reader = Cursor::new(data.calculate_hash().to_be_bytes());
|
||||
let signature_box = minisign::sign(
|
||||
None,
|
||||
&private_key,
|
||||
data_reader,
|
||||
None,
|
||||
Some(&serde_json::to_string(&public_key_id)?),
|
||||
)?;
|
||||
|
||||
// We have a signature! Let's inspect it a little bit.
|
||||
println!(
|
||||
"Untrusted comment: [{}]",
|
||||
signature_box.untrusted_comment().unwrap()
|
||||
);
|
||||
println!(
|
||||
"Trusted comment: [{}]",
|
||||
signature_box.trusted_comment().unwrap()
|
||||
);
|
||||
|
||||
// store signature
|
||||
// Converting the signature box to a string in order to save it is easy.
|
||||
let signature = signature_box.into_string();
|
||||
println!("BLA");
|
||||
println!("{signature}");
|
||||
|
||||
Ok(Json(VerificationRequest {
|
||||
signature,
|
||||
signed_from: "".to_string(),
|
||||
}))
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/signature/verify",
|
||||
summary = "Verify a signature",
|
||||
description = "Verify the signature.",
|
||||
request_body(content = SigningKeyRequest, description = "Signing Key Request", content_type = "application/json"),
|
||||
responses(
|
||||
(status = OK, description = "Successfully signed"),
|
||||
),
|
||||
tag = SIGNATURE_TAG)]
|
||||
pub async fn verify(
|
||||
auth_session: AuthBackendType,
|
||||
Json(verification_request): Json<VerificationRequest>,
|
||||
) -> Result<(), ApiError> {
|
||||
let backend = auth_session.backend();
|
||||
|
||||
// Now, let's verify the signature.
|
||||
// Assuming we just loaded it into `signature_box_str`, get the box back.
|
||||
let signature_box = SignatureBox::from_string(&verification_request.signature)?;
|
||||
|
||||
// get public key id from signature
|
||||
let public_key_id: PublicKeyID = serde_json::from_str(&signature_box.untrusted_comment()?)?;
|
||||
|
||||
// Load the public key of the user that signed.
|
||||
let public_key_str = sql::get_public_key_by_id(backend.pool(), public_key_id)
|
||||
.await?
|
||||
.key
|
||||
.ok_or(ApiError::InternalError(
|
||||
"No corresponding Public Key was found.".to_string(),
|
||||
))?;
|
||||
let public_key_box = PublicKeyBox::from_string(&public_key_str)?;
|
||||
let public_key = public_key_box.into_public_key()?;
|
||||
|
||||
// And verify the data.
|
||||
let data = SignatureTest {
|
||||
value1: "blfjdljfa".to_string(),
|
||||
value2: false,
|
||||
value3: vec!["bladfa".to_string(), "noch bla".to_string()],
|
||||
};
|
||||
let data_reader = Cursor::new(data.calculate_hash().to_be_bytes());
|
||||
let verified = minisign::verify(&public_key, &signature_box, data_reader, true, false, false);
|
||||
match verified {
|
||||
Ok(()) => println!("Success!"),
|
||||
Err(_) => println!("Verification failed"),
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
use std::{
|
||||
fmt::Display,
|
||||
hash::{DefaultHasher, Hash, Hasher},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use chrono::NaiveDateTime;
|
||||
@ -10,7 +10,10 @@ use sqlx::PgPool;
|
||||
use ts_rs::TS;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::{api::routes::signature::sql, errors::ApiError};
|
||||
use crate::{
|
||||
api::{backend::private_key_cache::PrivateKeyCache, routes::signature::sql},
|
||||
errors::ApiError,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS, ToSchema)]
|
||||
#[ts(export)]
|
||||
@ -32,7 +35,10 @@ pub struct PublicKeyID {
|
||||
#[derive(Debug, Clone, Deserialize, TS, ToSchema)]
|
||||
#[ts(export)]
|
||||
pub struct SigningKeyRequest {
|
||||
pub secret: String,
|
||||
// if no secret is given, the key will be searched in the cache
|
||||
pub secret: Option<String>,
|
||||
// if ttl is set, the key is cached for ttl in seconds
|
||||
pub ttl: Option<u32>,
|
||||
}
|
||||
|
||||
impl SigningKeyRequest {
|
||||
@ -40,19 +46,41 @@ impl SigningKeyRequest {
|
||||
self,
|
||||
pool: &PgPool,
|
||||
user_id: &str,
|
||||
private_key_cache: PrivateKeyCache,
|
||||
) -> Result<Option<SecretKey>, ApiError> {
|
||||
// try to get private key from database
|
||||
let private_key = match sql::get_private_key(pool, user_id).await?.key {
|
||||
Some(key) => key,
|
||||
None => return Ok(None),
|
||||
// get private key from database and decrypt it or get it from cache
|
||||
// this depends on the request containing a secret
|
||||
// if a secret and a TTL is given the decrypted private key is cached
|
||||
let private_key = match self.secret {
|
||||
Some(secret) => {
|
||||
// try to get private key from database
|
||||
let encrypted_private_key = match sql::get_private_key(pool, user_id).await?.key {
|
||||
Some(key) => key,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
// create MinSign Box from key string
|
||||
let private_key_box = SecretKeyBox::from_string(&encrypted_private_key)?;
|
||||
// and the box can be opened using the password to reveal the original secret key:
|
||||
let private_key = private_key_box.into_secret_key(Some(secret))?;
|
||||
|
||||
// cache key if TTL is set
|
||||
if let Some(ttl) = self.ttl {
|
||||
let duration = Duration::from_secs(ttl.into());
|
||||
private_key_cache
|
||||
.add_key(user_id, &private_key, duration)
|
||||
.await;
|
||||
}
|
||||
|
||||
private_key
|
||||
}
|
||||
None => match private_key_cache.get_key(user_id).await {
|
||||
Some(key) => key.secret_key(),
|
||||
None => return Err(ApiError::InvalidCredentials),
|
||||
},
|
||||
};
|
||||
|
||||
// create MinSign Box from key string
|
||||
let private_key_box = SecretKeyBox::from_string(&private_key)?;
|
||||
// and the box can be opened using the password to reveal the original secret key:
|
||||
let secret_key = private_key_box.into_secret_key(Some(self.secret))?;
|
||||
|
||||
Ok(Some(secret_key))
|
||||
Ok(Some(private_key))
|
||||
}
|
||||
}
|
||||
|
||||
|
4
src/api/routes/users/handlers/mod.rs
Normal file
4
src/api/routes/users/handlers/mod.rs
Normal file
@ -0,0 +1,4 @@
|
||||
pub mod users_available_ad_get;
|
||||
pub mod users_get;
|
||||
pub mod users_post;
|
||||
pub mod users_put;
|
53
src/api/routes/users/handlers/users_available_ad_get.rs
Normal file
53
src/api/routes/users/handlers/users_available_ad_get.rs
Normal file
@ -0,0 +1,53 @@
|
||||
use axum::{debug_handler, Extension, Json};
|
||||
use axum_jwt_login::UserPermissions;
|
||||
|
||||
use crate::{
|
||||
api::{
|
||||
backend::{
|
||||
ldap::{ActiveDirectoryUser, LDAPBackend},
|
||||
ApiBackend,
|
||||
},
|
||||
description::USERS_TAG,
|
||||
routes::{auth::models::Credentials, users::sql},
|
||||
},
|
||||
errors::ApiError,
|
||||
};
|
||||
|
||||
#[debug_handler]
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/users/available_ad_users",
|
||||
summary = "Get Active Directory Users",
|
||||
description = "Get all Available Users from the Active Directory that are not already registered with this API.",
|
||||
responses(
|
||||
(status = OK, body = Vec<ActiveDirectoryUser>, description = "List of AD users"),
|
||||
),
|
||||
security(
|
||||
("user_auth" = ["write:users",]),
|
||||
),
|
||||
tag = USERS_TAG)]
|
||||
pub async fn get_ad_users(
|
||||
Extension(backend): Extension<ApiBackend>,
|
||||
Json(credentials): Json<Option<Credentials>>,
|
||||
) -> Result<Json<Vec<ActiveDirectoryUser>>, ApiError> {
|
||||
let api_user_ids: Vec<String> = sql::get_users(backend.pool(), None, None)
|
||||
.await?
|
||||
.iter()
|
||||
.map(|user| user.id())
|
||||
.collect();
|
||||
let mut ldap = LDAPBackend::from_config(backend.config()).await?;
|
||||
// bind to AD user if credentials are given
|
||||
if let Some(credentials) = credentials {
|
||||
ldap.ad_bind(&credentials.id, &credentials.password).await?;
|
||||
}
|
||||
let ad_users = ldap
|
||||
.get_ad_user_list()
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|entry| !api_user_ids.contains(&entry.id))
|
||||
.collect();
|
||||
// disconnect from AD server
|
||||
ldap.unbind().await;
|
||||
|
||||
Ok(Json(ad_users))
|
||||
}
|
26
src/api/routes/users/handlers/users_get.rs
Normal file
26
src/api/routes/users/handlers/users_get.rs
Normal file
@ -0,0 +1,26 @@
|
||||
use axum::{debug_handler, Extension, Json};
|
||||
|
||||
use super::super::sql;
|
||||
use crate::{
|
||||
api::{backend::ApiBackend, description::USERS_TAG, routes::users::models::User},
|
||||
errors::ApiError,
|
||||
};
|
||||
|
||||
#[debug_handler]
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/users",
|
||||
summary = "Get all Users",
|
||||
description = "Get a list of all users.",
|
||||
responses(
|
||||
(status = OK, body = Vec<User>, description = "List of users"),
|
||||
),
|
||||
security(
|
||||
("user_auth" = ["read:users",]),
|
||||
),
|
||||
tag = USERS_TAG)]
|
||||
pub async fn get_users(
|
||||
Extension(backend): Extension<ApiBackend>,
|
||||
) -> Result<Json<Vec<User>>, ApiError> {
|
||||
Ok(Json(sql::get_users(backend.pool(), None, None).await?))
|
||||
}
|
46
src/api/routes/users/handlers/users_post.rs
Normal file
46
src/api/routes/users/handlers/users_post.rs
Normal file
@ -0,0 +1,46 @@
|
||||
use axum::{debug_handler, Extension, Json};
|
||||
|
||||
use super::super::sql;
|
||||
use crate::{
|
||||
api::{backend::ApiBackend, description::USERS_TAG, routes::users::models::User},
|
||||
errors::ApiError,
|
||||
utils::create_random,
|
||||
};
|
||||
|
||||
#[debug_handler]
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/users",
|
||||
summary = "Create a new User",
|
||||
description = "Creates a new user with the given information ",
|
||||
request_body(content = User, description = "User details", content_type = "application/json"),
|
||||
responses(
|
||||
(status = OK, description = "User successfully created (Assigned Password in body)", body = String),
|
||||
),
|
||||
security(
|
||||
("user_auth" = ["write:users",]),
|
||||
),
|
||||
tag = USERS_TAG)]
|
||||
pub async fn create_user(
|
||||
Extension(backend): Extension<ApiBackend>,
|
||||
Json(user): Json<User>,
|
||||
) -> Result<String, ApiError> {
|
||||
// create password if not Active Directory user
|
||||
let (password, hash) = match user.active_directory_auth {
|
||||
true => (String::new(), None),
|
||||
false => {
|
||||
let salt = create_random(20);
|
||||
let argon_config = argon2::Config::default();
|
||||
|
||||
let password = create_random(10);
|
||||
let password_hash =
|
||||
argon2::hash_encoded(password.as_bytes(), salt.as_bytes(), &argon_config)?;
|
||||
|
||||
(password, Some(password_hash))
|
||||
}
|
||||
};
|
||||
// create user
|
||||
sql::create_new_user(backend.pool(), &user, hash).await?;
|
||||
// send created password back to frontend
|
||||
Ok(password)
|
||||
}
|
33
src/api/routes/users/handlers/users_put.rs
Normal file
33
src/api/routes/users/handlers/users_put.rs
Normal file
@ -0,0 +1,33 @@
|
||||
use axum::{debug_handler, Extension, Json};
|
||||
|
||||
use crate::{
|
||||
api::{
|
||||
backend::ApiBackend,
|
||||
description::USERS_TAG,
|
||||
routes::users::{models::User, sql},
|
||||
},
|
||||
errors::ApiError,
|
||||
};
|
||||
|
||||
#[debug_handler]
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/users",
|
||||
summary = "Change User details",
|
||||
description = "Update user information / permissions / groups ",
|
||||
request_body(content = User, description = "User details", content_type = "application/json"),
|
||||
responses(
|
||||
(status = OK, description = "User successfully updated"),
|
||||
),
|
||||
security(
|
||||
("user_auth" = ["write:users"]),
|
||||
),
|
||||
tag = USERS_TAG)]
|
||||
pub async fn update_user(
|
||||
Extension(backend): Extension<ApiBackend>,
|
||||
Json(user): Json<User>,
|
||||
) -> Result<(), ApiError> {
|
||||
sql::update_user(backend.pool(), &user).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
@ -1,23 +1,14 @@
|
||||
use axum::{debug_handler, Extension, Json};
|
||||
use axum_jwt_login::UserPermissions;
|
||||
use models::User;
|
||||
use permissions::{Permission, PermissionDetail};
|
||||
use utoipa_axum::{router::OpenApiRouter, routes};
|
||||
|
||||
use crate::{
|
||||
api::{
|
||||
backend::{
|
||||
ldap::{ActiveDirectoryUser, LDAPBackend},
|
||||
ApiBackend,
|
||||
},
|
||||
description::USERS_TAG,
|
||||
routes::auth::models::Credentials,
|
||||
},
|
||||
errors::ApiError,
|
||||
permission_required,
|
||||
utils::create_random,
|
||||
};
|
||||
use crate::permission_required;
|
||||
|
||||
use handlers::users_available_ad_get::*;
|
||||
use handlers::users_get::*;
|
||||
use handlers::users_post::*;
|
||||
use handlers::users_put::*;
|
||||
|
||||
mod handlers;
|
||||
pub mod models;
|
||||
pub mod permissions;
|
||||
pub mod sql;
|
||||
@ -38,122 +29,3 @@ pub fn router() -> OpenApiRouter {
|
||||
|
||||
OpenApiRouter::new().merge(read).merge(write)
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/users",
|
||||
summary = "Get all Users",
|
||||
description = "Get a list of all users.",
|
||||
responses(
|
||||
(status = OK, body = Vec<User>, description = "List of users"),
|
||||
),
|
||||
security(
|
||||
("user_auth" = ["read:users",]),
|
||||
),
|
||||
tag = USERS_TAG)]
|
||||
pub async fn get_users(
|
||||
Extension(backend): Extension<ApiBackend>,
|
||||
) -> Result<Json<Vec<User>>, ApiError> {
|
||||
Ok(Json(sql::get_users(backend.pool(), None, None).await?))
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/users",
|
||||
summary = "Change User details",
|
||||
description = "Update user information / permissions / groups ",
|
||||
request_body(content = User, description = "User details", content_type = "application/json"),
|
||||
responses(
|
||||
(status = OK, description = "User successfully updated"),
|
||||
),
|
||||
security(
|
||||
("user_auth" = ["write:users"]),
|
||||
),
|
||||
tag = USERS_TAG)]
|
||||
pub async fn update_user(
|
||||
Extension(backend): Extension<ApiBackend>,
|
||||
Json(user): Json<User>,
|
||||
) -> Result<(), ApiError> {
|
||||
sql::update_user(backend.pool(), &user).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/users",
|
||||
summary = "Create a new User",
|
||||
description = "Creates a new user with the given information ",
|
||||
request_body(content = User, description = "User details", content_type = "application/json"),
|
||||
responses(
|
||||
(status = OK, description = "User successfully created (Assigned Password in body)", body = String),
|
||||
),
|
||||
security(
|
||||
("user_auth" = ["write:users",]),
|
||||
),
|
||||
tag = USERS_TAG)]
|
||||
pub async fn create_user(
|
||||
Extension(backend): Extension<ApiBackend>,
|
||||
Json(user): Json<User>,
|
||||
) -> Result<String, ApiError> {
|
||||
// create password if not Active Directory user
|
||||
let (password, hash) = match user.active_directory_auth {
|
||||
true => (String::new(), None),
|
||||
false => {
|
||||
let salt = create_random(20);
|
||||
let argon_config = argon2::Config::default();
|
||||
|
||||
let password = create_random(10);
|
||||
let password_hash =
|
||||
argon2::hash_encoded(password.as_bytes(), salt.as_bytes(), &argon_config)?;
|
||||
|
||||
(password, Some(password_hash))
|
||||
}
|
||||
};
|
||||
// create user
|
||||
sql::create_new_user(backend.pool(), &user, hash).await?;
|
||||
// send created password back to frontend
|
||||
Ok(password)
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/users/available_ad_users",
|
||||
summary = "Get Active Directory Users",
|
||||
description = "Get all Available Users from the Active Directory that are not already registered with this API.",
|
||||
responses(
|
||||
(status = OK, body = Vec<ActiveDirectoryUser>, description = "List of AD users"),
|
||||
),
|
||||
security(
|
||||
("user_auth" = ["write:users",]),
|
||||
),
|
||||
tag = USERS_TAG)]
|
||||
pub async fn get_ad_users(
|
||||
Extension(backend): Extension<ApiBackend>,
|
||||
Json(credentials): Json<Option<Credentials>>,
|
||||
) -> Result<Json<Vec<ActiveDirectoryUser>>, ApiError> {
|
||||
let api_user_ids: Vec<String> = sql::get_users(backend.pool(), None, None)
|
||||
.await?
|
||||
.iter()
|
||||
.map(|user| user.id())
|
||||
.collect();
|
||||
let mut ldap = LDAPBackend::from_config(backend.config()).await?;
|
||||
// bind to AD user if credentials are given
|
||||
if let Some(credentials) = credentials {
|
||||
ldap.ad_bind(&credentials.id, &credentials.password).await?;
|
||||
}
|
||||
let ad_users = ldap
|
||||
.get_ad_user_list()
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|entry| !api_user_ids.contains(&entry.id))
|
||||
.collect();
|
||||
// disconnect from AD server
|
||||
ldap.unbind().await;
|
||||
|
||||
Ok(Json(ad_users))
|
||||
}
|
||||
|
@ -1,10 +1,12 @@
|
||||
use std::fmt::{self, Display};
|
||||
|
||||
use axum::{
|
||||
extract::multipart::MultipartError,
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use chrono::{NaiveDateTime, ParseError, ParseResult};
|
||||
use error_stack::Context;
|
||||
use minisign::PError;
|
||||
use serde::Serialize;
|
||||
@ -36,6 +38,9 @@ pub enum ApiError {
|
||||
InvalidCredentials,
|
||||
InternalError(String),
|
||||
AccessDenied,
|
||||
MultipartForm(StatusCode, String),
|
||||
InvalidRequest(String),
|
||||
FileNotFound,
|
||||
}
|
||||
|
||||
impl ApiError {
|
||||
@ -44,7 +49,7 @@ impl ApiError {
|
||||
Self::InvalidCredentials => (StatusCode::UNAUTHORIZED, "Invalid credentials", None),
|
||||
Self::SQLQueryError(error) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Invalid credentials",
|
||||
"Error executing SQL request",
|
||||
Some(error),
|
||||
),
|
||||
Self::InternalError(error) => (
|
||||
@ -52,6 +57,13 @@ impl ApiError {
|
||||
"Internal Server Error",
|
||||
Some(error),
|
||||
),
|
||||
Self::InvalidRequest(s) => (
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Request contains invalid or missing data",
|
||||
Some(s),
|
||||
),
|
||||
Self::MultipartForm(c, s) => (*c, "Multipart Error", Some(s)),
|
||||
Self::FileNotFound => (StatusCode::NOT_FOUND, "File not found", None),
|
||||
Self::AccessDenied => (StatusCode::FORBIDDEN, "Access Denied", None), // ApiError::WrongCredentials => (StatusCode::UNAUTHORIZED, "Wrong credentials"),
|
||||
// ApiError::MissingCredentials => (StatusCode::BAD_REQUEST, "Missing credentials"),
|
||||
// ApiError::TokenCreation => (StatusCode::INTERNAL_SERVER_ERROR, "Token creation error"),
|
||||
@ -123,6 +135,12 @@ impl From<serde_json::Error> for ApiError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MultipartError> for ApiError {
|
||||
fn from(value: MultipartError) -> Self {
|
||||
Self::MultipartForm(value.status(), value.body_text())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ApiError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let error_info = self.as_error_info().1;
|
||||
|
21
src/migrations/07_draft_files.sql
Normal file
21
src/migrations/07_draft_files.sql
Normal file
@ -0,0 +1,21 @@
|
||||
CREATE TABLE IF NOT EXISTS public.draftfiles
|
||||
(
|
||||
"DraftID" timestamp without time zone NOT NULL,
|
||||
"UserID" character varying(10) COLLATE pg_catalog."default" NOT NULL,
|
||||
"ContentType" character varying(255) COLLATE pg_catalog."default" NOT NULL,
|
||||
"Name" character varying COLLATE pg_catalog."default" NOT NULL,
|
||||
"Hash" character varying COLLATE pg_catalog."default" NOT NULL,
|
||||
"Data" bytea NOT NULL,
|
||||
"Size" integer NOT NULL,
|
||||
CONSTRAINT draftfiles_pkey PRIMARY KEY ("DraftID", "UserID", "Hash"),
|
||||
CONSTRAINT "UserID" FOREIGN KEY ("UserID")
|
||||
REFERENCES public.users ("UserID") MATCH SIMPLE
|
||||
ON UPDATE NO ACTION
|
||||
ON DELETE NO ACTION
|
||||
NOT VALID
|
||||
)
|
||||
|
||||
TABLESPACE pg_default;
|
||||
|
||||
ALTER TABLE IF EXISTS public.draftfiles
|
||||
OWNER to postgres;
|
Loading…
Reference in New Issue
Block a user