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> {
 | 
			
		||||
        // 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 private_key = match sql::get_private_key(pool, user_id).await?.key {
 | 
			
		||||
                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(&private_key)?;
 | 
			
		||||
                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 secret_key = private_key_box.into_secret_key(Some(self.secret))?;
 | 
			
		||||
                let private_key = private_key_box.into_secret_key(Some(secret))?;
 | 
			
		||||
 | 
			
		||||
        Ok(Some(secret_key))
 | 
			
		||||
                // 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),
 | 
			
		||||
            },
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        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