feat: introducing Auth for admin dashboard (HBE-138) (#32)

This commit is contained in:
Anwarul Islam
2023-03-09 11:29:40 +06:00
committed by GitHub
parent 80898407c3
commit 9b76d62753
29 changed files with 1846 additions and 765 deletions

View File

@@ -26,15 +26,25 @@
<icon-lucide-bell class="text-gray-300 w-6" />
</button>
<div class="relative">
<div v-if="currentUser" class="relative">
<button
@click="dropdownOpen = !dropdownOpen"
class="relative z-10 block w-8 h-8 overflow-hidden rounded-full shadow focus:outline-none"
>
<img
class="object-cover w-full h-full"
src="https://media.licdn.com/dms/image/C5603AQHMCx72bNN1MA/profile-displayphoto-shrink_800_800/0/1630736416611?e=2147483647&v=beta&t=McWCdK_7t_NLeU4ze3JPB8xvwg5w50Okuj2JDBekqjw"
alt="Your avatar"
<ProfilePicture
v-if="currentUser.photoURL"
v-tippy="{
theme: 'tooltip',
}"
:url="currentUser.photoURL"
:alt="currentUser.displayName ?? 'No Name'"
:title="currentUser.displayName ?? currentUser.email ?? 'No Name'"
/>
<ProfilePicture
v-else
v-tippy="{ theme: 'tooltip' }"
:title="currentUser.displayName ?? currentUser.email ?? 'No Name'"
:initial="currentUser.displayName ?? currentUser.email"
/>
</button>
@@ -56,21 +66,13 @@
v-show="dropdownOpen"
class="absolute right-0 z-20 w-48 py-2 mt-2 bg-zinc-200 dark:bg-zinc-800 rounded-md shadow-xl"
>
<a
href="#"
class="block px-4 py-2 text-sm text-gray-800 dark:text-gray-200 hover:bg-emerald-700 hover:text-white"
>Profile</a
>
<a
href="#"
class="block px-4 py-2 text-sm text-gray-800 dark:text-gray-200 hover:bg-emerald-700 hover:text-white"
>Settings</a
>
<router-link
to="/"
class="block px-4 py-2 text-sm text-gray-800 dark:text-gray-200 hover:bg-emerald-700 hover:text-white"
>Log out</router-link
>
<HoppSmartItem to="/profile" :icon="IconUser" :label="'Profile'" />
<HoppSmartItem
to="/settings"
:icon="IconSettings"
:label="'Settings'"
/>
<AppLogout ref="logout" />
</div>
</transition>
</div>
@@ -79,11 +81,20 @@
</template>
<script setup lang="ts">
import IconSettings from '~icons/lucide/settings';
import IconUser from '~icons/lucide/user';
import { ref } from 'vue';
import { useSidebar } from '../../composables/useSidebar';
import { useReadonlyStream } from '~/composables/stream';
import { useSidebar } from '~/composables/useSidebar';
import { auth } from '~/helpers/auth';
const { isOpen, isExpanded } = useSidebar();
const currentUser = useReadonlyStream(
auth.getProbableUserStream(),
auth.getProbableUser()
);
const expandSidebar = () => {
isExpanded.value = !isExpanded.value;
};

View File

