mirror of
https://github.com/versia-pub/server.git
synced 2026-03-13 05:49:16 +01:00
Add full OpenID connect provider support
This commit is contained in:
parent
14d96ac9e6
commit
947c1f4991
47 changed files with 604 additions and 247 deletions
|
|
@ -1,6 +1,6 @@
|
|||
<script setup>
|
||||
// Import Tailwind style reset
|
||||
import '@unocss/reset/tailwind.css'
|
||||
import '@unocss/reset/tailwind-compat.css'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
|||
29
pages/components/LoginInput.vue
Normal file
29
pages/components/LoginInput.vue
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<template>
|
||||
<div class="flex items-center justify-between">
|
||||
<label for="password" class="block text-sm font-medium leading-6 text-gray-900">{{ label }}</label>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<input v-bind="$attrs" @input="checkValid" :class="['block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6',
|
||||
isInvalid && 'invalid:!ring-red-600 invalid:ring-2']">
|
||||
<span v-if="isInvalid" class="mt-1 text-xs text-red-600">{{ label }} is invalid</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
label: string;
|
||||
}>();
|
||||
|
||||
const isInvalid = ref(false);
|
||||
|
||||
const checkValid = (e: Event) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
if (target.checkValidity()) {
|
||||
isInvalid.value = false;
|
||||
} else {
|
||||
isInvalid.value = true;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -1,28 +1,43 @@
|
|||
<template>
|
||||
<div class="flex min-h-screen flex-col justify-center px-6 py-12 lg:px-8">
|
||||
<div class="flex min-h-screen flex-col justify-center px-6 py-12 lg:px-8">
|
||||
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
|
||||
<form class="space-y-6" method="POST" :action="`/auth/login?redirect_uri=${redirect_uri}&response_type=${response_type}&client_id=${client_id}&scope=${scope}`">
|
||||
<form class="space-y-6" method="POST"
|
||||
:action="`/auth/login?redirect_uri=${redirect_uri}&response_type=${response_type}&client_id=${client_id}&scope=${scope}`">
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium leading-6 text-gray-900">Email address</label>
|
||||
<div class="mt-2">
|
||||
<input id="email" name="email" type="email" autocomplete="email" required
|
||||
class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6">
|
||||
</div>
|
||||
<h1 class="font-bold text-2xl text-center tracking-tight">Login to your account</h1>
|
||||
</div>
|
||||
|
||||
<div v-if="error && error !== 'undefined'" class="rounded bg-purple-100 ring-1 ring-purple-800 py-2 px-4">
|
||||
<h3 class="font-bold">An error occured:</h3>
|
||||
<p>{{ error }}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex items-center justify-between">
|
||||
<label for="password" class="block text-sm font-medium leading-6 text-gray-900">Password</label>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<input id="password" name="password" type="password" autocomplete="current-password" required
|
||||
class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6">
|
||||
<LoginInput label="Email" id="email" name="email" type="email" autocomplete="email" required />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<LoginInput label="Password" id="password" name="password" type="password"
|
||||
autocomplete="current-password" required />
|
||||
</div>
|
||||
|
||||
<div v-if="oauthProviders && oauthProviders.length > 0" class="w-full flex flex-col gap-3">
|
||||
<h2 class="text-sm text-gray-700">Or sign in with</h2>
|
||||
<div class="grid grid-cols-1 gap-4 w-full">
|
||||
<a v-for="provider of oauthProviders" :key="provider.id"
|
||||
:href="`/oauth/authorize-external?issuer=${provider.id}&redirect_uri=${redirect_uri}&response_type=${response_type}&clientId=${client_id}&scope=${scope}`"
|
||||
class="flex flex-row rounded ring-1 gap-2 p-2 ring-black/10 hover:shadow duration-200">
|
||||
<img :src="provider.icon" :alt="provider.name" class="w-8 h-8" />
|
||||
<div class="flex flex-col gap-0 justify-center">
|
||||
<h3 class="font-bold">{{ provider.name }}</h3>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button type="submit"
|
||||
class="flex w-full justify-center rounded-md !bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:!bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Sign
|
||||
class="flex w-full justify-center rounded-md bg-purple-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:shadow-lg duration-200 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Sign
|
||||
in</button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -32,6 +47,8 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { useRoute } from 'vue-router';
|
||||
import LoginInput from "./components/LoginInput.vue"
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
const query = useRoute().query;
|
||||
|
||||
|
|
@ -39,4 +56,20 @@ const redirect_uri = query.redirect_uri;
|
|||
const response_type = query.response_type;
|
||||
const client_id = query.client_id;
|
||||
const scope = query.scope;
|
||||
const error = decodeURIComponent(query.error as string);
|
||||
|
||||
const oauthProviders = ref<{
|
||||
name: string;
|
||||
icon: string;
|
||||
id: string
|
||||
}[] | null>(null);
|
||||
|
||||
const getOauthProviders = async () => {
|
||||
const response = await fetch('/oauth/providers');
|
||||
return await response.json() as any;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
oauthProviders.value = await getOauthProviders();
|
||||
})
|
||||
</script>
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { createApp } from "vue";
|
||||
import "./style.css";
|
||||
import "virtual:uno.css";
|
||||
import { createRouter, createWebHistory } from "vue-router";
|
||||
import Login from "./login.vue";
|
||||
import App from "./App.vue";
|
||||
|
|
@ -8,7 +9,7 @@ const Home = { template: "<div>Home</div>" };
|
|||
|
||||
const routes = [
|
||||
{ path: "/", component: Home },
|
||||
{ path: "/oauth/login", component: Login },
|
||||
{ path: "/oauth/authorize", component: Login },
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
|
|
|
|||
155
pages/uno.css
155
pages/uno.css
|
|
@ -1,155 +0,0 @@
|
|||
/* layer: preflights */
|
||||
*,::before,::after{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 rgba(0,0,0,0);--un-ring-shadow:0 0 rgba(0,0,0,0);--un-shadow-inset: ;--un-shadow:0 0 rgba(0,0,0,0);--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgba(147,197,253,0.5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: ;}::backdrop{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 rgba(0,0,0,0);--un-ring-shadow:0 0 rgba(0,0,0,0);--un-shadow-inset: ;--un-shadow:0 0 rgba(0,0,0,0);--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgba(147,197,253,0.5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: ;}
|
||||
[type='text'], [type='email'], [type='url'], [type='password'], [type='number'], [type='date'], [type='datetime-local'], [type='month'], [type='search'], [type='tel'], [type='time'], [type='week'], [multiple], textarea, select { appearance: none;
|
||||
background-color: #fff;
|
||||
border-color: #6b7280;
|
||||
border-width: 1px;
|
||||
border-radius: 0;
|
||||
padding-top: 0.5rem;
|
||||
padding-right: 0.75rem;
|
||||
padding-bottom: 0.5rem;
|
||||
padding-left: 0.75rem;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5rem;
|
||||
--un-shadow: 0 0 #0000; }
|
||||
[type='text']:focus, [type='email']:focus, [type='url']:focus, [type='password']:focus, [type='number']:focus, [type='date']:focus, [type='datetime-local']:focus, [type='month']:focus, [type='search']:focus, [type='tel']:focus, [type='time']:focus, [type='week']:focus, [multiple]:focus, textarea:focus, select:focus { outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
--un-ring-inset: var(--un-empty,/*!*/ /*!*/);
|
||||
--un-ring-offset-width: 0px;
|
||||
--un-ring-offset-color: #fff;
|
||||
--un-ring-color: #2563eb;
|
||||
--un-ring-offset-shadow: var(--un-ring-inset) 0 0 0 var(--un-ring-offset-width) var(--un-ring-offset-color);
|
||||
--un-ring-shadow: var(--un-ring-inset) 0 0 0 calc(1px + var(--un-ring-offset-width)) var(--un-ring-color);
|
||||
box-shadow: var(--un-ring-offset-shadow), var(--un-ring-shadow), var(--un-shadow);
|
||||
border-color: #2563eb; }
|
||||
input::placeholder, textarea::placeholder { color: #6b7280;
|
||||
opacity: 1; }
|
||||
::-webkit-datetime-edit-fields-wrapper { padding: 0; }
|
||||
::-webkit-date-and-time-value { min-height: 1.5em; }
|
||||
::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field { padding-top: 0;
|
||||
padding-bottom: 0; }
|
||||
select { background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
|
||||
background-position: right 0.5rem center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 1.5em 1.5em;
|
||||
padding-right: 2.5rem;
|
||||
print-color-adjust: exact; }
|
||||
[multiple] { background-image: initial;
|
||||
background-position: initial;
|
||||
background-repeat: unset;
|
||||
background-size: initial;
|
||||
padding-right: 0.75rem;
|
||||
print-color-adjust: unset; }
|
||||
[type='checkbox'], [type='radio'] { appearance: none;
|
||||
padding: 0;
|
||||
print-color-adjust: exact;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
background-origin: border-box;
|
||||
user-select: none;
|
||||
flex-shrink: 0;
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
color: #2563eb;
|
||||
background-color: #fff;
|
||||
border-color: #6b7280;
|
||||
border-width: 1px;
|
||||
--un-shadow: 0 0 #0000; }
|
||||
[type='checkbox'] { border-radius: 0; }
|
||||
[type='radio'] { border-radius: 100%; }
|
||||
[type='checkbox']:focus, [type='radio']:focus { outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
--un-ring-inset: var(--un-empty,/*!*/ /*!*/);
|
||||
--un-ring-offset-width: 2px;
|
||||
--un-ring-offset-color: #fff;
|
||||
--un-ring-color: #2563eb;
|
||||
--un-ring-offset-shadow: var(--un-ring-inset) 0 0 0 var(--un-ring-offset-width) var(--un-ring-offset-color);
|
||||
--un-ring-shadow: var(--un-ring-inset) 0 0 0 calc(2px + var(--un-ring-offset-width)) var(--un-ring-color);
|
||||
box-shadow: var(--un-ring-offset-shadow), var(--un-ring-shadow), var(--un-shadow); }
|
||||
[type='checkbox']:checked, [type='radio']:checked { border-color: transparent;
|
||||
background-color: currentColor;
|
||||
background-size: 100% 100%;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat; }
|
||||
[type='checkbox']:checked { background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e"); }
|
||||
[type='radio']:checked { background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e"); }
|
||||
[type='checkbox']:checked:hover, [type='checkbox']:checked:focus, [type='radio']:checked:hover, [type='radio']:checked:focus { border-color: transparent;
|
||||
background-color: currentColor; }
|
||||
[type='checkbox']:indeterminate { background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3e%3c/svg%3e");
|
||||
border-color: transparent;
|
||||
background-color: currentColor;
|
||||
background-size: 100% 100%;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat; }
|
||||
[type='checkbox']:indeterminate:hover, [type='checkbox']:indeterminate:focus { border-color: transparent;
|
||||
background-color: currentColor; }
|
||||
[type='file'] { background: unset;
|
||||
border-color: inherit;
|
||||
border-width: 0;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
font-size: unset;
|
||||
line-height: inherit; }
|
||||
[type='file']:focus { outline: 1px solid ButtonText , 1px auto -webkit-focus-ring-color; }
|
||||
/* layer: default */
|
||||
.visible{visibility:visible;}
|
||||
.relative{position:relative;}
|
||||
.mt-10{margin-top:2.5rem;}
|
||||
.mt-2{margin-top:0.5rem;}
|
||||
.block{display:block;}
|
||||
.contents{display:contents;}
|
||||
.list-item{display:list-item;}
|
||||
.hidden{display:none;}
|
||||
.h6{height:1.5rem;}
|
||||
.min-h-screen{min-height:100vh;}
|
||||
.w-full{width:100%;}
|
||||
.flex{display:flex;}
|
||||
.flex-col{flex-direction:column;}
|
||||
.table{display:table;}
|
||||
.border-collapse{border-collapse:collapse;}
|
||||
.transform{transform:translateX(var(--un-translate-x)) translateY(var(--un-translate-y)) translateZ(var(--un-translate-z)) rotate(var(--un-rotate)) rotateX(var(--un-rotate-x)) rotateY(var(--un-rotate-y)) rotateZ(var(--un-rotate-z)) skewX(var(--un-skew-x)) skewY(var(--un-skew-y)) scaleX(var(--un-scale-x)) scaleY(var(--un-scale-y)) scaleZ(var(--un-scale-z));}
|
||||
.resize{resize:both;}
|
||||
.items-center{align-items:center;}
|
||||
.justify-center{justify-content:center;}
|
||||
.justify-between{justify-content:space-between;}
|
||||
.space-y-6>:not([hidden])~:not([hidden]){--un-space-y-reverse:0;margin-top:calc(1.5rem * calc(1 - var(--un-space-y-reverse)));margin-bottom:calc(1.5rem * var(--un-space-y-reverse));}
|
||||
.border{border-width:1px;}
|
||||
.border-0{border-width:0;}
|
||||
.rounded-md{border-radius:0.375rem;}
|
||||
.\!bg-indigo-600{--un-bg-opacity:1 !important;background-color:rgba(79,70,229,var(--un-bg-opacity)) !important;}
|
||||
.hover\:\!bg-indigo-500:hover{--un-bg-opacity:1 !important;background-color:rgba(99,102,241,var(--un-bg-opacity)) !important;}
|
||||
.px-3{padding-left:0.75rem;padding-right:0.75rem;}
|
||||
.px-6{padding-left:1.5rem;padding-right:1.5rem;}
|
||||
.py-1\.5{padding-top:0.375rem;padding-bottom:0.375rem;}
|
||||
.py-12{padding-top:3rem;padding-bottom:3rem;}
|
||||
.text-sm{font-size:0.875rem;line-height:1.25rem;}
|
||||
.font-medium{font-weight:500;}
|
||||
.font-semibold{font-weight:600;}
|
||||
.leading-6{line-height:1.5rem;}
|
||||
.text-gray-900{--un-text-opacity:1;color:rgba(17,24,39,var(--un-text-opacity));}
|
||||
.text-white{--un-text-opacity:1;color:rgba(255,255,255,var(--un-text-opacity));}
|
||||
.placeholder\:text-gray-400::placeholder{--un-text-opacity:1;color:rgba(156,163,175,var(--un-text-opacity));}
|
||||
.underline{text-decoration-line:underline;}
|
||||
.tab{-moz-tab-size:4;-o-tab-size:4;tab-size:4;}
|
||||
.shadow-sm{--un-shadow:var(--un-shadow-inset) 0 1px 2px 0 var(--un-shadow-color, rgba(0,0,0,0.05));box-shadow:var(--un-ring-offset-shadow), var(--un-ring-shadow), var(--un-shadow);}
|
||||
.focus-visible\:outline-2:focus-visible{outline-width:2px;}
|
||||
.focus-visible\:outline-indigo-600:focus-visible{--un-outline-color-opacity:1;outline-color:rgba(79,70,229,var(--un-outline-color-opacity));}
|
||||
.focus-visible\:outline-offset-2:focus-visible{outline-offset:2px;}
|
||||
.outline{outline-style:solid;}
|
||||
.focus-visible\:outline:focus-visible{outline-style:solid;}
|
||||
.ring-1{--un-ring-width:1px;--un-ring-offset-shadow:var(--un-ring-inset) 0 0 0 var(--un-ring-offset-width) var(--un-ring-offset-color);--un-ring-shadow:var(--un-ring-inset) 0 0 0 calc(var(--un-ring-width) + var(--un-ring-offset-width)) var(--un-ring-color);box-shadow:var(--un-ring-offset-shadow), var(--un-ring-shadow), var(--un-shadow);}
|
||||
.focus\:ring-2:focus{--un-ring-width:2px;--un-ring-offset-shadow:var(--un-ring-inset) 0 0 0 var(--un-ring-offset-width) var(--un-ring-offset-color);--un-ring-shadow:var(--un-ring-inset) 0 0 0 calc(var(--un-ring-width) + var(--un-ring-offset-width)) var(--un-ring-color);box-shadow:var(--un-ring-offset-shadow), var(--un-ring-shadow), var(--un-shadow);}
|
||||
.ring-gray-300{--un-ring-opacity:1;--un-ring-color:rgba(209,213,219,var(--un-ring-opacity));}
|
||||
.focus\:ring-indigo-600:focus{--un-ring-opacity:1;--un-ring-color:rgba(79,70,229,var(--un-ring-opacity));}
|
||||
.ring-inset{--un-ring-inset:inset;}
|
||||
.focus\:ring-inset:focus{--un-ring-inset:inset;}
|
||||
@media (min-width: 640px){
|
||||
.sm\:mx-auto{margin-left:auto;margin-right:auto;}
|
||||
.sm\:max-w-sm{max-width:24rem;}
|
||||
.sm\:w-full{width:100%;}
|
||||
.sm\:text-sm{font-size:0.875rem;line-height:1.25rem;}
|
||||
.sm\:leading-6{line-height:1.5rem;}
|
||||
}
|
||||
@media (min-width: 1024px){
|
||||
.lg\:px-8{padding-left:2rem;padding-right:2rem;}
|
||||
}
|
||||
|
|
@ -23,7 +23,7 @@ export default defineConfig({
|
|||
},
|
||||
plugins: [
|
||||
UnoCSS({
|
||||
mode: "vue-scoped",
|
||||
mode: "global",
|
||||
}),
|
||||
vue(),
|
||||
],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue