Skip to content

Commit

Permalink
feat: complete ssh keys management
Browse files Browse the repository at this point in the history
  • Loading branch information
yankeguo committed Jan 25, 2024
1 parent 1e40862 commit 73e4224
Show file tree
Hide file tree
Showing 9 changed files with 220 additions and 4 deletions.
65 changes: 65 additions & 0 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/yankeguo/rg"
"github.com/yankeguo/ufx"
"go.uber.org/fx"
"golang.org/x/crypto/ssh"
"gorm.io/gorm"
)

Expand Down Expand Up @@ -163,8 +164,72 @@ func (a *App) routeSignOut(c ufx.Context) {
c.JSON(map[string]any{})
}

func (a *App) routeListKeys(c ufx.Context) {
_, user := a.requireUser(c)

db := dao.Use(a.db)

keys := rg.Must(db.Key.Where(db.Key.UserID.Eq(user.ID)).Find())

c.JSON(map[string]any{"keys": keys})
}

func (a *App) routeCreateKey(c ufx.Context) {
_, user := a.requireUser(c)

var data struct {
DisplayName string `json:"display_name"`
PublicKey string `json:"public_key"`
}
c.Bind(&data)

if data.DisplayName == "" {
data.DisplayName = "Unnamed"
}

if data.PublicKey == "" {
halt.String("public key is required", halt.WithBadRequest())
return
}

k, _, _, _ := rg.Must4(ssh.ParseAuthorizedKey([]byte(data.PublicKey)))

id := ssh.FingerprintSHA256(k)

db := dao.Use(a.db)

key := &model.Key{
ID: id,
DisplayName: data.DisplayName,
UserID: user.ID,
CreatedAt: time.Now(),
}

rg.Must0(db.Key.Create(key))

c.JSON(map[string]any{"key": key})
}

func (a *App) routeDeleteKey(c ufx.Context) {
_, user := a.requireUser(c)

var data struct {
ID string `json:"id"`
}
c.Bind(&data)

db := dao.Use(a.db)

rg.Must(db.Key.Where(db.Key.ID.Eq(data.ID), db.Key.UserID.Eq(user.ID)).Delete())

c.JSON(map[string]any{})
}

func InstallAppToRouter(a *App, ur ufx.Router) {
ur.HandleFunc("/backend/current_user", a.routeCurrentUser)
ur.HandleFunc("/backend/sign_in", a.routeSignIn)
ur.HandleFunc("/backend/sign_out", a.routeSignOut)
ur.HandleFunc("/backend/keys", a.routeListKeys)
ur.HandleFunc("/backend/keys/create", a.routeCreateKey)
ur.HandleFunc("/backend/keys/delete", a.routeDeleteKey)
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.21.6
require (
github.com/git-lfs/wildmatch v1.0.4
github.com/glebarez/sqlite v1.10.0
github.com/yankeguo/halt v0.1.0
github.com/yankeguo/rg v1.1.0
github.com/yankeguo/ufx v0.2.2
go.uber.org/fx v1.20.1
Expand Down Expand Up @@ -41,7 +42,6 @@ require (
github.com/prometheus/common v0.46.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/yankeguo/halt v0.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 // indirect
go.opentelemetry.io/contrib/propagators/b3 v1.22.0 // indirect
go.opentelemetry.io/otel v1.22.0 // indirect
Expand Down
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,6 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
Expand Down
7 changes: 6 additions & 1 deletion ui/components/skeleton/dashboard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@ const links = [
],
[
{
label: "Profile",
label: "My SSH Keys",
icon: "i-mdi-key-chain",
to: { name: "dashboard-profile-keys" },
},
{
label: "My Profile",
icon: "i-mdi-account-circle",
to: { name: "dashboard-profile" },
},
Expand Down
7 changes: 7 additions & 0 deletions ui/composables/key.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const useKeys = () => {
return useAsyncData<{ keys: BKey[] }>("keys", () => $fetch("/backend/keys"), {
default() {
return { keys: [] };
},
});
};
File renamed without changes.
64 changes: 64 additions & 0 deletions ui/pages/dashboard/profile/keys/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<script setup lang="ts">
definePageMeta({
middleware: ["auth"],
});
const columns = [
{
key: "display_name",
label: "Name",
},
{
key: "id",
label: "Fingerprint",
},
{
key: "actions",
},
];
const { data: keys, refresh: refreshKeys } = await useKeys();
const deletionWorking = ref(false);
async function deleteKey(id: string) {
if (!confirm("Are you sure you want to delete this key?")) {
return;
}
deletionWorking.value = true;
try {
await $fetch("/backend/keys/delete", {
method: "POST",
body: JSON.stringify({ id }),
});
} catch (e: any) {
handleError(e);
} finally {
deletionWorking.value = false;
}
refreshKeys();
}
</script>

<template>
<SkeletonDashboard title-name="SSH Keys" title-icon="i-mdi-key-chain">
<div class="mb-4">
<UButton
:to="{ name: 'dashboard-profile-keys-new' }"
icon="i-mdi-plus"
label="Add SSH Key"
></UButton>
</div>
<UTable :rows="keys.keys" :columns="columns">
<template #actions-data="{ row }">
<UButton
variant="link"
color="red"
icon="i-mdi-trash"
label="Delete"
@click="deleteKey(row.id)"
></UButton>
</template>
</UTable>
</SkeletonDashboard>
</template>
70 changes: 70 additions & 0 deletions ui/pages/dashboard/profile/keys/new.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<script setup lang="ts">
import type { FormError, FormSubmitEvent } from "#ui/types";
definePageMeta({
middleware: ["auth"],
});
const state = reactive({
display_name: undefined,
public_key: undefined,
});
const validate = (state: any): FormError[] => {
const errors = [];
if (!state.display_name)
errors.push({ path: "display_name", message: "Required" });
if (!state.public_key)
errors.push({ path: "public_key", message: "Required" });
return errors;
};
const working = ref(false);
async function onSubmit(event: FormSubmitEvent<any>) {
working.value = true;
try {
await $fetch("/backend/keys/create", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(event.data),
});
navigateTo({ name: "dashboard-profile-keys" });
} catch (e: any) {
handleError(e);
} finally {
working.value = false;
}
}
</script>

<template>
<SkeletonDashboard title-name="SSH Keys" title-icon="i-mdi-key-chain">
<UForm
:validate="validate"
:state="state"
class="space-y-4 w-80"
@submit="onSubmit"
>
<UFormGroup label="Display Name" name="display_name">
<UInput v-model="state.display_name" />
</UFormGroup>

<UFormGroup label="Public Key" name="public_key">
<UTextarea v-model="state.public_key" :rows="12" />
</UFormGroup>

<UButton
type="submit"
:disabled="working"
:loading="working"
icon="i-mdi-check"
label="Submit"
>
</UButton>
</UForm>
</SkeletonDashboard>
</template>
7 changes: 7 additions & 0 deletions ui/utils/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
export interface BKey {
id: string;
display_name: string;
created_at: string;
visited_at: string;
}

export interface BUser {
id: string;
created_at: string;
Expand Down

0 comments on commit 73e4224

Please sign in to comment.