Implement a better authorization page

Fix default profile not making the button active

Fix it properly

Fix default profile

Fix invalid option value
This commit is contained in:
Saphire 2023-10-29 12:12:04 +06:00
parent 69dd3272b2
commit a029fa4b48
Signed by: Saphire
GPG Key ID: B26EB7A1F07044C4
7 changed files with 1221 additions and 2347 deletions

View File

@ -113,16 +113,13 @@ public class UserController : ControllerBase
p.ProfileUrl,
Clients = p.Clients.Select(c => new
{
c.Client.Id,
c.Scope,
c.FirstSeen,
c.LastSeen,
Client = new
{
c.Client.Id,
c.Client.ClientId,
c.Client.RedirectUri,
c.Client.Type
}
c.Client.ClientId,
c.Client.RedirectUri,
c.Client.Type
})
})
.ToListAsync();

3364
Web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -2,9 +2,20 @@ import { ref } from "vue";
import { defineStore } from "pinia";
import { get, send } from "@/utils";
interface UserClients {
id: number;
scope?: string;
firstSeen: string;
lastSeen: string;
clientId: string;
redirectUri: string;
type: number; // TODO: enum
}
interface UserProfiles {
id: number;
profileUrl: string;
clients: UserClients[];
}
interface UserInfo {

View File

@ -26,11 +26,6 @@ export async function send<Treceived, Tsent = any | undefined>(
method: "POST" | "PUT" | "PATCH" | "DELETE" | "GET" = "POST",
headers?: Record<string, string>
): Promise<Treceived> {
if (!url.startsWith("htt") && window.location.port == "5173")
url =
"http://localhost:3000" +
(url.startsWith("/") ? "" : window.location.pathname + "/") +
url;
if (fetch) {
return fetch(url, {
method: method,

View File

@ -1,55 +1,14 @@
<template>
<main>
<h2>
IndieAuth login request for: {{ state.request?.clientId }} ({{
state.request?.responseType
}})
</h2>
<form @submit.prevent="sendAuthorize">
<div>
<div v-if="state.request?.scope">
Scopes requested:
<ul>
<li v-for="scope in state.request.scope.split(' ')">
{{ scope }}
</li>
</ul>
</div>
<div v-else>No scopes requested</div>
</div>
<label>
<span>Profile ID</span>
<input
class="border border-slate-500"
placeholder="Profile ID"
v-model="state.profileId"
/>
</label>
<label v-if="!hasPkce">
<input v-model="state.ignoreMissingPkce" type="checkbox" />
Ignore missing PKCE
</label>
<div v-if="state.errorText">{{ state.errorText }}</div>
<button
type="submit"
:disabled="!hasPkce && !state.ignoreMissingPkce"
>
Authorize
</button>
<a v-if="state.redirectUri" :href="state.redirectUri">
Client redirect page
</a>
</form>
</main>
</template>
<script setup lang="ts">
import TailwindButton from "@/components/TailwindButton.vue";
import useAuthStore from "@/stores/auth";
import { send } from "@/utils";
import { computed } from "@vue/reactivity";
import { onMounted, ref } from "vue";
const auth = useAuthStore();
const state = ref({
profileId: "",
profileId: null as number | null,
errorText: "",
request: null as AuthRequest | null,
redirectUri: "",
@ -67,7 +26,8 @@ interface AuthRequest {
me?: string;
}
onMounted(() => {
onMounted(async () => {
await auth.ready;
const paramData = {} as { [idx: string]: string };
for (const iterator of new URLSearchParams(window.location.search)) {
paramData[
@ -75,6 +35,9 @@ onMounted(() => {
] = iterator[1];
}
state.value.request = paramData as any as AuthRequest;
// Plain default
if (!state.value.request.responseType)
state.value.request.responseType = "code";
});
const hasPkce = computed(
@ -83,6 +46,16 @@ const hasPkce = computed(
!!state.value.request.codeChallengeMethod
);
const defaultProfile = computed(() => auth.userInfo?.profiles[0]);
const validRequest = computed(
() =>
state.value.request &&
state.value.request.clientId &&
(state.value.profileId || defaultProfile.value) &&
(hasPkce.value || state.value.ignoreMissingPkce)
);
async function sendAuthorize() {
if (!hasPkce.value && !state.value.ignoreMissingPkce) return;
@ -90,6 +63,11 @@ async function sendAuthorize() {
if (!request) return;
if (!request.responseType) {
state.value.errorText = "No response type specified";
return;
}
if (
!request.clientId ||
!request.redirectUri ||
@ -104,7 +82,7 @@ async function sendAuthorize() {
"/api/indieauth/request",
{
...request,
profileId: +state.value.profileId,
profileId: state.value.profileId ?? defaultProfile.value?.id,
}
);
state.value.redirectUri = result.redirectUri;
@ -129,3 +107,70 @@ async function sendAuthorize() {
}
}
</script>
<template>
<main class="divide-y bg-zinc-800 rounded">
<div class="py-2 px-4">
<h2 class="text-2xl">IndieAuth login request</h2>
<div v-if="!state.request?.clientId" class="text-red-500">
No client ID!
</div>
<div v-else>
{{ state.request?.clientId }}
({{ state.request?.responseType }})
</div>
</div>
<form @submit.prevent="sendAuthorize" class="flex flex-col px-8 py-2">
<div class="mb-3">
<div v-if="state.request?.scope">
Scopes requested:
<ul>
<li v-for="scope in state.request.scope.split(' ')">
{{ scope }}
</li>
</ul>
</div>
<div v-else>No scopes requested</div>
</div>
<label class="mb-3">
<span>Profile ID: </span>
<select
class="border px-3 py-1 border-slate-500 text-slate-200"
placeholder="Profile ID"
v-model="state.profileId"
>
<option :value="null">
<template v-if="defaultProfile">
{{ defaultProfile?.profileUrl }} (Default)
</template>
<template v-else>No profiles</template>
</option>
<template v-for="profile in auth.userInfo?.profiles">
<option
v-if="profile != defaultProfile"
:value="profile.id"
>
{{ profile.profileUrl }}
</option>
</template>
</select>
</label>
<label class="mb-3" v-if="!hasPkce">
<input v-model="state.ignoreMissingPkce" type="checkbox" />
Ignore missing PKCE
</label>
<div v-if="state.errorText">{{ state.errorText }}</div>
<TailwindButton
custom-class="rounded bg-zinc-900 border-zinc-600 disabled:bg-zinc-800 disabled:text-zinc-600 disabled:border-zinc-700 hover:bg-zinc-700 hover:border-zinc-600"
class="mx-3"
type="submit"
:disabled="!validRequest"
>
Authorize
</TailwindButton>
<a v-if="state.redirectUri" :href="state.redirectUri">
Client redirect page
</a>
</form>
</main>
</template>

View File

@ -1,11 +1,36 @@
<script setup lang="ts">
import TailwindButton from "@/components/TailwindButton.vue";
import useAuthStore from "@/stores/auth";
import { onMounted } from "vue";
const auth = useAuthStore();
onMounted(async () => {
await auth.ready;
});
</script>
<template>
<main class="flex flex-col w-80">
<h2 class="mb-2 text-center text-xl">Home</h2>
<div>TODO</div>
<div v-if="!auth.userInfo" class="m-2">Loading...</div>
<div v-else class="m-2">
<div>
<span class="text-zinc-500">Logged in as:</span>
{{ auth.userInfo.username }}
</div>
<div class="my-4">
<div v-if="auth.userInfo.profiles.length == 0">
No profiles registered
</div>
<div
v-for="profile in auth.userInfo.profiles"
class="bg-zinc-600 text-zinc-200 py-3 px-4 first:rounded-t-lg last:rounded-b-lg cursor-pointer transition-colors focus:outline outline-2 outline-offset-2"
>
{{ profile.profileUrl }} - ID: {{ profile.id }}
</div>
</div>
</div>
<tailwind-button
title="This will erase all local data!"
to="logout"

View File

@ -28,4 +28,13 @@ export default defineConfig({
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
server: {
port: 5183,
proxy: {
"/api": {
target: "http://localhost:3000/",
changeOrigin: true,
},
},
},
});