This commit is contained in:
Eugene Pankov 2022-05-30 12:55:09 +02:00
parent d90aa7d2b0
commit 892e0173ac
No known key found for this signature in database
GPG key ID: 5896FCBBDD1CF4F4
27 changed files with 88 additions and 78 deletions

1
Cargo.lock generated
View file

@ -4128,6 +4128,7 @@ dependencies = [
name = "warpgate-db-migrations"
version = "0.1.0"
dependencies = [
"async-std",
"chrono",
"sea-orm",
"sea-orm-migration",

View file

@ -18,6 +18,9 @@ yarn *ARGS:
migrate *ARGS:
cargo run -p warpgate-db-migrations -- {{ARGS}}
lint:
cd warpgate-admin/app/ && yarn run lint
svelte-check:
cd warpgate-admin/app/ && yarn run check
@ -27,4 +30,4 @@ openapi-all:
openapi:
cd warpgate-admin/app/ && yarn openapi-client
cleanup: (fix "--allow-dirty") (clippy "--fix" "--allow-dirty") fmt svelte-check
cleanup: (fix "--allow-dirty") (clippy "--fix" "--allow-dirty") fmt svelte-check lint

View file

@ -53,6 +53,7 @@ rules:
'@typescript-eslint/no-use-before-define':
- error
- classes: false
functions: false
no-duplicate-imports: error
array-bracket-spacing:
- error
@ -130,12 +131,22 @@ rules:
- allowAliases: in-unions-and-intersections
allowLiterals: always
allowCallbacks: always
'@typescript-eslint/comma-dangle':
- error
- arrays: always-multiline
objects: always-multiline
imports: always-multiline
exports: always-multiline
functions: only-multiline
overrides:
- files: '*.svelte'
processor: svelte3/svelte3
rules:
# To allow prop definitions
'@typescript-eslint/init-declarations': off
# False positives for {#if}
'@typescript-eslint/no-unnecessary-condition': off
ignorePatterns:
- svelte.config.js

View file

@ -12,7 +12,7 @@
"lint": "eslint src && svelte-check",
"postinstall": "yarn run openapi-client",
"openapi-schema": "curl -k https://localhost:8888/api/openapi.json > openapi-schema.json",
"openapi-client": "openapi-generator-cli generate -g typescript-fetch -i openapi-schema.json -o api-client -p npmName=warpgate-api-client -p useSingleRequestParameter=true && cd api-client && npm i && npm run build",
"openapi-client": "openapi-generator-cli generate -g typescript-fetch -i openapi-schema.json -o api-client -p npmName=warpgate-api-client -p useSingleRequestParameter=true && cd api-client && npm i && yarn tsc --target esnext --module esnext && rm -rf src",
"openapi": "yarn run openapi-schema && yarn run openapi-client"
},
"devDependencies": {

View file

@ -2,7 +2,7 @@
import { faSignOut } from '@fortawesome/free-solid-svg-icons'
import { api } from 'lib/api'
import { authenticatedUsername } from 'lib/store'
import Fa from 'svelte-fa'
import { Fa } from 'svelte-fa'
import logo from '../public/assets/logo.svg'
@ -31,31 +31,31 @@ init()
const routes = {
'/': wrap({
asyncComponent: () => import('./Home.svelte')
asyncComponent: () => import('./Home.svelte'),
}),
'/login': wrap({
asyncComponent: () => import('./Login.svelte')
asyncComponent: () => import('./Login.svelte'),
}),
'/sessions/:id': wrap({
asyncComponent: () => import('./Session.svelte')
asyncComponent: () => import('./Session.svelte'),
}),
'/recordings/:id': wrap({
asyncComponent: () => import('./Recording.svelte')
asyncComponent: () => import('./Recording.svelte'),
}),
'/tickets': wrap({
asyncComponent: () => import('./Tickets.svelte')
asyncComponent: () => import('./Tickets.svelte'),
}),
'/tickets/create': wrap({
asyncComponent: () => import('./CreateTicket.svelte')
asyncComponent: () => import('./CreateTicket.svelte'),
}),
'/targets': wrap({
asyncComponent: () => import('./Targets.svelte')
asyncComponent: () => import('./Targets.svelte'),
}),
'/ssh': wrap({
asyncComponent: () => import('./SSH.svelte')
asyncComponent: () => import('./SSH.svelte'),
}),
'/log': wrap({
asyncComponent: () => import('./Log.svelte')
asyncComponent: () => import('./Log.svelte'),
}),
}
</script>

View file

@ -33,7 +33,7 @@ async function create () {
createTicketRequest: {
username: selectedUser.username,
targetName: selectedTarget.name,
}
},
})
} catch (err) {
error = err
@ -59,11 +59,11 @@ async function create () {
<h3>Connection instructions</h3>
<FormGroup floating label="SSH username">
<input type="text" class="form-control" readonly value={"ticket-" + result.secret} />
<input type="text" class="form-control" readonly value={'ticket-' + result.secret} />
</FormGroup>
<FormGroup floating label="Example command">
<input type="text" class="form-control" readonly value={"ssh ticket-" + result.secret + "@warpgate-host -p warpgate-port"} />
<input type="text" class="form-control" readonly value={'ssh ticket-' + result.secret + '@warpgate-host -p warpgate-port'} />
</FormGroup>
{/if}

View file

@ -1,5 +1,5 @@
<script lang="ts">
import Fa from 'svelte-fa'
import { Fa } from 'svelte-fa'
import { faCircleDot as iconActive } from '@fortawesome/free-regular-svg-icons'
import { Spinner, Button } from 'sveltestrap'
import { onDestroy } from 'svelte'
@ -7,7 +7,7 @@
import { api, SessionSnapshot } from 'lib/api'
import { derived, writable } from 'svelte/store'
import { firstBy } from 'thenby'
import moment from 'moment'
import moment, { duration } from 'moment'
import RelativeDate from 'RelativeDate.svelte'
const sessions = writable<SessionSnapshot[]|null>(null)
@ -73,7 +73,7 @@
<div class="meta">
{#if session.ended }
{moment.duration(moment(session.ended).diff(session.started)).humanize()}
{duration(moment(session.ended).diff(session.started)).humanize()}
{/if}
</div>

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { api, LogEntry } from 'lib/api'
import { Alert, FormGroup } from 'sveltestrap'
import { firstBy } from 'thenby';
import { Alert } from 'sveltestrap'
import { firstBy } from 'thenby'
import IntersectionObserver from 'svelte-intersection-observer'
import { link } from 'svelte-spa-router'
import { onDestroy, onMount } from 'svelte'
@ -59,18 +59,18 @@ async function loadNewer () {
}
}
async function loadOlder (search = false) {
async function loadOlder (searchMode = false) {
loading = true
try {
const newItems = await api.getLogs({
getLogsRequest: {
...filters ?? {},
before: search ? undefined : items?.at(-1)?.timestamp,
before: searchMode ? undefined : items?.at(-1)?.timestamp,
limit: PAGE_SIZE,
search: searchQuery,
},
})
if (search) {
if (searchMode) {
endReached = false
items = []
}
@ -109,7 +109,6 @@ onDestroy(() => {
clearInterval(reloadInterval)
})
</script>
{#if error}

View file

@ -9,13 +9,7 @@ let username = ''
let password = ''
let incorrectCredentials = false
function onInputKey (event: KeyboardEvent) {
if (event.key === 'Enter') {
login()
}
}
async function login (event?) {
async function login (event?: MouseEvent) {
event?.preventDefault()
error = null
incorrectCredentials = false
@ -26,11 +20,11 @@ async function login (event?) {
password,
},
})
} catch (error) {
if (error.status === 401) {
} catch (err) {
if (err.status === 401) {
incorrectCredentials = true
} else {
error = error
error = err
}
return
}
@ -38,6 +32,12 @@ async function login (event?) {
authenticatedUsername.set(info.username!)
replace('/')
}
function onInputKey (event: KeyboardEvent) {
if (event.key === 'Enter') {
login()
}
}
</script>
<form class="mt-5 row" autocomplete="on">

View file

@ -1,8 +1,7 @@
<script lang="ts">
import { api, Recording, RecordingKind } from 'lib/api'
import { api, Recording } from 'lib/api'
import { Alert, Spinner } from 'sveltestrap'
import TerminalRecordingPlayer from 'player/TerminalRecordingPlayer.svelte'
import { onDestroy } from 'svelte';
export let params = { id: '' }
@ -11,7 +10,6 @@ let recording: Recording|null = null
async function load () {
recording = await api.getRecording(params)
}
function getTCPDumpURL () {

View file

@ -1,10 +1,10 @@
<script lang="ts">
import { api, SessionSnapshot, Recording } from 'lib/api'
import { timeAgo } from 'lib/time'
import moment from 'moment'
import { onDestroy } from 'svelte';
import moment, { duration } from 'moment'
import { onDestroy } from 'svelte'
import { link } from 'svelte-spa-router'
import { Alert, Button, FormGroup, Spinner } from 'sveltestrap'
import { Alert, Button, Spinner } from 'sveltestrap'
import LogViewer from 'LogViewer.svelte'
import RelativeDate from 'RelativeDate.svelte'
@ -64,10 +64,10 @@ onDestroy(() => clearInterval(interval))
{getTargetDescription()}
</strong>
<span class="text-muted">
{moment.duration(moment(session.ended).diff(session.started)).humanize()} long, <RelativeDate date={session.started} />
{duration(moment(session.ended).diff(session.started)).humanize()} long, <RelativeDate date={session.started} />
</span>
{:else}
{moment.duration(moment().diff(session.started)).humanize()}
{duration(moment().diff(session.started)).humanize()}
{/if}
</div>
</div>

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { api, Target, UserSnapshot } from 'lib/api'
import { getSSHUsername } from 'lib/ssh';
import { getSSHUsername } from 'lib/ssh'
import { Alert, FormGroup, Modal, ModalBody, ModalHeader } from 'sveltestrap'
let error: Error|undefined
@ -86,7 +86,7 @@ $: sshUsername = getSSHUsername(selectedUser, selectedTarget)
</FormGroup>
<FormGroup floating label="Example command">
<input type="text" class="form-control" readonly value={"ssh " + sshUsername + "@warpgate-host -p warpgate-port"} />
<input type="text" class="form-control" readonly value={'ssh ' + sshUsername + '@warpgate-host -p warpgate-port'} />
</FormGroup>
{/if}
</ModalBody>

View file

@ -2,7 +2,7 @@
import { api, Ticket } from 'lib/api'
import { link } from 'svelte-spa-router'
import { Alert } from 'sveltestrap'
import RelativeDate from 'RelativeDate.svelte';
import RelativeDate from 'RelativeDate.svelte'
let error: Error|undefined
let tickets: Ticket[]|undefined

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

View file

@ -1,8 +1,8 @@
import { DefaultApi, Configuration } from '../../api-client/src'
import { DefaultApi, Configuration } from '../../api-client/dist'
const configuration = new Configuration({
basePath: '/api'
basePath: '/api',
})
export const api = new DefaultApi(configuration)
export * from '../../api-client/src/models'
export * from '../../api-client'

View file

@ -1,5 +1,5 @@
import type { Target, UserSnapshot } from './api'
export function getSSHUsername (user: UserSnapshot|undefined, target: Target|undefined): string {
return `${user?.username ?? "<username>"}:${target?.name}`
return `${user?.username ?? '<username>'}:${target?.name}`
}

View file

@ -1,5 +1,5 @@
import moment from 'moment'
export function timeAgo(t: any): string {
export function timeAgo (t: Date): string {
return moment(t).fromNow()
}

View file

@ -3,7 +3,7 @@ import './theme.scss'
import App from './App.svelte'
const app = new App({
target: document.getElementById('app')!
target: document.getElementById('app')!,
})
export default app

View file

@ -1,5 +1,5 @@
<script lang="ts">
import Fa from 'svelte-fa'
import { Fa } from 'svelte-fa'
import { onDestroy, onMount } from 'svelte'
import { Terminal } from 'xterm'
import { SerializeAddon } from 'xterm-addon-serialize'
@ -16,7 +16,7 @@
let timestamp = 0
let seekInputValue = 0
let duration = 0
let resizeObserver: ResizeObserver
let resizeObserver: ResizeObserver|undefined
let events: (SizeEvent | DataEvent | SnapshotEvent)[] = []
let playing = false
let loading = true
@ -64,14 +64,15 @@
width: number
height: number
}
// eslint-disable-next-line @typescript-eslint/no-type-alias
type AsciiCastData = [number, 'o', string]
type AsciiCastItem = AsciiCastHeader | AsciiCastData
function isAsciiCastHeader(data: AsciiCastItem): data is AsciiCastHeader {
function isAsciiCastHeader (data: AsciiCastItem): data is AsciiCastHeader {
return 'version' in data
}
function isAsciiCastData(data: AsciiCastItem): data is AsciiCastData {
function isAsciiCastData (data: AsciiCastItem): data is AsciiCastData {
return data[1] === 'o'
}
@ -163,7 +164,7 @@
function fitSize () {
metricsCanvas ??= document.createElement('canvas')
const context = metricsCanvas.getContext('2d')!
context.font = '10px ' + term.options.fontFamily ?? 'monospace'
context.font = `10px ${term.options.fontFamily ?? 'monospace'}`
const metrics = context.measureText('abcdef')
const fontWidth = containerElement.clientWidth / term.cols
@ -172,12 +173,12 @@
let seekPromise = Promise.resolve()
async function seek (time) {
async function seek (time: number) {
seekPromise = seekPromise.then(() => _seekInternal(time))
await seekPromise
}
async function _seekInternal (time) {
async function _seekInternal (time: number) {
let nearestSnapshot: SnapshotEvent|null = null
for (const event of events) {
@ -253,7 +254,7 @@
seekInputValue = 100 * time / duration
}
function resize (cols, rows) {
function resize (cols: number, rows: number) {
if (term.cols === cols && term.rows === rows) {
return
}

View file

@ -12,5 +12,12 @@ export default defineConfig({
],
build: {
sourcemap: true,
commonjsOptions: {
include: [
'api-client/dist/*.js',
'**/*.js',
],
transformMixedEsModules: true,
},
},
})

View file

@ -181,10 +181,7 @@ pub async fn api_get_recording_stream(
if let Some(mut receiver) = receiver {
tokio::spawn(async move {
if let Err(error) = async {
loop {
let Ok(data) = receiver.recv().await else {
break;
};
while let Ok(data) = receiver.recv().await {
let content: TerminalRecordingItem = serde_json::from_slice(&data)?;
let cast: AsciiCast = content.into();
let msg = serde_json::to_string(&json!({ "data": cast }))?;

View file

@ -48,7 +48,9 @@ pub fn install_database_logger(database: Arc<Mutex<DatabaseConnection>>) {
fn values_to_log_entry_data(mut values: SerializedRecordValues) -> Option<LogEntry::ActiveModel> {
let session_id = (*values).remove("session");
let username = (*values).remove("session_username");
let message = (*values).remove("message").unwrap_or("".to_string());
let message = (*values)
.remove("message")
.unwrap_or_else(|| "".to_string());
use sea_orm::ActiveValue::Set;
let session_id = session_id.and_then(|x| Uuid::parse_str(&x).ok());

View file

@ -7,7 +7,7 @@ version = "0.1.0"
[dependencies]
chrono = {version = "0.4", features = ["serde"]}
poem-openapi = {version = "^1.3.30", features = ["chrono", "uuid"]}
sea-orm = {version = "^0.8", features = ["macros", "with-chrono", "with-uuid"], default-features = false}
sea-orm = {version = "^0.8", features = ["macros", "with-chrono", "with-uuid", "with-json"], default-features = false}
serde = "1.0"
serde_json = "1.0"
uuid = {version = "0.8", features = ["v4", "serde"]}

View file

@ -27,16 +27,6 @@ pub struct Model {
pub kind: RecordingKind,
}
// #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
// pub enum Relation {
// #[sea_orm(
// belongs_to = "super::Session::Entity",
// from = "Column::SessionId",
// to = "super::Session::Column::Id"
// )]
// Session,
// }
#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {
Session,

View file

@ -8,6 +8,7 @@ version = "0.1.0"
[lib]
[dependencies]
async-std = "^1.11"
chrono = "0.4"
sea-orm = {version = "^0.8", features = ["sqlx-sqlite", "runtime-tokio-native-tls", "macros"], default-features = false}
sea-orm-migration = {version = "^0.8", default-features = false}

View file

@ -1,6 +1,6 @@
use sea_orm::DatabaseConnection;
use sea_orm_migration::MigrationTrait;
use sea_orm_migration::prelude::*;
use sea_orm_migration::MigrationTrait;
mod m00001_create_ticket;
mod m00002_create_session;
@ -24,5 +24,5 @@ impl MigratorTrait for Migrator {
}
pub async fn migrate_database(connection: &DatabaseConnection) -> Result<(), DbErr> {
Migrator::up(&connection, None).await
Migrator::up(connection, None).await
}

View file

@ -1,4 +1,4 @@
use sea_schema::migration::*;
use sea_orm_migration::prelude::*;
use warpgate_db_migrations::Migrator;
#[async_std::main]