@@ -0,0 +1,236 @@
<template>
<div v-if="nonAdminUser" class="text-center">
Logged in as non admin user. Please
<span @click="logout()" class="text-red-500 cursor-pointer underline"
>sign out</span
>
and login with an admin account.
</div>
<div v-else>
<div v-if="mode === 'sign-in'" class="flex flex-col space-y-2">
<HoppSmartItem
:loading="signingInWithGitHub"
:icon="IconGithub"
:label="`Continue with GitHub`"
@click="signInWithGithub"
/>
<HoppSmartItem
:loading="signingInWithGoogle"
:icon="IconGoogle"
:label="`Continue with Google`"
@click="signInWithGoogle"
/>
<HoppSmartItem
:loading="signingInWithMicrosoft"
:icon="IconMicrosoft"
:label="`Continue with Microsoft`"
@click="signInWithMicrosoft"
/>
<HoppSmartItem
:icon="IconEmail"
:label="`Continue with Email`"
@click="mode = 'email'"
/>
</div>
<form
v-if="mode === 'email'"
class="flex flex-col space-y-2"
@submit.prevent="signInWithEmail"
>
<div class="flex flex-col">
<input
id="email"
v-model="form.email"
class="input floating-input"
placeholder=" "
type="email"
name="email"
autocomplete="off"
required
spellcheck="false"
v-focus
autofocus
/>
<label for="email"> Email </label>
</div>
<HoppButtonPrimary
:loading="signingInWithEmail"
type="submit"
:label="`Send magic link`"
/>
</form>
<div v-if="mode === 'email-sent'" class="flex flex-col px-4">
<div class="flex flex-col items-center justify-center max-w-md">
<icon-lucide-inbox class="w-6 h-6 text-accent" />
<h3 class="my-2 text-lg text-center">
We sent a magic link to {{ form.email }}
</h3>
<p class="text-center">
We sent a magic link to {{ form.email }}. Click on the link to sign
in.
</p>
</div>
</div>
<section class="text-center mt-10">
<div v-if="mode === 'sign-in'" class="text-secondaryLight text-tiny">
By signing in, you are agreeing to our
<HoppSmartAnchor
class="link"
to="https://docs.hoppscotch.io/terms"
blank
label="Terms of Service"
/>
and
<HoppSmartAnchor
class="link"
to="https://docs.hoppscotch.io/privacy"
blank
label="Privacy Policy"
/>
</div>
<div v-if="mode === 'email'">
<HoppButtonSecondary
:label="'All sign in option'"
:icon="IconArrowLeft"
class="!p-0"
@click="mode = 'sign-in'"
/>
</div>
<div
v-if="mode === 'email-sent'"
class="flex justify-between flex-1 text-secondaryLight"
>
<HoppSmartAnchor
class="link"
:label="'Re enter email'"
:icon="IconArrowLeft"
@click="mode = 'email'"
/>
</div>
</section>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import IconGithub from '~icons/auth/github';
import IconGoogle from '~icons/auth/google';
import IconEmail from '~icons/auth/email';
import IconMicrosoft from '~icons/auth/microsoft';
import IconArrowLeft from '~icons/lucide/arrow-left';
import { setLocalConfig } from '~/helpers/localpersistence';
import { useStreamSubscriber } from '~/composables/stream';
import { useToast } from '~/composables/toast';
import { auth } from '~/helpers/auth';
const { subscribeToStream } = useStreamSubscriber();
const toast = useToast();
// DATA
const form = ref({
email: '',
});
const signingInWithGoogle = ref(false);
const signingInWithGitHub = ref(false);
const signingInWithMicrosoft = ref(false);
const signingInWithEmail = ref(false);
const mode = ref('sign-in');
const nonAdminUser = ref(false);
onMounted(() => {
const currentUser$ = auth.getCurrentUserStream();
subscribeToStream(currentUser$, (user) => {
if (user && !user.isAdmin) {
nonAdminUser.value = true;
toast.error(`You are logged in. But you're not an admin`);
}
});
});
async function signInWithGoogle() {
signingInWithGoogle.value = true;
try {
await auth.signInUserWithGoogle();
} catch (e) {
console.error(e);
/*
A auth/account-exists-with-different-credential Firebase error wont happen between Google and any other providers
Seems Google account overwrites accounts of other providers https://github.com/firebase/firebase-android-sdk/issues/25
*/
toast.error(`Failed to sign in with Google`);
}
signingInWithGoogle.value = false;
}
async function signInWithGithub() {
signingInWithGitHub.value = true;
try {
await auth.signInUserWithGithub();
} catch (e) {
console.error(e);
/*
A auth/account-exists-with-different-credential Firebase error wont happen between Google and any other providers
Seems Google account overwrites accounts of other providers https://github.com/firebase/firebase-android-sdk/issues/25
*/
toast.error(`Failed to sign in with GitHub`);
}
signingInWithGitHub.value = false;
}
async function signInWithMicrosoft() {
signingInWithMicrosoft.value = true;
try {
await auth.signInUserWithMicrosoft();
} catch (e) {
console.error(e);
/*
A auth/account-exists-with-different-credential Firebase error wont happen between MS with Google or Github
If a Github account exists and user then logs in with MS email we get a "Something went wrong toast" and console errors and MS replaces GH as only provider.
The error messages are as follows:
FirebaseError: Firebase: Error (auth/popup-closed-by-user).
@firebase/auth: Auth (9.6.11): INTERNAL ASSERTION FAILED: Pending promise was never set
They may be related to https://github.com/firebase/firebaseui-web/issues/947
*/
toast.error(`Something went wrong`);
}
signingInWithMicrosoft.value = false;
}
async function signInWithEmail() {
signingInWithEmail.value = true;
await auth
.signInWithEmail(form.value.email)
.then(() => {
mode.value = 'email-sent';
setLocalConfig('emailForSignIn', form.value.email);
})
.catch((e: any) => {
console.error(e);
toast.error(e.message);
signingInWithEmail.value = false;
})
.finally(() => {
signingInWithEmail.value = false;
});
}
const logout = async () => {
try {
await auth.signOutUser();
window.location.reload();
toast.success(`Logged out`);
} catch (e) {
console.error(e);
toast.error(`Something went wrong`);
}
};
</script>

