mirror of
https://github.com/Drop-OSS/drop.git
synced 2026-01-31 15:37:09 +01:00
Depot API & v4 (#298)
* feat: nginx + torrential basics & services system * fix: lint + i18n * fix: update torrential to remove openssl * feat: add torrential to Docker build * feat: move to self hosted runner * fix: move off self-hosted runner * fix: update nginx.conf * feat: torrential cache invalidation * fix: update torrential for cache invalidation * feat: integrity check task * fix: lint * feat: move to version ids * fix: client fixes and client-side checks * feat: new depot apis and version id fixes * feat: update torrential * feat: droplet bump and remove unsafe update functions * fix: lint * feat: v4 featureset: emulators, multi-launch commands * fix: lint * fix: mobile ui for game editor * feat: launch options * fix: lint * fix: remove axios, use $fetch * feat: metadata and task api improvements * feat: task actions * fix: slight styling issue * feat: fix style and lints * feat: totp backend routes * feat: oidc groups * fix: update drop-base * feat: creation of passkeys & totp * feat: totp signin * feat: webauthn mfa/signin * feat: launch selecting ui * fix: manually running tasks * feat: update add company game modal to use new SelectorGame * feat: executor selector * fix(docker): update rust to rust nightly for torrential build (#305) * feat: new version ui * feat: move package lookup to build time to allow for deno dev * fix: lint * feat: localisation cleanup * feat: apply localisation cleanup * feat: potential i18n refactor logic * feat: remove args from commands * fix: lint * fix: lockfile --------- Co-authored-by: Aden Lindsay <140392385+AdenMGB@users.noreply.github.com>
This commit is contained in:
32
pages/mfa/setup/successful.vue
Normal file
32
pages/mfa/setup/successful.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import { CheckCircleIcon } from "@heroicons/vue/24/outline";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-full w-full flex items-center justify-center py-24">
|
||||
<div class="flex flex-col items-center">
|
||||
<CheckCircleIcon class="h-12 w-12 text-green-600" aria-hidden="true" />
|
||||
<div class="mt-3 text-center sm:mt-5">
|
||||
<h1 class="text-3xl font-semibold font-display leading-6 text-zinc-100">
|
||||
Added your 2FA method!
|
||||
</h1>
|
||||
<div class="mt-4">
|
||||
<p class="mx-auto text-sm text-zinc-400 max-w-sm">
|
||||
Drop has successfully created and added your 2FA method. If this is
|
||||
your first time configuring 2FA, your account now requires it to
|
||||
sign in.
|
||||
</p>
|
||||
|
||||
<div class="mt-10 flex justify-center">
|
||||
<NuxtLink
|
||||
href="/account/security"
|
||||
class="text-sm/6 font-semibold text-blue-400"
|
||||
><span aria-hidden="true">←</span> Back to account
|
||||
security</NuxtLink
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
91
pages/mfa/setup/totp.vue
Normal file
91
pages/mfa/setup/totp.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<main
|
||||
class="mx-auto grid lg:grid-cols-2 max-w-md lg:max-w-none min-h-full place-items-center w-full gap-4 px-6 py-12 sm:py-32 lg:px-8"
|
||||
>
|
||||
<div>
|
||||
<div class="text-left max-w-md">
|
||||
<h1
|
||||
class="mt-4 text-3xl font-bold font-display tracking-tight text-zinc-100 sm:text-5xl"
|
||||
>
|
||||
Set up your authenticator
|
||||
</h1>
|
||||
<p class="mt-6 text-base leading-7 text-zinc-400">
|
||||
Use your TOTP authenticator, like Google Authenticator, Aegis, or
|
||||
Bitwarden, to add 2FA to your Drop account.
|
||||
</p>
|
||||
<div class="mt-8">
|
||||
<p class="text-xs leading-7 text-zinc-200">
|
||||
Enter the generated code to enable TOTP
|
||||
</p>
|
||||
<div class="mt-2 flex flex-row gap-2">
|
||||
<CodeInput
|
||||
:length="6"
|
||||
placeholder="123456"
|
||||
size="w-10 h-10 text-sm"
|
||||
@complete="(code) => complete(code)"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="error"
|
||||
class="mt-4 rounded-md bg-red-600/10 p-4 max-w-sm mx-auto"
|
||||
>
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<XCircleIcon class="h-5 w-5 text-red-600" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-600">
|
||||
{{ error }}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="max-w-2xl flex flex-col items-center gap-2">
|
||||
<div id="qrcode" />
|
||||
<p
|
||||
class="font-bold font-display text-zinc-500 uppercase font-sm tracking-tight"
|
||||
>
|
||||
{{ totpSecrets?.secret }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FetchError } from "ofetch";
|
||||
|
||||
useHead({
|
||||
title: "Set up TOTP",
|
||||
});
|
||||
|
||||
const totpSecrets = await $dropFetch("/api/v1/user/mfa/totp/start", {
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
const error = ref<string | undefined>();
|
||||
const router = useRouter();
|
||||
|
||||
onMounted(async () => {
|
||||
const kjua = await import("kjua");
|
||||
const el = kjua.default({ text: totpSecrets.url, render: "svg", size: 400 });
|
||||
document.querySelector("#qrcode")?.appendChild(el);
|
||||
});
|
||||
|
||||
async function complete(code: string) {
|
||||
try {
|
||||
await $dropFetch("/api/v1/user/mfa/totp/finish", {
|
||||
method: "POST",
|
||||
body: { code },
|
||||
});
|
||||
router.push("/mfa/setup/successful");
|
||||
} catch (e) {
|
||||
error.value =
|
||||
(e as FetchError).data?.message ?? (e as FetchError).statusMessage;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
133
pages/mfa/setup/webauthn.vue
Normal file
133
pages/mfa/setup/webauthn.vue
Normal file
@@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex min-h-full flex-1 flex-col justify-center px-6 py-12 lg:px-8"
|
||||
>
|
||||
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
|
||||
<KeyIcon class="text-blue-600 mx-auto h-10 w-auto" />
|
||||
<h2
|
||||
class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-white"
|
||||
>
|
||||
Create a passkey
|
||||
</h2>
|
||||
<p class="text-sm text-center text-zinc-400">
|
||||
WebAuthn, or passkeys, allow you to sign in or complete 2FA with
|
||||
biometrics or hardware security devices.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
|
||||
<form
|
||||
class="space-y-6"
|
||||
action="#"
|
||||
method="POST"
|
||||
@submit.prevent="attemptPasskeyWrapper"
|
||||
>
|
||||
<div>
|
||||
<label for="name" class="block text-sm/6 font-medium text-gray-100"
|
||||
>Name</label
|
||||
>
|
||||
<div class="mt-2">
|
||||
<input
|
||||
id="name"
|
||||
v-model="name"
|
||||
type="text"
|
||||
name="name"
|
||||
required
|
||||
placeholder="My New Passkey"
|
||||
class="block w-full rounded-md bg-white/5 px-3 py-1.5 text-base text-white outline-1 -outline-offset-1 outline-white/10 placeholder:text-gray-500 focus:outline-2 focus:-outline-offset-2 focus:outline-blue-500 sm:text-sm/6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<LoadingButton :disabled="disabled" :loading="loading" class="w-full">
|
||||
Create
|
||||
</LoadingButton>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="error"
|
||||
class="mt-4 rounded-md bg-red-600/10 p-4 max-w-sm mx-auto"
|
||||
>
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<XCircleIcon class="h-5 w-5 text-red-600" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-600">
|
||||
{{ error }}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { KeyIcon, XCircleIcon } from "@heroicons/vue/24/outline";
|
||||
import type { FetchError } from "ofetch";
|
||||
import { startRegistration } from "@simplewebauthn/browser";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const name = ref("");
|
||||
const disabled = computed(() => !name.value);
|
||||
const loading = ref(false);
|
||||
const error = ref<string | undefined>();
|
||||
|
||||
useHead({
|
||||
title: "Create a passkey",
|
||||
});
|
||||
|
||||
async function attemptPasskeyWrapper() {
|
||||
loading.value = true;
|
||||
try {
|
||||
await attemptPasskey();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
error.value = (e as FetchError)?.data?.message ?? e;
|
||||
}
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
async function attemptPasskey() {
|
||||
if (!window.PublicKeyCredential)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: "Browser does not support WebAuthn",
|
||||
fatal: true,
|
||||
});
|
||||
|
||||
const optionsJSON = await $dropFetch("/api/v1/user/mfa/webauthn/start", {
|
||||
method: "POST",
|
||||
body: {
|
||||
name: name.value,
|
||||
},
|
||||
});
|
||||
|
||||
let attResp;
|
||||
try {
|
||||
// Pass the options to the authenticator and wait for a response
|
||||
attResp = await startRegistration({ optionsJSON });
|
||||
} catch {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: "WebAuthn request cancelled.",
|
||||
});
|
||||
}
|
||||
if (!attResp)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: "WebAuthn request cancelled.",
|
||||
});
|
||||
|
||||
await $dropFetch("/api/v1/user/mfa/webauthn/finish", {
|
||||
method: "POST",
|
||||
body: attResp,
|
||||
});
|
||||
|
||||
router.push("/mfa/setup/successful");
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user