mirror of
https://github.com/warp-tech/warpgate.git
synced 2025-09-06 14:44:24 +08:00
API tokens (#1191)
This commit is contained in:
parent
42301623e8
commit
010534a12f
32 changed files with 859 additions and 95 deletions
|
@ -7,7 +7,7 @@ use warpgate_core::Services;
|
|||
pub fn admin_api_app(services: &Services) -> impl IntoEndpoint {
|
||||
let api_service = OpenApiService::new(
|
||||
crate::api::get(),
|
||||
"Warpgate Web Admin",
|
||||
"Warpgate admin API",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
)
|
||||
.server("/@warpgate/admin/api");
|
||||
|
|
|
@ -431,9 +431,30 @@ impl ConfigProvider for DatabaseConfigProvider {
|
|||
|
||||
active_model.update(&*db).await.map_err(|e| {
|
||||
error!("Failed to update last_used for public key: {:?}", e);
|
||||
WarpgateError::DatabaseError(e.into())
|
||||
WarpgateError::DatabaseError(e)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn validate_api_token(&mut self, token: &str) -> Result<Option<User>, WarpgateError> {
|
||||
let db = self.db.lock().await;
|
||||
let Some(ticket) = entities::ApiToken::Entity::find()
|
||||
.filter(entities::ApiToken::Column::Secret.eq(token))
|
||||
.one(&*db)
|
||||
.await?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let Some(user) = ticket
|
||||
.find_related(entities::User::Entity)
|
||||
.one(&*db)
|
||||
.await?
|
||||
else {
|
||||
return Err(WarpgateError::InconsistentState);
|
||||
};
|
||||
|
||||
Ok(Some(user.try_into()?))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -52,6 +52,8 @@ pub trait ConfigProvider {
|
|||
&self,
|
||||
credential: Option<AuthCredential>,
|
||||
) -> Result<(), WarpgateError>;
|
||||
|
||||
async fn validate_api_token(&mut self, token: &str) -> Result<Option<User>, WarpgateError>;
|
||||
}
|
||||
|
||||
//TODO: move this somewhere
|
||||
|
|
42
warpgate-db-entities/src/ApiToken.rs
Normal file
42
warpgate-db-entities/src/ApiToken.rs
Normal file
|
@ -0,0 +1,42 @@
|
|||
use chrono::{DateTime, Utc};
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::sea_query::ForeignKeyAction;
|
||||
use serde::Serialize;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize)]
|
||||
#[sea_orm(table_name = "api_tokens")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub label: String,
|
||||
pub secret: String,
|
||||
pub created: DateTime<Utc>,
|
||||
pub expiry: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter)]
|
||||
pub enum Relation {
|
||||
User,
|
||||
}
|
||||
|
||||
impl RelationTrait for Relation {
|
||||
fn def(&self) -> RelationDef {
|
||||
match self {
|
||||
Self::User => Entity::belongs_to(super::User::Entity)
|
||||
.from(Column::UserId)
|
||||
.to(super::User::Column::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade)
|
||||
.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::User::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::User.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
|
@ -51,6 +51,12 @@ impl Related<super::SsoCredential::Entity> for Entity {
|
|||
}
|
||||
}
|
||||
|
||||
impl Related<super::ApiToken::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::ApiTokens.def()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter)]
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
pub enum Relation {
|
||||
|
@ -58,6 +64,7 @@ pub enum Relation {
|
|||
PasswordCredentials,
|
||||
PublicKeyCredentials,
|
||||
SsoCredentials,
|
||||
ApiTokens,
|
||||
}
|
||||
|
||||
impl RelationTrait for Relation {
|
||||
|
@ -79,6 +86,10 @@ impl RelationTrait for Relation {
|
|||
.from(Column::Id)
|
||||
.to(super::SsoCredential::Column::UserId)
|
||||
.into(),
|
||||
Self::ApiTokens => Entity::has_many(super::ApiToken::Entity)
|
||||
.from(Column::Id)
|
||||
.to(super::ApiToken::Column::UserId)
|
||||
.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
#![allow(non_snake_case)]
|
||||
|
||||
pub mod ApiToken;
|
||||
pub mod KnownHost;
|
||||
pub mod LogEntry;
|
||||
pub mod OtpCredential;
|
||||
|
|
|
@ -15,6 +15,7 @@ mod m00010_parameters;
|
|||
mod m00011_rsa_key_algos;
|
||||
mod m00012_add_openssh_public_key_label;
|
||||
mod m00013_add_openssh_public_key_dates;
|
||||
mod m00014_api_tokens;
|
||||
|
||||
pub struct Migrator;
|
||||
|
||||
|
@ -35,6 +36,7 @@ impl MigratorTrait for Migrator {
|
|||
Box::new(m00011_rsa_key_algos::Migration),
|
||||
Box::new(m00012_add_openssh_public_key_label::Migration),
|
||||
Box::new(m00013_add_openssh_public_key_dates::Migration),
|
||||
Box::new(m00014_api_tokens::Migration),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
76
warpgate-db-migrations/src/m00014_api_tokens.rs
Normal file
76
warpgate-db-migrations/src/m00014_api_tokens.rs
Normal file
|
@ -0,0 +1,76 @@
|
|||
use sea_orm::Schema;
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
use super::m00008_users::user as User;
|
||||
|
||||
pub mod api_tokens {
|
||||
use chrono::{DateTime, Utc};
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::sea_query::ForeignKeyAction;
|
||||
use serde::Serialize;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize)]
|
||||
#[sea_orm(table_name = "api_tokens")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub label: String,
|
||||
pub secret: String,
|
||||
pub created: DateTime<Utc>,
|
||||
pub expiry: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter)]
|
||||
pub enum Relation {
|
||||
User,
|
||||
}
|
||||
|
||||
impl RelationTrait for Relation {
|
||||
fn def(&self) -> RelationDef {
|
||||
match self {
|
||||
Self::User => Entity::belongs_to(super::User::Entity)
|
||||
.from(Column::UserId)
|
||||
.to(super::User::Column::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade)
|
||||
.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::User::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::User.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
}
|
||||
|
||||
pub struct Migration;
|
||||
|
||||
impl MigrationName for Migration {
|
||||
fn name(&self) -> &str {
|
||||
"m00014_api_tokens"
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
let builder = manager.get_database_backend();
|
||||
let schema = Schema::new(builder);
|
||||
manager
|
||||
.create_table(schema.create_table_from_entity(api_tokens::Entity))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(api_tokens::Entity).to_owned())
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
168
warpgate-protocol-http/src/api/api_tokens.rs
Normal file
168
warpgate-protocol-http/src/api/api_tokens.rs
Normal file
|
@ -0,0 +1,168 @@
|
|||
use chrono::{DateTime, Utc};
|
||||
use poem::web::Data;
|
||||
use poem_openapi::param::Path;
|
||||
use poem_openapi::payload::Json;
|
||||
use poem_openapi::{ApiResponse, Object, OpenApi};
|
||||
use sea_orm::{ActiveModelTrait, ColumnTrait, ModelTrait, QueryFilter, Set};
|
||||
use uuid::Uuid;
|
||||
use warpgate_common::helpers::hash::generate_ticket_secret;
|
||||
use warpgate_common::WarpgateError;
|
||||
use warpgate_core::Services;
|
||||
use warpgate_db_entities::ApiToken;
|
||||
|
||||
use super::common::get_user;
|
||||
use crate::common::{endpoint_auth, RequestAuthorization};
|
||||
|
||||
pub struct Api;
|
||||
|
||||
#[derive(ApiResponse)]
|
||||
enum GetApiTokensResponse {
|
||||
#[oai(status = 200)]
|
||||
Ok(Json<Vec<ExistingApiToken>>),
|
||||
#[oai(status = 401)]
|
||||
Unauthorized,
|
||||
}
|
||||
|
||||
#[derive(Object)]
|
||||
struct NewApiToken {
|
||||
label: String,
|
||||
expiry: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Object)]
|
||||
struct ExistingApiToken {
|
||||
id: Uuid,
|
||||
label: String,
|
||||
created: DateTime<Utc>,
|
||||
expiry: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl From<ApiToken::Model> for ExistingApiToken {
|
||||
fn from(token: ApiToken::Model) -> Self {
|
||||
Self {
|
||||
id: token.id,
|
||||
label: token.label,
|
||||
created: token.created,
|
||||
expiry: token.expiry,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Object)]
|
||||
struct TokenAndSecret {
|
||||
token: ExistingApiToken,
|
||||
secret: String,
|
||||
}
|
||||
|
||||
#[derive(ApiResponse)]
|
||||
enum CreateApiTokenResponse {
|
||||
#[oai(status = 201)]
|
||||
Created(Json<TokenAndSecret>),
|
||||
#[oai(status = 401)]
|
||||
Unauthorized,
|
||||
}
|
||||
|
||||
#[derive(ApiResponse)]
|
||||
enum DeleteApiTokenResponse {
|
||||
#[oai(status = 204)]
|
||||
Deleted,
|
||||
#[oai(status = 401)]
|
||||
Unauthorized,
|
||||
#[oai(status = 404)]
|
||||
NotFound,
|
||||
}
|
||||
|
||||
#[OpenApi]
|
||||
impl Api {
|
||||
#[oai(
|
||||
path = "/profile/api-tokens",
|
||||
method = "get",
|
||||
operation_id = "get_my_api_tokens",
|
||||
transform = "endpoint_auth"
|
||||
)]
|
||||
async fn api_get_api_tokens(
|
||||
&self,
|
||||
auth: Data<&RequestAuthorization>,
|
||||
services: Data<&Services>,
|
||||
) -> Result<GetApiTokensResponse, WarpgateError> {
|
||||
let db = services.db.lock().await;
|
||||
|
||||
let Some(user_model) = get_user(*auth, &*db).await? else {
|
||||
return Ok(GetApiTokensResponse::Unauthorized);
|
||||
};
|
||||
|
||||
let api_tokens = user_model.find_related(ApiToken::Entity).all(&*db).await?;
|
||||
|
||||
Ok(GetApiTokensResponse::Ok(Json(
|
||||
api_tokens.into_iter().map(Into::into).collect(),
|
||||
)))
|
||||
}
|
||||
|
||||
#[oai(
|
||||
path = "/profile/api-tokens",
|
||||
method = "post",
|
||||
operation_id = "create_api_token",
|
||||
transform = "endpoint_auth"
|
||||
)]
|
||||
async fn api_create_api_token(
|
||||
&self,
|
||||
auth: Data<&RequestAuthorization>,
|
||||
services: Data<&Services>,
|
||||
body: Json<NewApiToken>,
|
||||
) -> Result<CreateApiTokenResponse, WarpgateError> {
|
||||
let db = services.db.lock().await;
|
||||
|
||||
let Some(user_model) = get_user(&*auth, &*db).await? else {
|
||||
return Ok(CreateApiTokenResponse::Unauthorized);
|
||||
};
|
||||
|
||||
let secret = generate_ticket_secret();
|
||||
let object = ApiToken::ActiveModel {
|
||||
id: Set(Uuid::new_v4()),
|
||||
user_id: Set(user_model.id),
|
||||
created: Set(Utc::now()),
|
||||
expiry: Set(body.expiry),
|
||||
label: Set(body.label.clone()),
|
||||
secret: Set(secret.expose_secret().to_string()),
|
||||
}
|
||||
.insert(&*db)
|
||||
.await
|
||||
.map_err(WarpgateError::from)?;
|
||||
|
||||
Ok(CreateApiTokenResponse::Created(Json(TokenAndSecret {
|
||||
token: object.into(),
|
||||
secret: secret.expose_secret().to_string(),
|
||||
})))
|
||||
}
|
||||
|
||||
#[oai(
|
||||
path = "/profile/api-tokens/:id",
|
||||
method = "delete",
|
||||
operation_id = "delete_my_api_token",
|
||||
transform = "endpoint_auth"
|
||||
)]
|
||||
async fn api_delete_api_token(
|
||||
&self,
|
||||
auth: Data<&RequestAuthorization>,
|
||||
services: Data<&Services>,
|
||||
id: Path<Uuid>,
|
||||
) -> Result<DeleteApiTokenResponse, WarpgateError> {
|
||||
let db = services.db.lock().await;
|
||||
|
||||
let Some(user_model) = get_user(&*auth, &*db).await? else {
|
||||
return Ok(DeleteApiTokenResponse::Unauthorized);
|
||||
};
|
||||
|
||||
let Some(model) = user_model
|
||||
.find_related(ApiToken::Entity)
|
||||
.filter(ApiToken::Column::Id.eq(id.0))
|
||||
.one(&*db)
|
||||
.await?
|
||||
else {
|
||||
return Ok(DeleteApiTokenResponse::NotFound);
|
||||
};
|
||||
|
||||
model.delete(&*db).await?;
|
||||
Ok(DeleteApiTokenResponse::Deleted)
|
||||
}
|
||||
}
|
|
@ -1,6 +1,10 @@
|
|||
use poem::session::Session;
|
||||
use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter};
|
||||
use tracing::info;
|
||||
use warpgate_common::WarpgateError;
|
||||
use warpgate_db_entities as entities;
|
||||
|
||||
use crate::common::RequestAuthorization;
|
||||
use crate::session::SessionStore;
|
||||
|
||||
pub fn logout(session: &Session, session_middleware: &mut SessionStore) {
|
||||
|
@ -8,3 +12,22 @@ pub fn logout(session: &Session, session_middleware: &mut SessionStore) {
|
|||
session.clear();
|
||||
info!("Logged out");
|
||||
}
|
||||
|
||||
pub async fn get_user(
|
||||
auth: &RequestAuthorization,
|
||||
db: &DatabaseConnection,
|
||||
) -> Result<Option<entities::User::Model>, WarpgateError> {
|
||||
let Some(username) = auth.username() else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let Some(user_model) = entities::User::Entity::find()
|
||||
.filter(entities::User::Column::Username.eq(username))
|
||||
.one(db)
|
||||
.await?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
Ok(Some(user_model))
|
||||
}
|
||||
|
|
|
@ -5,14 +5,13 @@ use poem::{Endpoint, EndpointExt, FromRequest, IntoResponse};
|
|||
use poem_openapi::param::Path;
|
||||
use poem_openapi::payload::Json;
|
||||
use poem_openapi::{ApiResponse, Enum, Object, OpenApi};
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, ModelTrait, QueryFilter, Set,
|
||||
};
|
||||
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, ModelTrait, QueryFilter, Set};
|
||||
use uuid::Uuid;
|
||||
use warpgate_common::{User, UserPasswordCredential, UserRequireCredentialsPolicy, WarpgateError};
|
||||
use warpgate_core::Services;
|
||||
use warpgate_db_entities::{self as entities, Parameters, PasswordCredential, PublicKeyCredential};
|
||||
|
||||
use super::common::get_user;
|
||||
use crate::common::{endpoint_auth, RequestAuthorization};
|
||||
|
||||
pub struct Api;
|
||||
|
@ -168,25 +167,6 @@ pub fn parameters_based_auth<E: Endpoint + 'static>(e: E) -> impl Endpoint {
|
|||
})
|
||||
}
|
||||
|
||||
async fn get_user(
|
||||
auth: &RequestAuthorization,
|
||||
db: &DatabaseConnection,
|
||||
) -> Result<Option<entities::User::Model>, WarpgateError> {
|
||||
let Some(username) = auth.username() else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let Some(user_model) = entities::User::Entity::find()
|
||||
.filter(entities::User::Column::Username.eq(username))
|
||||
.one(db)
|
||||
.await?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
Ok(Some(user_model))
|
||||
}
|
||||
|
||||
#[OpenApi]
|
||||
impl Api {
|
||||
#[oai(
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use poem_openapi::auth::ApiKey;
|
||||
use poem_openapi::{OpenApi, SecurityScheme};
|
||||
|
||||
mod api_tokens;
|
||||
pub mod auth;
|
||||
mod common;
|
||||
mod credentials;
|
||||
|
@ -33,5 +34,6 @@ pub fn get() -> impl OpenApi {
|
|||
sso_provider_list::Api,
|
||||
sso_provider_detail::Api,
|
||||
credentials::Api,
|
||||
api_tokens::Api,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -106,7 +106,9 @@ async fn get_target_for_request(
|
|||
});
|
||||
username
|
||||
}
|
||||
RequestAuthorization::AdminToken => return Ok(None),
|
||||
RequestAuthorization::UserToken { .. } | RequestAuthorization::AdminToken => {
|
||||
return Ok(None)
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(target_name) = selected_target_name {
|
||||
|
|
|
@ -115,6 +115,7 @@ impl SessionAuthorization {
|
|||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub enum RequestAuthorization {
|
||||
Session(SessionAuthorization),
|
||||
UserToken { username: String },
|
||||
AdminToken,
|
||||
}
|
||||
|
||||
|
@ -122,6 +123,7 @@ impl RequestAuthorization {
|
|||
pub fn username(&self) -> Option<&String> {
|
||||
match self {
|
||||
Self::Session(auth) => Some(auth.username()),
|
||||
Self::UserToken { username } => Some(username),
|
||||
Self::AdminToken => None,
|
||||
}
|
||||
}
|
||||
|
@ -133,6 +135,7 @@ async fn is_user_admin(req: &Request, auth: &RequestAuthorization) -> poem::Resu
|
|||
let username = match auth {
|
||||
RequestAuthorization::Session(SessionAuthorization::User(username)) => username,
|
||||
RequestAuthorization::Session(SessionAuthorization::Ticket { .. }) => return Ok(false),
|
||||
RequestAuthorization::UserToken { username } => username,
|
||||
RequestAuthorization::AdminToken => return Ok(true),
|
||||
};
|
||||
|
||||
|
@ -184,13 +187,21 @@ pub async fn _inner_auth<E: Endpoint + 'static>(
|
|||
Some(auth) => RequestAuthorization::Session(auth),
|
||||
None => match req.headers().get(&X_WARPGATE_TOKEN) {
|
||||
Some(token_from_header) => {
|
||||
if Some(
|
||||
token_from_header
|
||||
.to_str()
|
||||
.map_err(poem::error::BadRequest)?,
|
||||
) == services.admin_token.lock().await.as_deref()
|
||||
{
|
||||
let token_from_header = token_from_header
|
||||
.to_str()
|
||||
.map_err(poem::error::BadRequest)?;
|
||||
if Some(token_from_header) == services.admin_token.lock().await.as_deref() {
|
||||
RequestAuthorization::AdminToken
|
||||
} else if let Some(user) = services
|
||||
.config_provider
|
||||
.lock()
|
||||
.await
|
||||
.validate_api_token(token_from_header)
|
||||
.await?
|
||||
{
|
||||
RequestAuthorization::UserToken {
|
||||
username: user.username,
|
||||
}
|
||||
} else {
|
||||
return Ok(None);
|
||||
}
|
||||
|
|
|
@ -63,7 +63,7 @@ impl ProtocolServer for HTTPProtocolServer {
|
|||
let admin_api_app = admin_api_app(&self.services).into_endpoint();
|
||||
let api_service = OpenApiService::new(
|
||||
crate::api::get(),
|
||||
"Warpgate HTTP proxy",
|
||||
"Warpgate user API",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
)
|
||||
.server("/@warpgate/api");
|
||||
|
|
|
@ -89,7 +89,7 @@
|
|||
</div>
|
||||
{/snippet}
|
||||
|
||||
{#snippet item({ item: session })}
|
||||
{#snippet item(session)}
|
||||
<a
|
||||
|
||||
class="list-group-item list-group-item-action"
|
||||
|
|
|
@ -15,8 +15,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
|
||||
<div class="page-summary-bar mt-4">
|
||||
<div class="page-summary-bar">
|
||||
<h1>roles</h1>
|
||||
<a
|
||||
class="btn btn-primary ms-auto"
|
||||
|
@ -27,7 +26,7 @@
|
|||
</div>
|
||||
|
||||
<ItemList load={getRoles} showSearch={true}>
|
||||
{#snippet item({ item: role })}
|
||||
{#snippet item(role)}
|
||||
<a
|
||||
class="list-group-item list-group-item-action"
|
||||
href="/roles/{role.id}"
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
import ItemList, { type LoadOptions, type PaginatedResponse } from 'common/ItemList.svelte'
|
||||
import { link } from 'svelte-spa-router'
|
||||
import { TargetKind } from 'gateway/lib/api'
|
||||
import EmptyState from 'common/EmptyState.svelte'
|
||||
|
||||
function getTargets (options: LoadOptions): Observable<PaginatedResponse<Target>> {
|
||||
return from(api.getTargets({
|
||||
|
@ -27,7 +28,13 @@
|
|||
</div>
|
||||
|
||||
<ItemList load={getTargets} showSearch={true}>
|
||||
{#snippet item({ item: target })}
|
||||
{#snippet empty()}
|
||||
<EmptyState
|
||||
title="No targets yet"
|
||||
hint="Targets are destinations on the internal network that your users will connect to"
|
||||
/>
|
||||
{/snippet}
|
||||
{#snippet item(target)}
|
||||
<a
|
||||
class="list-group-item list-group-item-action"
|
||||
class:disabled={target.options.kind === TargetKind.WebAdmin}
|
||||
|
|
|
@ -1,40 +1,40 @@
|
|||
<script lang="ts">
|
||||
import { api, type Ticket } from 'admin/lib/api'
|
||||
import { link } from 'svelte-spa-router'
|
||||
import RelativeDate from '../RelativeDate.svelte'
|
||||
import Fa from 'svelte-fa'
|
||||
import { faCalendarXmark, faCalendarCheck, faSquareXmark, faSquareCheck } from '@fortawesome/free-solid-svg-icons'
|
||||
import { stringifyError } from 'common/errors'
|
||||
import Alert from 'common/sveltestrap-s5-ports/Alert.svelte'
|
||||
import { api, type Ticket } from 'admin/lib/api'
|
||||
import { link } from 'svelte-spa-router'
|
||||
import RelativeDate from '../RelativeDate.svelte'
|
||||
import Fa from 'svelte-fa'
|
||||
import { faCalendarXmark, faCalendarCheck, faSquareXmark, faSquareCheck } from '@fortawesome/free-solid-svg-icons'
|
||||
import { stringifyError } from 'common/errors'
|
||||
import Alert from 'common/sveltestrap-s5-ports/Alert.svelte'
|
||||
import EmptyState from 'common/EmptyState.svelte'
|
||||
|
||||
let error: string|undefined = $state()
|
||||
let tickets: Ticket[]|undefined = $state()
|
||||
let error: string|undefined = $state()
|
||||
let tickets: Ticket[]|undefined = $state()
|
||||
|
||||
async function load () {
|
||||
tickets = await api.getTickets()
|
||||
}
|
||||
async function load () {
|
||||
tickets = await api.getTickets()
|
||||
}
|
||||
|
||||
load().catch(async e => {
|
||||
error = await stringifyError(e)
|
||||
})
|
||||
|
||||
async function deleteTicket (ticket: Ticket) {
|
||||
await api.deleteTicket(ticket)
|
||||
load()
|
||||
}
|
||||
load().catch(async e => {
|
||||
error = await stringifyError(e)
|
||||
})
|
||||
|
||||
async function deleteTicket (ticket: Ticket) {
|
||||
await api.deleteTicket(ticket)
|
||||
load()
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if error}
|
||||
<Alert color="danger">{error}</Alert>
|
||||
{/if}
|
||||
|
||||
{#if tickets }
|
||||
{#if tickets}
|
||||
<div class="page-summary-bar">
|
||||
{#if tickets.length }
|
||||
<h1>access tickets: <span class="counter">{tickets.length}</span></h1>
|
||||
{:else}
|
||||
<h1>No tickets created yet</h1>
|
||||
<h1>access tickets</h1>
|
||||
{/if}
|
||||
<a
|
||||
class="btn btn-primary ms-auto"
|
||||
|
@ -44,7 +44,7 @@ async function deleteTicket (ticket: Ticket) {
|
|||
</a>
|
||||
</div>
|
||||
|
||||
{#if tickets.length }
|
||||
{#if tickets.length}
|
||||
<div class="list-group list-group-flush">
|
||||
{#each tickets as ticket}
|
||||
<div class="list-group-item">
|
||||
|
@ -79,9 +79,10 @@ async function deleteTicket (ticket: Ticket) {
|
|||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<Alert color="info" fade={false}>
|
||||
Tickets are secret keys that allow access to one specific target without any additional authentication.
|
||||
</Alert>
|
||||
<EmptyState
|
||||
title="No tickets yet"
|
||||
hint="Tickets are secret keys that allow access to one specific target without any additional authentication"
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
</div>
|
||||
|
||||
<ItemList load={getUsers} showSearch={true}>
|
||||
{#snippet item({ item: user })}
|
||||
{#snippet item(user)}
|
||||
<a
|
||||
class="list-group-item list-group-item-action"
|
||||
href="/users/{user.id}"
|
||||
|
|
33
warpgate-web/src/common/EmptyState.svelte
Normal file
33
warpgate-web/src/common/EmptyState.svelte
Normal file
|
@ -0,0 +1,33 @@
|
|||
<script lang="ts">
|
||||
export let title: string
|
||||
export let hint = ''
|
||||
</script>
|
||||
|
||||
<div class="empty-state">
|
||||
<h2>{title}</h2>
|
||||
{#if hint}
|
||||
<p class="text-muted">{hint}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 50%;
|
||||
min-width: 200px;
|
||||
text-align: center;
|
||||
margin: 4rem auto;
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-family: 'Poppins';
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: .9rem;
|
||||
}
|
||||
</style>
|
|
@ -18,16 +18,18 @@
|
|||
import { observe } from 'svelte-observable'
|
||||
import { Input } from '@sveltestrap/sveltestrap'
|
||||
import DelayedSpinner from './DelayedSpinner.svelte'
|
||||
import { onDestroy } from 'svelte'
|
||||
import { onDestroy, type Snippet } from 'svelte'
|
||||
import EmptyState from './EmptyState.svelte'
|
||||
|
||||
interface Props {
|
||||
page?: number
|
||||
pageSize?: number|undefined
|
||||
load: (_: LoadOptions) => Observable<PaginatedResponse<T>>
|
||||
showSearch?: boolean
|
||||
header?: import('svelte').Snippet<[any]>
|
||||
item?: import('svelte').Snippet<[any]>
|
||||
footer?: import('svelte').Snippet<[any]>
|
||||
header?: Snippet<[]>
|
||||
item?: Snippet<[T]>
|
||||
footer?: Snippet<[T[]]>
|
||||
empty?: Snippet<[]>
|
||||
}
|
||||
|
||||
let {
|
||||
|
@ -38,6 +40,7 @@
|
|||
header,
|
||||
item,
|
||||
footer,
|
||||
empty,
|
||||
}: Props = $props()
|
||||
|
||||
let filter = $state('')
|
||||
|
@ -95,7 +98,7 @@
|
|||
{#if showSearch}
|
||||
<Input bind:value={filter} placeholder="Search..." class="flex-grow-1 border-0" />
|
||||
{/if}
|
||||
{@render header?.({ items })}
|
||||
{@render header?.()}
|
||||
</div>
|
||||
{#await $items}
|
||||
<DelayedSpinner />
|
||||
|
@ -103,18 +106,20 @@
|
|||
{#if _items}
|
||||
<div class="list-group list-group-flush mb-3">
|
||||
{#each _items as _item}
|
||||
{@render item?.({ item: _item })}
|
||||
{@render item?.(_item)}
|
||||
{/each}
|
||||
</div>
|
||||
{@render footer?.({ items: _items })}
|
||||
{@render footer?.(_items)}
|
||||
{:else}
|
||||
<DelayedSpinner />
|
||||
{/if}
|
||||
|
||||
{#if filter && loaded && !_items?.length}
|
||||
<em>
|
||||
Nothing found
|
||||
</em>
|
||||
{#if loaded && !_items?.length}
|
||||
{#if filter}
|
||||
<EmptyState title="Nothing found" />
|
||||
{:else}
|
||||
{@render empty?.()}
|
||||
{/if}
|
||||
{/if}
|
||||
{/await}
|
||||
|
||||
|
|
|
@ -4,13 +4,13 @@
|
|||
import DelayedSpinner from './DelayedSpinner.svelte'
|
||||
import { stringifyError } from './errors'
|
||||
|
||||
let { promise, children }: {
|
||||
let { promise, children, data = $bindable() }: {
|
||||
promise: Promise<T>
|
||||
data?: T,
|
||||
children: Snippet<[T]>
|
||||
} = $props()
|
||||
|
||||
let loaded = $state(false)
|
||||
let data: T | undefined = $state()
|
||||
let error: string | undefined = $state()
|
||||
|
||||
$effect(() => {
|
||||
|
|
88
warpgate-web/src/gateway/ApiTokenManager.svelte
Normal file
88
warpgate-web/src/gateway/ApiTokenManager.svelte
Normal file
|
@ -0,0 +1,88 @@
|
|||
<script lang="ts">
|
||||
import { api, type ExistingApiToken } from 'gateway/lib/api'
|
||||
import Loadable from 'common/Loadable.svelte'
|
||||
import { faKey } from '@fortawesome/free-solid-svg-icons'
|
||||
import Fa from 'svelte-fa'
|
||||
import CreateApiTokenModal from './CreateApiTokenModal.svelte'
|
||||
import Alert from 'common/sveltestrap-s5-ports/Alert.svelte'
|
||||
import CopyButton from 'common/CopyButton.svelte'
|
||||
import Badge from 'common/sveltestrap-s5-ports/Badge.svelte'
|
||||
import EmptyState from 'common/EmptyState.svelte'
|
||||
|
||||
let tokens: ExistingApiToken[] = $state([])
|
||||
let creatingToken = $state(false)
|
||||
let lastCreatedSecret: string | undefined = $state()
|
||||
const now = Date.now()
|
||||
|
||||
async function deleteToken (token: ExistingApiToken) {
|
||||
tokens = tokens.filter(c => c.id !== token.id)
|
||||
await api.deleteMyApiToken(token)
|
||||
lastCreatedSecret = undefined
|
||||
}
|
||||
|
||||
async function createToken (label: string, expiry: Date) {
|
||||
const { secret, token } = await api.createApiToken({ newApiToken : { label, expiry } })
|
||||
lastCreatedSecret = secret
|
||||
tokens = [...tokens, token]
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="page-summary-bar mt-4">
|
||||
<h1>API tokens</h1>
|
||||
<a href={''} class="btn btn-primary ms-auto" onclick={e => {
|
||||
creatingToken = true
|
||||
e.preventDefault()
|
||||
}}>Create token</a>
|
||||
</div>
|
||||
|
||||
{#if lastCreatedSecret}
|
||||
<Alert color="info">
|
||||
<div>Your token - shown only once:</div>
|
||||
<div class="d-flex align-items-center mt-2">
|
||||
<code style="min-width: 0">{lastCreatedSecret}</code>
|
||||
<CopyButton class="ms-auto" text={lastCreatedSecret} />
|
||||
</div>
|
||||
</Alert>
|
||||
{/if}
|
||||
|
||||
<Loadable promise={api.getMyApiTokens()} bind:data={tokens}>
|
||||
{#if tokens.length === 0}
|
||||
<EmptyState
|
||||
title="No tokens yet"
|
||||
hint="Tokens let you manage Warpgate programmatically via its API"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<div class="list-group list-group-flush mb-3">
|
||||
{#each tokens as token}
|
||||
<div class="list-group-item d-flex align-items-center">
|
||||
<Fa fw icon={faKey} />
|
||||
<span class="label ms-3">{token.label}</span>
|
||||
{#if token.expiry.getTime() < now}
|
||||
<Badge color="danger" class="ms-2">Expired</Badge>
|
||||
{:else}
|
||||
<Badge color="success" class="ms-2">{token.expiry.toLocaleDateString()}</Badge>
|
||||
{/if}
|
||||
<span class="ms-auto"></span>
|
||||
<a
|
||||
color="link"
|
||||
href={''}
|
||||
class="ms-2"
|
||||
onclick={e => {
|
||||
deleteToken(token)
|
||||
e.preventDefault()
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</a>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</Loadable>
|
||||
|
||||
{#if creatingToken}
|
||||
<CreateApiTokenModal
|
||||
bind:isOpen={creatingToken}
|
||||
create={createToken}
|
||||
/>
|
||||
{/if}
|
|
@ -46,6 +46,14 @@
|
|||
asyncComponent: () => import('./Profile.svelte') as any,
|
||||
conditions: [requireLogin],
|
||||
}),
|
||||
'/profile/api-tokens': wrap({
|
||||
asyncComponent: () => import('./ProfileApiTokens.svelte') as any,
|
||||
conditions: [requireLogin],
|
||||
}),
|
||||
'/profile/credentials': wrap({
|
||||
asyncComponent: () => import('./ProfileCredentials.svelte') as any,
|
||||
conditions: [requireLogin],
|
||||
}),
|
||||
'/login': wrap({
|
||||
asyncComponent: () => import('./Login.svelte') as any,
|
||||
}),
|
||||
|
|
77
warpgate-web/src/gateway/CreateApiTokenModal.svelte
Normal file
77
warpgate-web/src/gateway/CreateApiTokenModal.svelte
Normal file
|
@ -0,0 +1,77 @@
|
|||
<script lang="ts">
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
FormGroup,
|
||||
Input,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
} from '@sveltestrap/sveltestrap'
|
||||
|
||||
import ModalHeader from 'common/sveltestrap-s5-ports/ModalHeader.svelte'
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean
|
||||
create: (label: string, expiry: Date) => void
|
||||
}
|
||||
|
||||
let {
|
||||
isOpen = $bindable(true),
|
||||
create,
|
||||
}: Props = $props()
|
||||
let label = $state('')
|
||||
let expiry = $state(new Date(Date.now() + 1000 * 60 * 60 * 24 * 7).toISOString())
|
||||
let field: HTMLInputElement|undefined = $state()
|
||||
let validated = $state(false)
|
||||
|
||||
function _save () {
|
||||
create(label, new Date(expiry))
|
||||
_cancel()
|
||||
}
|
||||
|
||||
function _cancel () {
|
||||
isOpen = false
|
||||
label = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal toggle={_cancel} isOpen={isOpen} on:open={() => field?.focus()}>
|
||||
<Form {validated} on:submit={e => {
|
||||
_save()
|
||||
e.preventDefault()
|
||||
}}>
|
||||
<ModalHeader toggle={_cancel}>
|
||||
New API token
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<FormGroup floating label="Descriptive label">
|
||||
<Input
|
||||
bind:inner={field}
|
||||
required
|
||||
bind:value={label} />
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup floating label="Expiry">
|
||||
<Input
|
||||
type="datetime-local"
|
||||
bind:value={expiry} />
|
||||
</FormGroup>
|
||||
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<div class="d-flex">
|
||||
<Button
|
||||
class="ms-auto"
|
||||
on:click={() => validated = true}
|
||||
>Create</Button>
|
||||
|
||||
<Button
|
||||
class="ms-2"
|
||||
color="danger"
|
||||
on:click={_cancel}
|
||||
>Cancel</Button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</Form>
|
||||
</Modal>
|
|
@ -66,15 +66,15 @@
|
|||
<div class="list-group list-group-flush mb-3">
|
||||
<div class="list-group-item credential">
|
||||
{#if creds.password === PasswordState.Unset}
|
||||
<span class="label">Your account has no password set</span>
|
||||
<span class="label ms-3">Your account has no password set</span>
|
||||
{/if}
|
||||
{#if creds.password === PasswordState.Set}
|
||||
<Fa fw icon={faKeyboard} />
|
||||
<span class="label">Password set</span>
|
||||
<span class="label ms-3">Password set</span>
|
||||
{/if}
|
||||
{#if creds.password === PasswordState.MultipleSet}
|
||||
<Fa fw icon={faKeyboard} />
|
||||
<span class="label">Multiple passwords set</span>
|
||||
<span class="label ms-3">Multiple passwords set</span>
|
||||
{/if}
|
||||
|
||||
<span class="ms-auto"></span>
|
||||
|
@ -116,7 +116,7 @@
|
|||
{#each creds.otp as credential}
|
||||
<div class="list-group-item credential">
|
||||
<Fa fw icon={faMobilePhone} />
|
||||
<span class="label">OTP device</span>
|
||||
<span class="label ms-3">OTP device</span>
|
||||
<span class="ms-auto"></span>
|
||||
<a
|
||||
class="ms-2"
|
||||
|
@ -150,7 +150,7 @@
|
|||
{#each creds.publicKeys as credential}
|
||||
<div class="list-group-item credential">
|
||||
<Fa fw icon={faKey} />
|
||||
<div class="main">
|
||||
<div class="main ms-3">
|
||||
<div class="label">{credential.label}</div>
|
||||
<small class="d-block text-muted">{credential.abbreviated}</small>
|
||||
</div>
|
||||
|
@ -185,7 +185,7 @@
|
|||
{#each creds.sso as credential}
|
||||
<div class="list-group-item credential">
|
||||
<Fa fw icon={faIdBadge} />
|
||||
<span class="label">
|
||||
<span class="label ms-3">
|
||||
{credential.email}
|
||||
{#if credential.provider} ({credential.provider}){/if}
|
||||
</span>
|
||||
|
@ -222,9 +222,5 @@
|
|||
.credential {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.label:not(:first-child), .main {
|
||||
margin-left: .75rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,19 +1,24 @@
|
|||
<script lang="ts">
|
||||
import { serverInfo } from 'gateway/lib/store'
|
||||
import Alert from 'common/sveltestrap-s5-ports/Alert.svelte'
|
||||
import CredentialManager from './CredentialManager.svelte'
|
||||
import NavListItem from 'common/NavListItem.svelte'
|
||||
</script>
|
||||
|
||||
<div class="page-summary-bar">
|
||||
<h1>{$serverInfo!.username}</h1>
|
||||
</div>
|
||||
|
||||
<NavListItem
|
||||
title="API tokens"
|
||||
description="Manage your API tokens"
|
||||
href="/profile/api-tokens"
|
||||
/>
|
||||
|
||||
{#if $serverInfo}
|
||||
{#if $serverInfo.ownCredentialManagementAllowed}
|
||||
<CredentialManager />
|
||||
{:else}
|
||||
<Alert color="info">
|
||||
Credential management is disabled by your administrator
|
||||
</Alert>
|
||||
<NavListItem
|
||||
title="Credentials"
|
||||
description="Manage your passwords and keys"
|
||||
href="/profile/credentials"
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
|
|
48
warpgate-web/src/gateway/ProfileApiTokens.svelte
Normal file
48
warpgate-web/src/gateway/ProfileApiTokens.svelte
Normal file
|
@ -0,0 +1,48 @@
|
|||
<script lang="ts">
|
||||
import { faFileContract, faFlaskVial } from '@fortawesome/free-solid-svg-icons'
|
||||
import Fa from 'svelte-fa'
|
||||
import ApiTokenManager from './ApiTokenManager.svelte'
|
||||
</script>
|
||||
|
||||
<!-- <div class="page-summary-bar">
|
||||
<h1>API tokens</h1>
|
||||
</div> -->
|
||||
|
||||
<ApiTokenManager />
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h4>User API</h4>
|
||||
<a class="link" target="_blank" href="/@warpgate/api/swagger">
|
||||
<Fa icon={faFlaskVial} fw />
|
||||
<span>Playground</span>
|
||||
</a>
|
||||
<a class="link" target="_blank" href="/@warpgate/api/openapi.json">
|
||||
<Fa icon={faFileContract} fw />
|
||||
<span>Schema</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<h4>Admin API</h4>
|
||||
<a class="link" target="_blank" href="/@warpgate/admin/api/swagger">
|
||||
<Fa icon={faFlaskVial} fw />
|
||||
<span>Playground</span>
|
||||
</a>
|
||||
<a class="link" target="_blank" href="/@warpgate/admin/api/openapi.json">
|
||||
<Fa icon={faFileContract} fw />
|
||||
<span>Schema</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
}
|
||||
</style>
|
19
warpgate-web/src/gateway/ProfileCredentials.svelte
Normal file
19
warpgate-web/src/gateway/ProfileCredentials.svelte
Normal file
|
@ -0,0 +1,19 @@
|
|||
<script lang="ts">
|
||||
import { serverInfo } from 'gateway/lib/store'
|
||||
import Alert from 'common/sveltestrap-s5-ports/Alert.svelte'
|
||||
import CredentialManager from './CredentialManager.svelte'
|
||||
</script>
|
||||
|
||||
<div class="page-summary-bar">
|
||||
<h1>Credentials</h1>
|
||||
</div>
|
||||
|
||||
{#if $serverInfo}
|
||||
{#if $serverInfo.ownCredentialManagementAllowed}
|
||||
<CredentialManager />
|
||||
{:else}
|
||||
<Alert color="info">
|
||||
Credential management is disabled by your administrator
|
||||
</Alert>
|
||||
{/if}
|
||||
{/if}
|
|
@ -45,7 +45,7 @@ function loadURL (url: string) {
|
|||
</script>
|
||||
|
||||
<ItemList load={loadTargets} showSearch={true}>
|
||||
{#snippet item({ item: target })}
|
||||
{#snippet item(target)}
|
||||
<a
|
||||
class="list-group-item list-group-item-action target-item"
|
||||
href={
|
||||
|
|
|
@ -571,6 +571,86 @@
|
|||
},
|
||||
"operationId": "delete_my_otp"
|
||||
}
|
||||
},
|
||||
"/profile/api-tokens": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json; charset=utf-8": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ExistingApiToken"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"operationId": "get_my_api_tokens"
|
||||
},
|
||||
"post": {
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json; charset=utf-8": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/NewApiToken"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json; charset=utf-8": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/TokenAndSecret"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"operationId": "create_api_token"
|
||||
}
|
||||
},
|
||||
"/profile/api-tokens/{id}": {
|
||||
"delete": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"deprecated": false,
|
||||
"explode": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
},
|
||||
"401": {
|
||||
"description": ""
|
||||
},
|
||||
"404": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"operationId": "delete_my_api_token"
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
|
@ -672,6 +752,32 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"ExistingApiToken": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"label",
|
||||
"created",
|
||||
"expiry"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
"created": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"expiry": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ExistingOtpCredential": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
@ -793,6 +899,22 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"NewApiToken": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"label",
|
||||
"expiry"
|
||||
],
|
||||
"properties": {
|
||||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
"expiry": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
}
|
||||
},
|
||||
"NewOtpCredential": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
@ -941,6 +1063,21 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"TokenAndSecret": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"token",
|
||||
"secret"
|
||||
],
|
||||
"properties": {
|
||||
"token": {
|
||||
"$ref": "#/components/schemas/ExistingApiToken"
|
||||
},
|
||||
"secret": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"UserRequireCredentialsPolicy": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
Loading…
Add table
Reference in a new issue