CLASSROOMIO-T-126: Invite teacher and teacher can login

This commit is contained in:
Rotimi Best
2023-09-01 15:28:47 +01:00
parent 9d0fd56f05
commit 929edea63c
29 changed files with 498 additions and 74 deletions
+15 -8
View File
@@ -29,7 +29,7 @@
import { user, profile } from '$lib/utils/store/user';
import { getSupabase } from '$lib/utils/functions/supabase';
import { isMobile } from '$lib/utils/store/useMobile';
import { ROUTES_TO_HIDE_NAV, ROUTE } from '$lib/utils/constants/routes';
import { ROUTE } from '$lib/utils/constants/routes';
import { getOrganizations, getCurrentOrg } from '$lib/utils/services/org';
import { toggleBodyByTheme } from '$lib/utils/functions/app';
import { appStore } from '$lib/utils/store/app';
@@ -37,6 +37,7 @@
import { setTheme } from '$lib/utils/functions/theme';
import '../app.postcss';
import hideNavByRoute from '$lib/utils/functions/routes/hideNavByRoute';
export let data;
@@ -98,13 +99,13 @@
.from('profile')
.insert({
id: authUser.id,
username: regexUsernameMatch[1] + '1669991321770',
username: regexUsernameMatch[1] + `${new Date().getTime()}`,
fullname: regexUsernameMatch[1],
email: authUser.email
})
.select();
// Profile created, go to profile page
// Profile created, go to onboarding or lms
if (!error && data) {
$user.fetchingUser = false;
$user.isLoggedIn = true;
@@ -141,7 +142,10 @@
return;
}
goto(ROUTE.ONBOARDING);
// On invite page, don't go to onboarding
if (!path.includes('invite')) {
goto(ROUTE.ONBOARDING);
}
}
$user.fetchingUser = false;
} else if (profileData) {
@@ -164,17 +168,20 @@
goto('/lms');
}
} else {
if (isEmpty(orgRes.orgs)) {
// Not on invite page or no org, go to onboarding
if (isEmpty(orgRes.orgs) && !path.includes('invite')) {
goto(ROUTE.ONBOARDING);
} else if (
['login', 'signup', 'onboarding'].some((r) => path.includes(r)) ||
['login', 'signup', 'onboarding', 'invite'].some((r) => path.includes(r)) ||
path === '' // for home page
) {
// By default redirect to first organization
goto(`/org/${orgRes.currentOrg.siteName}`);
}
setTheme(orgRes.currentOrg.theme);
if (typeof orgRes?.currentOrg?.theme === 'string') {
setTheme(orgRes.currentOrg.theme);
}
}
}
}
@@ -282,7 +289,7 @@
<Moon size="60" color="#1d4ed8" unit="px" duration="1s" />
</Backdrop>
{/if}
{#if !ROUTES_TO_HIDE_NAV.includes($page.url.pathname)}
{#if !hideNavByRoute($page.url.pathname)}
{#if isOrgPage($page.url.pathname) || $page.url.pathname.includes('profile') || isCoursesPage(path)}
<OrgNavigation bind:title={$course.title} isCoursePage={isCoursesPage(path)} />
{:else if isLMSPage($page.url.pathname)}
+68
View File
@@ -0,0 +1,68 @@
import { render } from 'svelte-email';
import { json } from '@sveltejs/kit';
import TeacherInviteTemplate from './template.svelte';
import { getSendgrid, SENDGRID_FROM_HELP } from '$lib/utils/services/sendgrid';
import { getSupabase } from '$lib/utils/functions/supabase';
const sendgrid = getSendgrid();
const supabase = getSupabase();
// API to send invite to teacher
export async function POST({ request }) {
const body = await request.json();
const { org, email } = body;
const accessToken = request.headers.get('Authorization');
console.log('/POST api/email/invite', body);
if (!org || !Object.keys(org) || !email) {
return json(
{ success: false, message: 'Org data and Teacher name are required' },
{ status: 400 }
);
}
let user;
try {
const { data } = await supabase.auth.getUser(accessToken);
user = data.user;
} catch (error) {
console.error(error);
}
console.log('user', user);
if (!user) {
return json({ success: false, message: 'Unauthenticated user' }, { status: 401 });
}
const { id, name, siteName } = org;
const origin = request.headers.get('origin');
const inviteData = JSON.stringify({
email,
orgId: id,
orgSiteName: siteName
});
const inviteLink = `${origin}/invite/${encodeURIComponent(btoa(inviteData))}`;
console.log('inviteData', inviteData);
const options = {
from: SENDGRID_FROM_HELP,
to: email,
subject: `Join ${name} on ClassroomIO`,
html: render({
template: TeacherInviteTemplate,
props: {
orgName: name,
inviteLink
}
})
};
sendgrid.send(options);
return json({
success: true,
message: 'Email sent'
});
}
@@ -0,0 +1,57 @@
<script lang="ts">
import { Text, Section, Button, Link } from 'svelte-email';
export let orgName = '';
export let inviteLink = '';
const fontFamily =
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif';
const paragraph = {
fontFamily,
fontSize: '16px',
lineHeight: '26px'
};
const paragraphContent = {
...paragraph,
margin: '20px 0'
};
const footerParagraph = {
...paragraph,
margin: '0px'
};
const btnContainer = {
textAlign: 'center' as const
};
const button = {
fontFamily,
backgroundColor: '#1d4ee2',
borderRadius: '5px',
color: '#fff',
fontSize: '16px',
textDecoration: 'none',
textAlign: 'center' as const,
display: 'block'
};
const linkStyle = {
color: '#1d4ee2'
};
</script>
<Text style={paragraph}>Hey there,</Text>
<Text style={paragraphContent}>
You have been invited to join {orgName} on ClassroomIO.
</Text>
<Section style={btnContainer}>
<Button pX={12} pY={12} style={button} href={inviteLink}>Accept Invitation</Button>
</Section>
<Text style={footerParagraph}>Best regards</Text>
<Text style={footerParagraph}>Rotimi Best</Text>
<Text style={footerParagraph}
><Link style={linkStyle} href="https://classroomio.com">ClassroomIO</Link></Text
>
@@ -0,0 +1,15 @@
<script lang="ts">
import EmailTemplate from '$lib/components/Email/EmailTemplate.svelte';
import Invitation from './invitation.svelte';
export let orgName = '';
export let inviteLink = '';
</script>
<EmailTemplate
children={Invitation}
props={{
orgName,
inviteLink
}}
/>
-7
View File
@@ -1,7 +0,0 @@
import { json } from '@sveltejs/kit';
export async function GET({ url }) {
// Get org data from request
// Get teacher email
// Generate
}
+4 -1
View File
@@ -109,7 +109,10 @@
isStudent = user.role_id === 3;
}
}
$: $exerciseMode.editMode = !isStudent;
$: {
console.log('isStudent', isStudent);
$exerciseMode.editMode = !isStudent;
}
</script>
<CourseContainer bind:isStudent>
@@ -0,0 +1,24 @@
import { redirect } from '@sveltejs/kit';
import { getCurrentOrg } from '$lib/utils/services/org';
import { getSupabase } from '$lib/utils/functions/supabase';
export const load = async ({ params = { hash: '' } }) => {
try {
const hashData = atob(decodeURIComponent(params.hash));
console.log('hashData', hashData);
const { orgId, email, orgSiteName } = JSON.parse(hashData);
getSupabase();
const _currentOrg = await getCurrentOrg(orgSiteName, true);
return {
orgId,
email,
currentOrg: _currentOrg
};
} catch (error) {
console.error('Error decoding invite params.hash', error);
throw redirect(301, '/404');
}
};
+194
View File
@@ -0,0 +1,194 @@
<script lang="ts">
import { goto } from '$app/navigation';
import TextField from '$lib/components/Form/TextField.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { getSupabase } from '$lib/utils/functions/supabase';
import {
authValidation,
getConfirmPasswordError,
getDisableSubmit
} from '$lib/utils/functions/validator';
import { SIGNUP_FIELDS } from '$lib/utils/constants/authentication';
import AuthUI from '$lib/components/AuthUI/index.svelte';
import { currentOrg } from '$lib/utils/store/org';
import { setTheme } from '$lib/utils/functions/theme';
import type { CurrentOrg } from '$lib/utils/types/org.js';
import { onMount } from 'svelte';
import Error from '../../+error.svelte';
export let data;
let supabase = getSupabase();
let fields = Object.assign({}, SIGNUP_FIELDS);
let loading = false;
let errors: {
name?: string;
email?: string;
password?: string;
confirmPassword?: string;
} = {};
let submitError: string;
let disableSubmit = false;
let formRef: HTMLFormElement;
async function handleSubmit() {
const validationRes = authValidation(fields);
console.log('validationRes', validationRes);
if (Object.keys(validationRes).length) {
errors = Object.assign(errors, validationRes);
return;
}
try {
loading = true;
// Signup
const { data: signupData, error } = await supabase.auth.signUp({
email: data.email,
password: fields.password
});
console.log('Signup', signupData);
if (error) throw error;
if (!signupData.user) {
throw 'User cannot be created ';
}
// Insert profile
const userId = signupData.user.id;
const profileRes = await supabase
.from('profile')
.insert({
id: userId,
username: fields.name.toLowerCase().replace(' ', '-') + new Date().getTime(),
fullname: fields.name,
email: data.email
})
.select();
console.log('Insert profile', profileRes.data);
if (profileRes.error) {
throw profileRes.error;
}
const profile = profileRes.data[0] || {};
if (!profile.id) {
throw 'Unable to create profile';
}
// Update member response
const updateMemberRes = await supabase
.from('organizationmember')
.update({
verified: true,
profile_id: profile.id
})
.match({ email: data.email, organization_id: data.currentOrg?.id });
console.log('Update member response', updateMemberRes);
formRef?.reset();
return goto('/login');
} catch (error) {
if (error instanceof Error) {
submitError = error.message;
}
} finally {
loading = false;
}
}
function setCurOrg(cOrg: CurrentOrg) {
if (!cOrg) return;
currentOrg.set(cOrg);
}
onMount(() => {
setTheme(data.currentOrg?.theme || '');
});
$: errors.confirmPassword = getConfirmPasswordError(fields);
$: disableSubmit = getDisableSubmit(fields);
$: setCurOrg(data.currentOrg as CurrentOrg);
</script>
<svelte:head>
<title>Join ClassroomIO</title>
</svelte:head>
<AuthUI
{supabase}
isLogin={false}
{handleSubmit}
isLoading={loading}
showOnlyContent={true}
showLogo={true}
bind:formRef
>
<div class="mt-4 w-full">
<p class="dark:text-white text-lg font-semibold mb-6">Create a free account</p>
<TextField
label="Your Email"
value={data.email}
type="email"
placeholder="you@domain.com"
className="mb-6"
inputClassName="w-full"
isDisabled={true}
/>
<TextField
label="Full Name"
bind:value={fields.name}
type="text"
autoFocus={true}
placeholder="e.g Joke Silva"
className="mb-6"
inputClassName="w-full"
isDisabled={loading}
errorMessage={errors.name}
isRequired
/>
<TextField
label="Your Password"
bind:value={fields.password}
type="password"
placeholder="************"
className="mb-6"
inputClassName="w-full"
isDisabled={loading}
errorMessage={errors.password}
helperMessage="Password must be more than 6 characters"
isRequired
/>
<TextField
label="Confirm Password"
bind:value={fields.confirmPassword}
type="password"
placeholder="************"
className="mb-6"
inputClassName="w-full"
isDisabled={loading}
errorMessage={errors.confirmPassword}
isRequired
/>
{#if submitError}
<p class="text-sm text-red-500">{submitError}</p>
{/if}
</div>
<div class="my-4 w-full flex justify-end items-center">
<PrimaryButton
label="Create and Join"
type="submit"
className="sm:w-full w-full"
isDisabled={disableSubmit || loading}
isLoading={loading}
/>
</div>
</AuthUI>
+5
View File
@@ -11,6 +11,7 @@
import { welcomeModalStore } from '$lib/components/WelcomeModal/store';
import { getOrganizations } from '$lib/utils/services/org';
import { sendWelcomeEmail } from './utils';
import { currentOrg } from '$lib/utils/store/org';
interface OnboardingField {
fullname?: string;
@@ -200,6 +201,10 @@
?.replace(/\s+/g, '-')
?.replace(/[^a-zA-Z0-9-]/g, '');
$: setOrgSiteName(fields.orgName, isSiteNameTouched);
$: {
console.log('currentOrg', $currentOrg);
}
</script>
{#if $profile.id}
+1 -1
View File
@@ -8,7 +8,7 @@
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { courses, createCourseModal, courseMetaDeta } from '$lib/components/Courses/store';
import { setProfileIdOfGroupMember } from '$lib/utils/services/courses';
import { currentOrg } from '$lib/utils/store/org.js';
import { currentOrg } from '$lib/utils/store/org';
export let data;
let { allCourses, cantFetch } = data;