Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(auth): revamp auth #204

Merged
merged 8 commits into from
Dec 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions app/auth.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { Admin } from '~~/server/db/schema';

declare module '#auth-utils' {
interface User extends Admin {}
}

export {};
26 changes: 0 additions & 26 deletions app/components/HandleFlexStrip.vue

This file was deleted.

6 changes: 3 additions & 3 deletions app/components/Onboarding/KeyInput.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
const emits = defineEmits<{
done: [string];
done: [string, boolean];
}>();

const key = ref('');
Expand All @@ -11,12 +11,12 @@ const verify = async () => {
if (!key.value) return;
try {
isVerifying.value = true;
const response = await $fetch<{ result: boolean }>('/api/onboarding/validate-key', {
const response = await $fetch<{ result: boolean; registrationNeeded: boolean }>('/api/onboarding/validate-key', {
method: 'POST',
body: { key: key.value },
});
if (response.result) {
emits('done', key.value);
emits('done', key.value, response.registrationNeeded);
} else {
error.value = 'Invalid Key. Try again.';
}
Expand Down
85 changes: 85 additions & 0 deletions app/components/Onboarding/Register.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<script setup lang="ts">
import { registerSchema, loginSchema } from '~~/shared/schemas/authentication';

const props = defineProps<{
register: boolean;
onboardingKey: string;
}>();

const formSchema = toTypedSchema(props.register ? registerSchema : loginSchema);
const { handleSubmit, errors, defineField } = useForm({
validationSchema: formSchema,
});

const loginError = ref<string>();

// @ts-expect-error
const [firstName] = defineField('firstName');
// @ts-expect-error
const [lastName] = defineField('lastName');
const [email] = defineField('email');
const [password] = defineField('password');

const emits = defineEmits<{
done: [];
}>();

const submit = handleSubmit(async (validatedData) => {
try {
await $fetch(`/api/${props.register ? 'register' : 'login'}`, {
method: 'POST',
body: validatedData,
headers: {
'x-onboarding-token': props.onboardingKey,
},
});
emits('done');
} catch (e: any) {
if (e.data.statusCode === 401) {
loginError.value = e.data.message;
} else {
loginError.value = 'Something went wrong.';
}
}
});
</script>

<template>
<div class="flex flex-col space-y-2">
<div class="flex flex-col justify-center">
<InputLabel class="text-center" label-class="text-xl" label="Personal Details" v-if="register" />
<InputLabel class="text-center" label-class="text-xl" label="Log back in" v-else />
<InputLabel
class="text-center"
label-class="!text-zinc-500"
label="Registering as super admin for this instance."
v-if="register"
/>
<InputLabel
class="text-center"
label-class="!text-zinc-500"
label="Super admin already registered, log back in."
v-else
/>
</div>
<div class="flex space-x-2 w-full" v-if="register">
<!-- @vue-expect-error -->
<InputText class="w-1/2" placeholder="First Name" v-model="firstName" :error="errors['firstName']" />
<!-- @vue-expect-error -->
<InputText class="w-1/2" placeholder="Last Name" v-model="lastName" :error="errors['lastName']" />
</div>
<InputText class="w-full" placeholder="Email" v-model="email" :error="errors['email']" />

<InputText
class="w-full"
placeholder="Password"
type-override="password"
v-model="password"
:error="errors['password']"
/>
<InputButton @click="submit">
{{ register ? 'Save' : 'Continue' }}
</InputButton>
<InputLabel :error="loginError" v-if="loginError" />
</div>
</template>
33 changes: 8 additions & 25 deletions app/components/admin/ApplicationRow.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,46 +4,29 @@ const props = defineProps<{
application: Application;
}>();

const user = props.applicant.user;
const resume = props.applicant.handles.find((h) => h.key == 'resume')?.value;
const isResumeAvailable = Boolean(resume);
const candidate = props.applicant.candidate;
</script>

<template>
<div
class="flex bg-white border border-zinc-200 rounded-xl items-center justify-between py-2 px-4"
v-if="props.applicant && user"
v-if="props.applicant && candidate"
>
<div class="flex">
<div class="flex items-center">
<div class="w-10 h-10 mr-2">
<img
class="rounded-xl"
:src="user.picture || undefined"
width="36"
height="36"
:alt="`${user.firstName}'s Profile Picture'`"
/>
</div>
<div class="font-medium text-zinc-800">
<span class="font-bold">{{ user.firstName + ' ' + user.lastName }}</span
><br />
<span class="text-zinc-700">{{ user.email }}</span>
</div>
<div class="font-medium text-zinc-800">
<span class="font-bold">
{{ candidate.firstName + ' ' + candidate.lastName }}
</span>
<br />
<span class="text-zinc-700">{{ candidate.email }}</span>
</div>
</div>
<div class="flex items-center justify-between space-x-20">
<div class="whitespace-nowrap">
<HandleFlexStrip :handles="applicant.handles" />
</div>
<div class="whitespace-nowrap">
<div class="text-left">
{{ timeAgo(new Date(application.createdAt)) }}
</div>
</div>
<InputButton variant="secondary" as="a" target="_blank" :href="resume" :disabled="!isResumeAvailable">
View Resume
</InputButton>
</div>
</div>
</template>
6 changes: 3 additions & 3 deletions app/components/admin/MemberCard.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<script setup lang="ts">
import type { User } from '~~/server/db/schema';
import type { Admin } from '~~/server/db/schema';

const props = defineProps<{
member: User;
member: Admin;
}>();

const { deleteData } = await useMembersRepository();
const { profile } = useAuth();
const { user: profile } = useUserSession();

const onRemove = () => {
deleteData({ id: props.member.id });
Expand Down
46 changes: 25 additions & 21 deletions app/components/admin/MembersAddAction.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<script setup lang="ts">
import type { User } from '~~/server/db/schema';
import type { Admin } from '~~/server/db/schema';
import { watchDebounced } from '@vueuse/core';

const { postData, changing } = await useMembersRepository();

const suggestedUsers = ref<User[]>();
const suggestedAdmins = ref<Admin[]>();

const submit = async (id: string, closeFn: () => void) => {
let success = false;
Expand All @@ -16,23 +16,23 @@ const submit = async (id: string, closeFn: () => void) => {
}
};

const userSearchQuery = ref<string>('');
const fetchingUserSuggestions = ref(false);
const adminSearchQuery = ref<string>('');
const fetchingAdminSuggestions = ref(false);

watchDebounced(
userSearchQuery,
adminSearchQuery,
async (q) => {
try {
fetchingUserSuggestions.value = true;
suggestedUsers.value = await $fetch<User[]>('/api/user/lookup', {
fetchingAdminSuggestions.value = true;
suggestedAdmins.value = await $fetch<Admin[]>('/api/admin/lookup', {
query: {
q,
},
});
} catch (error) {
console.error('error fetching user suggestions', error);
console.error('error fetching admin suggestions', error);
} finally {
fetchingUserSuggestions.value = false;
fetchingAdminSuggestions.value = false;
}
},
{ debounce: 500, maxWait: 1000 }
Expand All @@ -52,35 +52,39 @@ watchDebounced(
type="text"
class="input-custom"
placeholder="Start searching to add members"
v-model="userSearchQuery"
v-model="adminSearchQuery"
:disabled="changing"
/>
<div
class="flex flex-col space-y-3 overflow-y-scroll no-scrollbar h-64 mt-3"
v-if="suggestedUsers && suggestedUsers.length > 0"
v-if="suggestedAdmins && suggestedAdmins.length > 0"
>
<div class="flex w-full justify-between p-2 border rounded-lg" v-for="user in suggestedUsers" :key="user.id">
<div
class="flex w-full justify-between p-2 border rounded-lg"
v-for="admin in suggestedAdmins"
:key="admin.id"
>
<div class="flex space-x-1 items-center">
<img :src="user.picture" v-if="user.picture" class="w-10 h-10 rounded-full" />
<img :src="admin.picture" v-if="admin.picture" class="w-10 h-10 rounded-full" />
<div class="flex flex-col text-sm">
<span class="font-bold">{{ user.firstName }} {{ user.lastName }}</span>
<span>{{ user.email }}</span>
<span class="font-bold">{{ admin.firstName }} {{ admin.lastName }}</span>
<span>{{ admin.email }}</span>
</div>
</div>
<InputButton size="sm" variant="outline" @click="submit(user.id, close)" :loading="changing"
<InputButton size="sm" variant="outline" @click="submit(admin.id, close)" :loading="changing"
>Add</InputButton
>
</div>
</div>
<div class="h-64 flex w-full justify-center items-center" v-else>
<span class="text-center" v-if="userSearchQuery && !fetchingUserSuggestions">
<span class="font-bold">No User Found</span><br />
<span class="text-xs">User has to login atleast once to be added.</span>
<span class="text-center" v-if="adminSearchQuery && !fetchingAdminSuggestions">
<span class="font-bold">No Admin Found</span><br />
<span class="text-xs">Admin has to login atleast once to be added.</span>
</span>
<span class="text-center" v-else-if="!userSearchQuery">
<span class="text-center" v-else-if="!adminSearchQuery">
<span class="font-bold">Start typing to see suggestions.</span><br />
<span class="text-xs">
Any user added will be able to see / create job postings and see / manage all the applicants.
Any admin added will be able to see / create job postings and see / manage all the applicants.
</span>
</span>
</div>
Expand Down
25 changes: 25 additions & 0 deletions app/components/admin/ReviewTag/Picker.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<script setup lang="ts">
import { useVModel } from '@vueuse/core';
import type { SelectableOption } from '~~/shared/types/general';

const { data: reviewTags } = await useReviewTagsRepository();

const options: SelectableOption[] = reviewTagGroupVsIndex
.flatMap((_, i) => reviewTags.value.filter((rt) => rt.parent === i))
.map((rt) => ({
id: rt.id,
title: rt.title,
logo: 'material-symbols:circle',
logoClass: `text-${reviewTagColorVsIndex[rt.parent]}-200 rounded-full border border-${reviewTagColorVsIndex[rt.parent]}-600`,
}));

const props = defineProps<{
modelValue?: number;
}>();

const selectedTag = useVModel(props, 'modelValue');
</script>

<template>
<AbstractDropdownSingleSelect :options title="Select Tag" v-model="selectedTag" />
</template>
Loading
Loading