Skip to content

Commit

Permalink
feat: API keys (#135)
Browse files Browse the repository at this point in the history
  • Loading branch information
darioAnongba authored Oct 15, 2024
1 parent 31fb9f5 commit b9fc0ee
Show file tree
Hide file tree
Showing 27 changed files with 56 additions and 34 deletions.
13 changes: 6 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ humantime = "2.1.0"
rust_socketio = { version = "0.6.0", features = ["async"] }
bcrypt = "0.15.1"
migration = { path = "migration" }
utoipa = { version = "4.2.3", features = ["axum_extras", "chrono", "uuid"] }
utoipa-scalar = { version = "0.1.0", features = ["axum"] }
utoipa = { version = "5.0.0", features = ["axum_extras", "chrono", "uuid"] }
utoipa-scalar = { version = "0.2.0", features = ["axum"] }
native-tls = "0.2.12"
nostr-sdk = "0.35.0"
rand = "0.8.5"
Expand Down
1 change: 1 addition & 0 deletions migration/src/m20241009_6_api_key_table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ impl MigrationTrait for Migration {
CREATE TABLE api_key (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id VARCHAR(255) NOT NULL,
name VARCHAR(255) NOT NULL,
key_hash BYTEA UNIQUE NOT NULL,
permissions TEXT[] NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT current_timestamp,
Expand Down
5 changes: 5 additions & 0 deletions src/application/dtos/api_key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ use crate::domains::user::{ApiKey, Permission};
pub struct CreateApiKeyRequest {
/// User ID. Will be populated with your own ID by default
pub user_id: Option<String>,
/// API key name
pub name: String,
/// List of permissions for this API key
pub permissions: Vec<Permission>,
/// API key description
Expand All @@ -25,6 +27,8 @@ pub struct ApiKeyResponse {
pub id: Uuid,
/// User ID
pub user_id: String,
/// API key name
pub name: String,
/// API key (only returned once on creation, save it securely as it cannot be retrieved)
#[serde(skip_serializing_if = "Option::is_none")]
pub key: Option<String>,
Expand All @@ -43,6 +47,7 @@ impl From<ApiKey> for ApiKeyResponse {
ApiKeyResponse {
id: key.id,
user_id: key.user_id,
name: key.name,
key: key.key,
permissions: key.permissions,
description: key.description,
Expand Down
3 changes: 2 additions & 1 deletion src/application/errors/authentication_error.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use thiserror::Error;
use utoipa::ToSchema;

#[derive(Debug, Error)]
#[derive(Debug, Error, ToSchema)]
pub enum AuthenticationError {
#[error("Failed to fetch JWKS: {0}")]
Jwks(String),
Expand Down
3 changes: 2 additions & 1 deletion src/application/errors/authorization_error.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use thiserror::Error;
use utoipa::ToSchema;

use crate::domains::user::Permission;

#[derive(Debug, Error)]
#[derive(Debug, Error, ToSchema)]
pub enum AuthorizationError {
#[error("Missing required permission: {0:?}")]
MissingPermission(Permission),
Expand Down
3 changes: 2 additions & 1 deletion src/application/errors/config_error.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use thiserror::Error;
use utoipa::ToSchema;

#[derive(Debug, Error)]
#[derive(Debug, Error, ToSchema)]
pub enum ConfigError {
#[error("Failed to load configuration: {0}")]
Load(String),
Expand Down
3 changes: 2 additions & 1 deletion src/application/errors/data_error.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use thiserror::Error;
use utoipa::ToSchema;

#[derive(Debug, Error)]
#[derive(Debug, Error, ToSchema)]
pub enum DataError {
#[error("Conflict: {0}")]
Conflict(String),
Expand Down
3 changes: 2 additions & 1 deletion src/application/errors/database_error.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use thiserror::Error;
use utoipa::ToSchema;

#[derive(Debug, Error)]
#[derive(Debug, Error, ToSchema)]
pub enum DatabaseError {
#[error("Failed to connect to database: {0}")]
Connect(String),
Expand Down
3 changes: 2 additions & 1 deletion src/application/errors/lightning_error.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use thiserror::Error;
use utoipa::ToSchema;

#[derive(Debug, Error)]
#[derive(Debug, Error, ToSchema)]
pub enum LightningError {
#[error("Failed to initialize logging: {0}")]
Logging(String),
Expand Down
3 changes: 2 additions & 1 deletion src/application/errors/web_server_error.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use thiserror::Error;
use utoipa::ToSchema;

#[derive(Debug, Error)]
#[derive(Debug, Error, ToSchema)]
pub enum WebServerError {
#[error("Failed to create TCP listener: {0}")]
Listener(String),
Expand Down
2 changes: 1 addition & 1 deletion src/domains/invoice/invoice_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use crate::{
BAD_REQUEST_EXAMPLE, FORBIDDEN_EXAMPLE, INTERNAL_EXAMPLE, NOT_FOUND_EXAMPLE,
UNAUTHORIZED_EXAMPLE, UNPROCESSABLE_EXAMPLE,
},
dtos::{InvoiceResponse, LnInvoiceResponse, NewInvoiceRequest},
dtos::{ErrorResponse, InvoiceResponse, LnInvoiceResponse, NewInvoiceRequest},
errors::ApplicationError,
},
domains::user::{Permission, User},
Expand Down
2 changes: 1 addition & 1 deletion src/domains/ln_address/ln_address_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use crate::{
BAD_REQUEST_EXAMPLE, FORBIDDEN_EXAMPLE, INTERNAL_EXAMPLE, NOT_FOUND_EXAMPLE,
UNAUTHORIZED_EXAMPLE, UNPROCESSABLE_EXAMPLE,
},
dtos::{RegisterLnAddressRequest, UpdateLnAddressRequest},
dtos::{ErrorResponse, RegisterLnAddressRequest, UpdateLnAddressRequest},
errors::ApplicationError,
},
domains::user::{Permission, User},
Expand Down
6 changes: 3 additions & 3 deletions src/domains/ln_node/breez_node_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ use crate::{
UNAUTHORIZED_EXAMPLE,
},
dtos::{
CheckMessageRequest, CheckMessageResponse, ConnectLSPRequest, RedeemOnchainRequest,
RedeemOnchainResponse, SendOnchainPaymentRequest, SignMessageRequest,
SignMessageResponse,
CheckMessageRequest, CheckMessageResponse, ConnectLSPRequest, ErrorResponse,
RedeemOnchainRequest, RedeemOnchainResponse, SendOnchainPaymentRequest,
SignMessageRequest, SignMessageResponse,
},
errors::{ApplicationError, LightningError},
},
Expand Down
2 changes: 1 addition & 1 deletion src/domains/lnurl/lnurl_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use utoipa::OpenApi;
use crate::{
application::{
docs::{BAD_REQUEST_EXAMPLE, INTERNAL_EXAMPLE, NOT_FOUND_EXAMPLE, UNPROCESSABLE_EXAMPLE},
dtos::{LNUrlpInvoiceQueryParams, LnUrlCallbackResponse},
dtos::{ErrorResponse, LNUrlpInvoiceQueryParams, LnUrlCallbackResponse},
errors::ApplicationError,
},
infra::{
Expand Down
6 changes: 3 additions & 3 deletions src/domains/nostr/nostr_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use utoipa::OpenApi;
use crate::{
application::{
docs::{INTERNAL_EXAMPLE, NOT_FOUND_EXAMPLE},
dtos::{NostrNIP05QueryParams, NostrNIP05Response},
dtos::{ErrorResponse, NostrNIP05QueryParams, NostrNIP05Response},
errors::ApplicationError,
},
infra::{
Expand All @@ -17,7 +17,7 @@ use crate::{

#[derive(OpenApi)]
#[openapi(
paths(well_known),
paths(well_known_nostr),
components(schemas(NostrNIP05Response)),
tags(
(name = "Nostr", description = "Public Nostr endpoints as defined in the [protocol specification](https://github.com/nostr-protocol/nips). Allows any Nostr client to identify a user's public keys")
Expand All @@ -40,7 +40,7 @@ pub struct NostrHandler;
(status = 500, description = "Internal Server Error", body = ErrorResponse, example = json!(INTERNAL_EXAMPLE))
)
)]
pub async fn well_known(
pub async fn well_known_nostr(
Query(query_params): Query<NostrNIP05QueryParams>,
State(app_state): State<Arc<AppState>>,
) -> Result<Json<NostrNIP05Response>, ApplicationError> {
Expand Down
2 changes: 1 addition & 1 deletion src/domains/payment/payment_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use crate::{
BAD_REQUEST_EXAMPLE, FORBIDDEN_EXAMPLE, INTERNAL_EXAMPLE, NOT_FOUND_EXAMPLE,
UNAUTHORIZED_EXAMPLE, UNPROCESSABLE_EXAMPLE,
},
dtos::{PaymentResponse, SendPaymentRequest},
dtos::{ErrorResponse, PaymentResponse, SendPaymentRequest},
errors::ApplicationError,
},
domains::{
Expand Down
2 changes: 1 addition & 1 deletion src/domains/user/api_key_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use crate::{
BAD_REQUEST_EXAMPLE, FORBIDDEN_EXAMPLE, INTERNAL_EXAMPLE, NOT_FOUND_EXAMPLE,
UNAUTHORIZED_EXAMPLE, UNPROCESSABLE_EXAMPLE,
},
dtos::{ApiKeyResponse, CreateApiKeyRequest},
dtos::{ApiKeyResponse, CreateApiKeyRequest, ErrorResponse},
errors::ApplicationError,
},
infra::{
Expand Down
3 changes: 2 additions & 1 deletion src/domains/user/api_key_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,12 @@ impl ApiKeyUseCases for ApiKeyService {
// Generate a new API key
let mut bytes = [0u8; 32];
OsRng.fill_bytes(&mut bytes);
let api_key_plain = BASE64_STANDARD.encode(&bytes);
let api_key_plain = BASE64_STANDARD.encode(bytes);
let key_hash = sha256::Hash::hash(&bytes).to_vec();

let api_key = ApiKey {
user_id: request.user_id.expect("user_id should be defined"),
name: request.name,
key_hash,
permissions: request.permissions.clone(),
expires_at,
Expand Down
2 changes: 1 addition & 1 deletion src/domains/user/auth_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use utoipa::OpenApi;
use crate::{
application::{
docs::{BAD_REQUEST_EXAMPLE, UNAUTHORIZED_EXAMPLE, UNSUPPORTED_EXAMPLE},
dtos::{SignInRequest, SignInResponse},
dtos::{ErrorResponse, SignInRequest, SignInResponse},
errors::ApplicationError,
},
infra::{app::AppState, axum::Json},
Expand Down
1 change: 1 addition & 0 deletions src/domains/user/entities/api_key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use super::Permission;
pub struct ApiKey {
pub id: Uuid,
pub user_id: String,
pub name: String,
pub key: Option<String>,
pub key_hash: Vec<u8>,
pub permissions: Vec<Permission>,
Expand Down
2 changes: 1 addition & 1 deletion src/domains/wallet/user_wallet_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use crate::{
UNPROCESSABLE_EXAMPLE,
},
dtos::{
ApiKeyResponse, CreateApiKeyRequest, InvoiceResponse, NewInvoiceRequest,
ApiKeyResponse, CreateApiKeyRequest, ErrorResponse, InvoiceResponse, NewInvoiceRequest,
PaymentResponse, RegisterLnAddressRequest, SendPaymentRequest, UpdateLnAddressRequest,
WalletResponse,
},
Expand Down
2 changes: 1 addition & 1 deletion src/domains/wallet/wallet_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use crate::{
BAD_REQUEST_EXAMPLE, FORBIDDEN_EXAMPLE, INTERNAL_EXAMPLE, NOT_FOUND_EXAMPLE,
UNAUTHORIZED_EXAMPLE, UNPROCESSABLE_EXAMPLE,
},
dtos::{RegisterWalletRequest, WalletResponse},
dtos::{ErrorResponse, RegisterWalletRequest, WalletResponse},
errors::ApplicationError,
},
domains::user::{Permission, User},
Expand Down
2 changes: 1 addition & 1 deletion src/infra/app/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,6 @@ impl Server {
fn well_known_router() -> Router<Arc<AppState>> {
Router::new()
.route("/lnurlp/:username", get(lnurl::well_known))
.route("/nostr.json", get(nostr::well_known))
.route("/nostr.json", get(nostr::well_known_nostr))
}
}
1 change: 1 addition & 0 deletions src/infra/database/sea_orm/models/api_key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: Uuid,
pub user_id: String,
pub name: String,
#[sea_orm(column_type = "VarBinary(32)", unique)]
pub key_hash: Vec<u8>,
pub permissions: Vec<String>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ use crate::{
};
use async_trait::async_trait;
use sea_orm::{
ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, QueryOrder,
QuerySelect, QueryTrait, Set,
sea_query::Expr, ActiveModelTrait, ColumnTrait, Condition, DatabaseConnection, EntityTrait,
QueryFilter, QueryOrder, QuerySelect, QueryTrait, Set,
};
use uuid::Uuid;

Expand Down Expand Up @@ -35,6 +35,11 @@ impl ApiKeyRepository for SeaOrmApiKeyRepository {
async fn find_by_key_hash(&self, key_hash: Vec<u8>) -> Result<Option<ApiKey>, DatabaseError> {
let model = Entity::find()
.filter(Column::KeyHash.eq(key_hash))
.filter(
Condition::any()
.add(Expr::col(Column::ExpiresAt).gt(Expr::current_timestamp()))
.add(Expr::col(Column::ExpiresAt).is_null()),
)
.one(&self.db)
.await
.map_err(|e| DatabaseError::FindOne(e.to_string()))?;
Expand All @@ -59,6 +64,7 @@ impl ApiKeyRepository for SeaOrmApiKeyRepository {
async fn insert(&self, api_key: ApiKey) -> Result<ApiKey, DatabaseError> {
let model = ActiveModel {
user_id: Set(api_key.user_id),
name: Set(api_key.name),
key_hash: Set(api_key.key_hash),
permissions: Set(api_key.permissions.iter().map(|p| p.to_string()).collect()),
description: Set(api_key.description),
Expand Down
1 change: 1 addition & 0 deletions src/infra/database/sea_orm/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ impl From<ApiKeyModel> for ApiKey {
ApiKey {
id: model.id,
user_id: model.user_id,
name: model.name,
key: None,
key_hash: model.key_hash,
permissions: model
Expand Down

0 comments on commit b9fc0ee

Please sign in to comment.