Compare commits

2 Commits

Author SHA1 Message Date
643d74e29d feat: (auth) switch to OTP code via e-mail
All checks were successful
Build BAB Application Deployment Artifact / build (push) Successful in 2m24s
2024-06-14 16:23:48 -04:00
1526a10630 feat: (auth) Add ability to signup with e-mail 2024-06-14 15:19:29 -04:00
8 changed files with 292 additions and 128 deletions

View File

@@ -14,7 +14,6 @@ import type { Router } from 'vue-router';
const client = new Client();
console.log(import.meta.env);
const VITE_APPWRITE_API_ENDPOINT = import.meta.env.VITE_APPWRITE_API_ENDPOINT;
const VITE_APPWRITE_API_PROJECT = import.meta.env.VITE_APPWRITE_API_PROJECT;
@@ -28,7 +27,6 @@ if (VITE_APPWRITE_API_ENDPOINT && VITE_APPWRITE_API_PROJECT) {
);
}
console.log(process.env);
const pwresetUrl = process.env.DEV
? 'http://localhost:4000/pwreset'
: 'https://oys.undock.ca/pwreset';

View File

@@ -0,0 +1,62 @@
<template>
<q-card-section class="q-ma-sm">
<q-input
v-model="password"
label="New Password"
type="password"
color="darkblue"
:rules="[validatePasswordStrength]"
lazy-rules
filled></q-input>
<q-input
v-model="confirmPassword"
label="Confirm New Password"
type="password"
color="darkblue"
:rules="[validatePasswordStrength]"
lazy-rules
filled></q-input>
<div class="text-caption q-py-md">Enter a new password.</div>
</q-card-section>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
const password = ref('');
const confirmPassword = ref('');
const newPassword = defineModel();
const validatePasswordStrength = (val: string) => {
const hasUpperCase = /[A-Z]/.test(val);
const hasLowerCase = /[a-z]/.test(val);
const hasNumbers = /[0-9]/.test(val);
const hasNonAlphas = /[\W_]/.test(val);
const isValidLength = val.length >= 8;
return (
(hasUpperCase &&
hasLowerCase &&
hasNumbers &&
hasNonAlphas &&
isValidLength) ||
'Password must be at least 8 characters long and include uppercase, lowercase, number, and special character.'
);
};
const validatePasswordsMatch = (val: string) => {
return val === password.value || 'Passwords do not match.';
};
watch([password, confirmPassword], ([newpw, newpw1]) => {
if (
validatePasswordStrength(newpw) === true &&
validatePasswordsMatch(newpw1) === true
) {
newPassword.value = newpw;
} else {
newPassword.value = '';
}
});
</script>

View File

