8 Commits

Author SHA1 Message Date
jamie cae073728a Merge pull request 'fix: 🐛 branch name' (#11) from v1.0.0 into main
Reviewed-on: #11
2026-05-15 08:02:01 +01:00
jamie 0f35d5bd6f fix: 🐛 branch name
Release / release (pull_request) Successful in 26s
2026-05-15 07:01:38 +00:00
jamie 1d6cce88a8 Merge pull request 'feat: first release' (#10) from v1.0.0 into main
Reviewed-on: #10
2026-05-15 08:00:58 +01:00
jamie 18e256baee feat: first release 2026-05-15 07:00:35 +00:00
jamie 7682a94981 fix: 🐛 one-time credentials
CI / Build and Push (push) Successful in 4s
CI / SonarQube (push) Successful in 31s
2026-05-14 14:54:43 +00:00
jamie bb724377fe style: 🎨 more icons and ability to edit folders
CI / Build and Push (push) Successful in 8s
CI / SonarQube (push) Successful in 31s
2026-05-14 12:44:41 +00:00
jamie 22a3dc7cbe style: 🎨 delete icon and reduce icon size
CI / Build and Push (push) Successful in 7s
CI / SonarQube (push) Successful in 31s
2026-05-14 12:38:36 +00:00
jamie 6069f5395a style: 🎨 edit icon
CI / Build and Push (push) Successful in 8s
CI / SonarQube (push) Successful in 31s
2026-05-14 12:34:04 +00:00
8 changed files with 189 additions and 70 deletions
-46
View File
@@ -1,46 +0,0 @@
name: CI
on:
workflow_dispatch:
jobs:
build-and-push:
name: Build and Push
runs-on: build-htz-01
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Build and push Docker image
run: |
docker build -t cr.jdbnet.co.uk/public/ssh:latest .
docker push cr.jdbnet.co.uk/public/ssh:latest
sonarqube:
name: SonarQube
runs-on: build-htz-01
steps:
- name: Checkout Code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Create Valid Project Key
id: sonar_setup
run: |
CLEAN_KEY=$(echo "${{ gitea.repository }}" | tr '/' ':')
echo "key=$CLEAN_KEY" >> $GITHUB_OUTPUT
- name: SonarQube Scan
uses: sonarsource/sonarqube-scan-action@master
continue-on-error: true
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
with:
args: >
-Dsonar.projectKey=${{ steps.sonar_setup.outputs.key }}
-Dsonar.projectName=${{ gitea.repository }}
-Dsonar.qualitygate.wait=true
-2
View File
@@ -1,8 +1,6 @@
name: CI
on:
push:
branches: [ main ]
workflow_dispatch:
jobs:
+46
View File
@@ -0,0 +1,46 @@
name: Release
on:
pull_request:
branches:
- main
types: [closed]
jobs:
release:
if: github.event.pull_request.merged == true && startsWith(github.head_ref, 'v')
runs-on: build-htz-01
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Extract Version
id: get_version
run: echo "VERSION=${{ github.head_ref }}" >> $GITHUB_OUTPUT
# - name: Generate Changelog
# id: changelog
# uses: https://github.com/metcalfc/changelog-generator@v4.6.2
# with:
# myToken: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
run: |
VERSION=${{ steps.get_version.outputs.VERSION }}
docker build -t cr.jdbnet.co.uk/public/ssh:$VERSION \
-t cr.jdbnet.co.uk/public/ssh:latest \
--build-arg VERSION=$VERSION \
.
docker push cr.jdbnet.co.uk/public/ssh:$VERSION
docker push cr.jdbnet.co.uk/public/ssh:latest
- name: Create Gitea Release
uses: https://gitea.com/actions/gitea-release-action@v1
with:
tag_name: ${{ steps.get_version.outputs.VERSION }}
name: ${{ steps.get_version.outputs.VERSION }}
body: ${{ steps.changelog.outputs.changelog }}
draft: false
prerelease: false
+2
View File
@@ -8,6 +8,8 @@ RUN npm run build
FROM python:3.14-slim
LABEL org.opencontainers.image.vendor="JDB-NET"
WORKDIR /app
ARG VERSION=unknown
ENV VERSION=${VERSION}
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . /app
+20 -3
View File
@@ -48,6 +48,7 @@ app.config["SESSION_COOKIE_SAMESITE"] = "Lax"
app.config["PERMANENT_SESSION_LIFETIME"] = timedelta(
days=int(os.getenv("SESSION_DAYS", "14"))
)
app.config["VERSION"] = os.getenv("VERSION", "unknown")
if os.getenv("SESSION_COOKIE_SECURE", "").lower() in ("1", "true", "yes"):
app.config["SESSION_COOKIE_SECURE"] = True
@@ -111,7 +112,7 @@ def init_db():
label VARCHAR(255) NOT NULL,
hostname VARCHAR(512) NOT NULL,
port INT NOT NULL DEFAULT 22,
identity_id INT,
identity_id INT NULL,
inline_identity_auth_type ENUM('password','publickey') NULL,
inline_identity_encrypted_blob TEXT NULL,
inline_identity_encrypted_key_passphrase TEXT NULL,
@@ -170,6 +171,21 @@ def _ensure_jump_host_schema(cur) -> None:
def _ensure_inline_identity_schema(cur) -> None:
"""Migrate existing databases to support inline (one-time) credentials."""
# Check and modify identity_id to allow NULL
cur.execute(
"""
SELECT 1
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'ssh_hosts'
AND COLUMN_NAME = 'identity_id'
AND IS_NULLABLE = 'NO'
LIMIT 1
"""
)
if cur.fetchone() is not None:
cur.execute("ALTER TABLE ssh_hosts MODIFY COLUMN identity_id INT NULL")
# Check and add inline_identity_auth_type column
cur.execute(
"""
@@ -662,9 +678,10 @@ def api_logout():
@app.route("/api/me", methods=["GET"])
def api_me():
version = app.config.get("VERSION", "unknown")
if session.get("logged_in"):
return jsonify({"logged_in": True})
return jsonify({"logged_in": False})
return jsonify({"logged_in": True, "app_version": version})
return jsonify({"logged_in": False, "app_version": version})
@app.route("/api/identities", methods=["GET"])
+97 -12
View File
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { Folder } from "lucide-vue-next";
import { Folder, Pencil, Trash2 } from "lucide-vue-next";
import {
api,
type HostRow,
@@ -19,6 +19,7 @@ interface TabItem {
const loggedIn = ref(false);
const checking = ref(true);
const appVersion = ref("unknown");
const identities = ref<IdentityRow[]>([]);
const allHosts = ref<HostRow[]>([]);
const allFolders = ref<FolderRow[]>([]);
@@ -50,6 +51,12 @@ const auditShowAll = ref(false);
const deleteIdentityErr = ref("");
const deleteIdentityErrId = ref<number | null>(null);
const newFolderLabel = ref("");
const showEditFolder = ref(false);
const editFolderForm = ref({
id: 0,
label: "",
parent_id: null as number | null,
});
const identityForm = ref({
label: "",
auth_type: "password" as "password" | "publickey",
@@ -170,6 +177,9 @@ onMounted(async () => {
try {
const m = await api.me();
loggedIn.value = m.logged_in;
if (m.app_version) {
appVersion.value = m.app_version;
}
if (loggedIn.value) await refreshData();
} catch {
loggedIn.value = false;
@@ -376,6 +386,25 @@ async function submitFolder() {
await refreshBrowse();
}
function openEditFolder(f: FolderRow) {
editFolderForm.value = {
id: f.id,
label: f.label,
parent_id: f.parent_id,
};
showEditFolder.value = true;
}
async function submitEditFolder() {
const f = editFolderForm.value;
await api.updateFolder(f.id, {
label: f.label.trim(),
parent_id: f.parent_id,
});
showEditFolder.value = false;
await refreshBrowse();
}
function openEditHost(h: HostRow) {
const hasInlineIdentity = h.identity_id === null;
editHostForm.value = {
@@ -502,7 +531,15 @@ async function deleteIdentityRow(id: number) {
/>
</svg>
</button>
<a
href="https://git.jdbnet.co.uk/jamie/ssh"
target="_blank"
rel="noopener noreferrer"
class="flex items-center gap-2 truncate"
>
<span class="truncate text-sm font-semibold text-white">JDB-NET SSH</span>
<span class="truncate text-xs text-slate-400 hover:text-slate-300">{{ appVersion }}</span>
</a>
</div>
<div class="flex items-center gap-2">
<button
@@ -620,13 +657,24 @@ async function deleteIdentityRow(id: number) {
<span>{{ f.label }}</span>
</span>
</button>
<div class="flex gap-1 shrink-0">
<button
type="button"
class="shrink-0 text-[10px] text-red-400/80 hover:underline"
class="text-slate-400/70 hover:text-slate-300"
title="Rename folder"
@click="openEditFolder(f)"
>
<Pencil class="h-3 w-3" aria-hidden="true" />
</button>
<button
type="button"
class="text-red-400/70 hover:text-red-400"
title="Delete folder"
@click="deleteFolderRow(f.id)"
>
Del
<Trash2 class="h-3 w-3" aria-hidden="true" />
</button>
</div>
</li>
</ul>
<div v-if="browseHosts.length" class="mb-2 flex items-center justify-between">
@@ -667,20 +715,22 @@ async function deleteIdentityRow(id: number) {
>
{{ folderOptionLabel(h.folder_id) }}
</p>
<div class="mt-1 flex gap-2">
<div class="mt-1 flex gap-1">
<button
type="button"
class="text-[10px] text-slate-400 hover:text-white hover:underline"
class="text-slate-400/70 hover:text-slate-300"
title="Edit host"
@click="openEditHost(h)"
>
Edit
<Pencil class="h-3 w-3" aria-hidden="true" />
</button>
<button
type="button"
class="text-[10px] text-red-400/80 hover:underline"
class="text-red-400/70 hover:text-red-400"
title="Delete host"
@click="deleteHostRow(h.id)"
>
Remove
<Trash2 class="h-3 w-3" aria-hidden="true" />
</button>
</div>
</li>
@@ -720,17 +770,19 @@ async function deleteIdentityRow(id: number) {
<div class="flex gap-1">
<button
type="button"
class="shrink-0 text-slate-400/70 hover:text-slate-300 hover:underline"
class="shrink-0 text-slate-400/70 hover:text-slate-300"
title="Edit identity"
@click="openEditIdentity(i)"
>
Edit
<Pencil class="h-3 w-3" aria-hidden="true" />
</button>
<button
type="button"
class="shrink-0 text-red-400/70 hover:underline"
class="shrink-0 text-red-400/70 hover:text-red-400"
title="Delete identity"
@click="deleteIdentityRow(i.id)"
>
×
<Trash2 class="h-3 w-3" aria-hidden="true" />
</button>
</div>
</li>
@@ -1185,6 +1237,39 @@ async function deleteIdentityRow(id: number) {
</form>
</div>
<div
v-if="showEditFolder"
class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4"
>
<form
class="w-full max-w-sm rounded-xl border border-slate-800 bg-surface-raised p-6 shadow-xl"
@submit.prevent="submitEditFolder"
>
<h2 class="text-lg font-semibold text-white">Rename folder</h2>
<label class="mt-4 block text-xs uppercase text-slate-500">Name</label>
<input
v-model="editFolderForm.label"
required
class="mt-1 w-full rounded border border-slate-700 bg-surface-overlay px-2 py-1.5 text-sm"
>
<div class="mt-6 flex justify-end gap-2">
<button
type="button"
class="rounded-lg px-4 py-2 text-sm text-slate-400 hover:bg-slate-800"
@click="showEditFolder = false"
>
Cancel
</button>
<button
type="submit"
class="rounded-lg bg-accent px-4 py-2 text-sm font-medium text-slate-950"
>
Save
</button>
</div>
</form>
</div>
<div
v-if="showEditHost"
class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4"
+17 -1
View File
@@ -21,7 +21,7 @@ function browseParams(folderId: number | null, q: string): string {
}
export const api = {
async me(): Promise<{ logged_in: boolean }> {
async me(): Promise<{ logged_in: boolean; app_version?: string }> {
const res = await fetch("/api/me", { credentials: "include" });
return handle(res);
},
@@ -92,6 +92,22 @@ export const api = {
await handle(res);
},
async updateFolder(
id: number,
body: {
label?: string;
parent_id?: number | null;
},
): Promise<void> {
const res = await fetch(`/api/folders/${id}`, {
method: "PATCH",
credentials: "include",
headers: jsonHeaders,
body: JSON.stringify(body),
});
await handle(res);
},
async listIdentities(): Promise<IdentityRow[]> {
const res = await fetch("/api/identities", { credentials: "include" });
const d = await handle<{ items: IdentityRow[] }>(res);
Submodule
+1
Submodule jdbnet.co.uk added at 4c2697f274