View File

@@ -0,0 +1,62 @@
<template>
<div class="flex" @click="openLogoutModal()">
<HoppSmartItem
:icon="IconLogOut"
:label="'Logout'"
:outline="outline"
:shortcut="shortcut"
@click="openLogoutModal()"
/>
<HoppSmartConfirmModal
:show="confirmLogout"
:title="`Confirm Logout`"
@hide-modal="confirmLogout = false"
@resolve="logout"
/>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import IconLogOut from '~icons/lucide/log-out';
import { useToast } from '~/composables/toast';
import { useRouter } from 'vue-router';
import { auth } from '~/helpers/auth';
const router = useRouter();
defineProps({
outline: {
type: Boolean,
default: false,
},
shortcut: {
type: Array,
default: () => [],
},
});
const emit = defineEmits<{
(e: 'confirm-logout'): void;
}>();
const confirmLogout = ref(false);
const toast = useToast();
const logout = async () => {
try {
await auth.signOutUser();
router.push(`/`);
toast.success(`Logged out`);
} catch (e) {
console.error(e);
toast.error(`Something went wrong`);
}
};
const openLogoutModal = () => {
emit('confirm-logout');
confirmLogout.value = true;
};
</script>

View File

@@ -0,0 +1,87 @@
<template>
<div
tabindex="0"
class="relative flex items-center justify-center cursor-pointer focus:outline-none focus-visible:ring focus-visible:ring-primaryDark"
:class="[`rounded-${rounded}`, `w-${size} h-${size}`]"
>
<img
v-if="url"
class="absolute object-cover object-center transition bg-primaryDark"
:class="[`rounded-${rounded}`, `w-${size} h-${size}`]"
:src="url"
:alt="alt"
loading="lazy"
/>
<div
v-else
class="absolute flex items-center justify-center object-cover object-center transition bg-primaryDark text-accentContrast"
:class="[`rounded-${rounded}`, `w-${size} h-${size}`]"
:style="`background-color: ${initial ? toHex(initial) : '#480000'}`"
>
<template v-if="initial && initial.charAt(0).toUpperCase()">
{{ initial.charAt(0).toUpperCase() }}
</template>
<icon-lucide-user v-else></icon-lucide-user>
</div>
<span
v-if="indicator"
class="border-primary border-2 h-2.5 -top-0.5 -right-0.5 w-2.5 absolute"
:class="[`rounded-${rounded}`, indicatorStyles]"
></span>
<!-- w-5 h-5 rounded-lg -->
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
export default defineComponent({
props: {
url: {
type: String,
default: '',
},
alt: {
type: String,
default: 'Profile picture',
},
indicator: {
type: Boolean,
default: false,
},
indicatorStyles: {
type: String,
default: 'bg-green-500',
},
rounded: {
type: String,
default: 'full',
},
size: {
type: String,
default: '5',
},
initial: {
type: String as PropType<string | undefined | null>,
default: '',
},
},
methods: {
toHex(initial: string) {
let hash = 0;
if (initial.length === 0) return hash;
for (let i = 0; i < initial.length; i++) {
hash = initial.charCodeAt(i) + ((hash << 5) - hash);
hash = hash & hash;
}
let color = '#';
for (let i = 0; i < 3; i++) {
const value = (hash >> (i * 8)) & 255;
color += `00${value.toString(16)}`.slice(-2);
}
return color;
},
},
});
</script>