@@ -14,7 +14,7 @@
<div class="col text-h6">Log in</div>
</div>
</q-card-section>
<q-form>
<q-form @keydown.enter.prevent="doTokenLogin">
<q-card-section class="q-gutter-md">
<q-input
v-model="email"
@@ -23,41 +23,36 @@
color="darkblue"
filled></q-input>
<q-input
v-model="password"
label="Password"
type="password"
v-if="userId"
v-model="token"
label="6-digit code"
type="number"
color="darkblue"
filled></q-input>
<q-card-actions>
<q-btn
type="button"
@click="doLogin"
label="Login"
color="primary"></q-btn>
<q-space />
<q-btn
flat
color="secondary"
to="/pwreset">
Reset password
</q-btn>
<!-- <q-btn
type="button"
@click="register"
color="secondary"
label="Register"
flat
></q-btn> -->
</q-card-actions>
</q-card-section>
</q-form>
<q-card-section>
<q-card-section class="q-pa-none">
<div class="row justify-center q-ma-sm">
<q-btn
type="button"
@click="doTokenLogin"
color="primary"
label="Login with E-mail"
style="width: 300px" />
</div>
<div class="row justify-center q-ma-sm">
<GoogleOauthComponent />
</div>
<div class="row justify-center q-ma-sm">
<DiscordOauthComponent />
</div>
<div class="row justify-center">
<q-btn
flat
color="secondary"
to="/pwreset"
label="Forgot Password?" />
</div>
</q-card-section>
</q-card>
</q-page>
@@ -82,16 +77,75 @@
<script setup lang="ts">
import { ref } from 'vue';
import { login } from 'boot/appwrite';
import GoogleOauthComponent from 'src/components/GoogleOauthComponent.vue';
import DiscordOauthComponent from 'src/components/DiscordOauthComponent.vue';
import { Dialog, Notify } from 'quasar';
import { useAuthStore } from 'src/stores/auth';
import { useRouter } from 'vue-router';
import { AppwriteException } from 'appwrite';
const email = ref('');
const password = ref('');
const token = ref('');
const userId = ref();
console.log('version:' + process.env.VUE_APP_VERSION);
const doLogin = async () => {
login(email.value, password.value);
const doTokenLogin = async () => {
const authStore = useAuthStore();
if (!userId.value) {
try {
const sessionToken = await authStore.createTokenSession(email.value);
userId.value = sessionToken.userId;
Dialog.create({ message: 'Check your e-mail for your login code.' });
} catch (e) {
Dialog.create({
message: 'An error occurred. Please ask for help in Discord',
});
}
} else {
const notification = Notify.create({
type: 'primary',
position: 'top',
spinner: true,
message: 'Logging you in...',
timeout: 8000,
group: false,
});
try {
await authStore.tokenLogin(userId.value, token.value);
notification({
type: 'positive',
message: 'Logged in!',
timeout: 2000,
spinner: false,
icon: 'check_circle',
});
useRouter().replace({ name: 'index' });
} catch (error: unknown) {
if (error instanceof AppwriteException) {
if (error.type === 'user_session_already_exists') {
useRouter().replace({ name: 'index' });
notification({
type: 'positive',
message: 'Already Logged in!',
timeout: 2000,
spinner: false,
icon: 'check_circle',
});
return;
}
Dialog.create({
title: 'Login Error!',
message: error.message,
persistent: true,
});
}
notification({
type: 'negative',
message: 'Login failed.',
timeout: 2000,
});
}
}
};
</script>

View File

@@ -34,58 +34,27 @@
@click="resetPw"
label="Send Reset Link"
color="primary"></q-btn>
<!-- <q-btn
type="button"
@click="register"
color="secondary"
label="Register"
flat
></q-btn> -->
</q-card-actions>
</q-card-section>
</q-form>
<q-form
@submit="submitNewPw"
v-else-if="validResetLink()">
<q-card-section class="q-ma-sm">
<q-input
v-model="password"
label="New Password"
type="password"
color="darkblue"
:rules="[validatePasswordStrength]"
lazy-rules
filled></q-input>
<q-input
v-model="confirmPassword"
label="Confirm New Password"
type="password"
color="darkblue"
:rules="[validatePasswordStrength]"
lazy-rules
filled></q-input>
<div class="text-caption q-py-md">Enter a new password.</div>
</q-card-section>
<q-card-actions>
<q-btn
type="submit"
label="Reset Password"
color="primary"></q-btn>
<!-- <q-btn
type="button"
@click="register"
color="secondary"
label="Register"
flat
></q-btn> -->
</q-card-actions>
</q-form>
<div v-else-if="validResetLink()">
<q-form
@submit="submitNewPw"
@keydown.enter.prevent="resetPw">
<NewPasswordComponent v-model="newPassword" />
<q-card-actions>
<q-btn
type="submit"
label="Reset Password"
color="primary"></q-btn>
</q-card-actions>
</q-form>
</div>
<q-card
v-else
class="text-center">
<span class="text-h5">Invalid reset link.</span>
</q-card>
<!-- <q-card-section><GoogleOauthComponent /></q-card-section> -->
</q-card>
</q-page>
</q-page-container>
@@ -112,38 +81,11 @@ import { ref } from 'vue';
import { account, resetPassword } from 'boot/appwrite';
import { useRouter } from 'vue-router';
import { Dialog } from 'quasar';
// import GoogleOauthComponent from 'src/components/GoogleOauthComponent.vue';
import NewPasswordComponent from 'components/NewPasswordComponent.vue';
const email = ref('');
const router = useRouter();
const password = ref('');
const confirmPassword = ref('');
const validatePasswordStrength = (val: string) => {
const hasUpperCase = /[A-Z]/.test(val);
const hasLowerCase = /[a-z]/.test(val);
const hasNumbers = /[0-9]/.test(val);
const hasNonAlphas = /[\W_]/.test(val);
const isValidLength = val.length >= 8;
return (
(hasUpperCase &&
hasLowerCase &&
hasNumbers &&
hasNonAlphas &&
isValidLength) ||
'Password must be at least 8 characters long and include uppercase, lowercase, number, and special character.'
);
};
const validatePasswordsMatch = (val: string) => {
return val === password.value || 'Passwords do not match.';
};
function isPasswordResetLink() {
const query = router.currentRoute.value.query;
return query && query.secret && query.userId && query.expire;
}
const newPassword = ref();
function validResetLink(): boolean {
const query = router.currentRoute.value.query;
@@ -153,27 +95,34 @@ function validResetLink(): boolean {
);
}
function isPasswordResetLink() {
const query = router.currentRoute.value.query;
return query && query.secret && query.userId && query.expire;
}
function submitNewPw() {
const query = router.currentRoute.value.query;
if (
validatePasswordStrength(password.value) === true &&
validatePasswordsMatch(confirmPassword.value) === true
) {
if (newPassword.value) {
account
.updateRecovery(
query.userId as string,
query.secret as string,
password.value
newPassword.value
)
.then(() => {
Dialog.create({ message: 'Password Changed!' });
router.replace('/login');
Dialog.create({ message: 'Password Changed!' }).onOk(() =>
router.replace('/login')
);
})
.catch((e) =>
Dialog.create({
message: 'Password change failed! Error: ' + e.message,
})
);
} else {
Dialog.create({
message: 'Invalid password. Try again',
});
}
}

86
src/pages/SignupPage.vue Normal file
View File

@@ -0,0 +1,86 @@
<template>
<q-layout>
<q-page-container>
<q-page class="flex bg-image flex-center">
<q-card
v-bind:style="$q.screen.lt.sm ? { width: '80%' } : { width: '30%' }">
<q-card-section>
<q-img
fit="scale-down"
src="~assets/oysqn_logo.png" />
</q-card-section>
<q-card-section>
<div class="text-center q-pt-sm">
<div class="col text-h6">Sign Up</div>
</div>
</q-card-section>
<q-form>
<q-card-section class="q-gutter-md">
<q-input
v-model="email"
label="E-Mail"
type="email"
color="darkblue"
:rules="['email']"
filled></q-input>
<NewPasswordComponent v-model="password" />
<q-card-actions>
<q-space />
<q-btn
type="button"
@click="doRegister"
label="Sign Up"
color="primary"></q-btn>
</q-card-actions>
</q-card-section>
</q-form>
</q-card>
</q-page>
</q-page-container>
</q-layout>
</template>
<style>
.bg-image {
background-image: url('/src/assets/oys_lighthouse.jpg');
background-repeat: no-repeat;
background-position-x: center;
background-size: cover;
/* background-image: linear-gradient(
135deg,
#ed232a 0%,
#ffffff 75%,
#14539a 100%
); */
}
</style>
<script setup lang="ts">
import { ref } from 'vue';
import { useAuthStore } from 'src/stores/auth';
import NewPasswordComponent from 'src/components/NewPasswordComponent.vue';
import { Dialog } from 'quasar';
import { useRouter } from 'vue-router';
const email = ref('');
const password = ref('');
const router = useRouter();
console.log('version:' + process.env.VUE_APP_VERSION);
const doRegister = async () => {
if (email.value && password.value) {
try {
await useAuthStore().register(email.value, password.value);
Dialog.create({
message: 'Account Created! Now log-in with your e-mail / password.',
}).onOk(() => router.replace('/login'));
} catch (e) {
console.log(e);
Dialog.create({
message: 'An error occurred. Please ask for support in Discord',
});
}
}
};
</script>

View File

@@ -49,6 +49,10 @@ export default route(function (/* { store, ssrContext } */) {
return next('/login');
}
if (to.name === 'login' && currentUser) {
return next('/');
}
if (requiredRoles) {
if (!currentUser) {
return next('/login');

View File

@@ -168,14 +168,14 @@ const routes: RouteRecordRaw[] = [
publicRoute: true,
},
},
// {
// path: '/register',
// component: () => import('pages/RegisterPage.vue'),
// name: 'register'
// meta: {
// accountRoute: true,
// }
// },
{
path: '/signup',
component: () => import('pages/SignupPage.vue'),
name: 'signup',
meta: {
publicRoute: true,
},
},
// Always leave this as last one,
// but you can also remove it
{

View File

@@ -45,22 +45,31 @@ export const useAuthStore = defineStore('auth', () => {
await init();
}
async function createTokenSession(email: string) {
return await account.createEmailToken(ID.unique(), email);
}
async function googleLogin() {
account.createOAuth2Session(
await account.createOAuth2Session(
OAuthProvider.Google,
'https://undock.ca',
'https://undock.ca/#/login'
'https://oys.undock.ca',
'https://oys.undock.ca/login'
);
currentUser.value = await account.get();
await init();
}
async function discordLogin() {
account.createOAuth2Session(
await account.createOAuth2Session(
OAuthProvider.Discord,
'https://undock.ca',
'https://undock.ca/#/login'
'https://oys.undock.ca',
'https://oys.undock.ca/login'
);
currentUser.value = await account.get();
await init();
}
async function tokenLogin(userId: string, token: string) {
await account.createSession(userId, token);
await init();
}
function getUserNameById(id: string | undefined | null): string {
@@ -102,6 +111,8 @@ export const useAuthStore = defineStore('auth', () => {
login,
googleLogin,
discordLogin,
createTokenSession,
tokenLogin,
logout,
init,
};