This commit is contained in:
Julien Nahum 2023-12-14 16:53:05 +01:00
parent 5c4dc2a3d6
commit a3a9254665
24 changed files with 445 additions and 122 deletions

View File

@ -33,7 +33,7 @@ export function useFormInput (props, context, formPrefixKey = null) {
}) })
const hasError = computed(() => { const hasError = computed(() => {
return hasValidation && props.form?.errors?.has(name) return hasValidation && props.form?.errors?.has(props.name)
}) })
const compVal = computed({ const compVal = computed({

View File

@ -1,5 +1,5 @@
<template> <template>
<portal to="modals" :order="portalOrder"> <Teleport to="body">
<transition @leave="(el,done) => motions.backdrop.leave(done)"> <transition @leave="(el,done) => motions.backdrop.leave(done)">
<div v-if="show" v-motion="'backdrop'" :variants="motionFadeIn" <div v-if="show" v-motion="'backdrop'" :variants="motionFadeIn"
class="fixed z-30 top-0 inset-0 px-4 sm:px-0 flex items-top justify-center bg-gray-700/75 w-full h-screen overflow-y-scroll" class="fixed z-30 top-0 inset-0 px-4 sm:px-0 flex items-top justify-center bg-gray-700/75 w-full h-screen overflow-y-scroll"
@ -46,7 +46,7 @@
</div> </div>
</div> </div>
</transition> </transition>
</portal> </Teleport>
</template> </template>
<script> <script>
@ -71,9 +71,6 @@ export default {
}, },
closeable: { closeable: {
default: true default: true
},
portalOrder: {
default: 1
} }
}, },

View File

@ -279,7 +279,7 @@ export default {
] ]
}, },
init () { init () {
if (this.$route.name === 'forms.create' || this.$route.name === 'forms-create-guest') { // Set Default fields if (this.$route.name === 'forms-create' || this.$route.name === 'forms-create-guest') { // Set Default fields
this.formFields = (this.form.properties.length > 0) ? clonedeep(this.form.properties) : this.getDefaultFields() this.formFields = (this.form.properties.length > 0) ? clonedeep(this.form.properties) : this.getDefaultFields()
} else { } else {
this.formFields = clonedeep(this.form.properties).map((field) => { this.formFields = clonedeep(this.form.properties).map((field) => {

View File

@ -41,19 +41,12 @@
</template> </template>
<script> <script>
import { computed } from 'vue'
import Form from 'vform'
import Cookies from 'js-cookie' import Cookies from 'js-cookie'
import { useAuthStore } from '../../../../stores/auth.js'
import OpenFormFooter from '../../OpenFormFooter.vue'
import Testimonials from '../../welcome/Testimonials.vue'
import ForgotPasswordModal from '../ForgotPasswordModal.vue' import ForgotPasswordModal from '../ForgotPasswordModal.vue'
export default { export default {
name: 'LoginForm', name: 'LoginForm',
components: { components: {
OpenFormFooter,
Testimonials,
ForgotPasswordModal ForgotPasswordModal
}, },
props: { props: {
@ -72,7 +65,7 @@ export default {
}, },
data: () => ({ data: () => ({
form: new Form({ form: useForm({
email: '', email: '',
password: '' password: ''
}), }),
@ -83,10 +76,10 @@ export default {
methods: { methods: {
async login () { async login () {
// Submit the form. // Submit the form.
const { data } = await this.form.post('/api/login') const data = await this.form.post('login')
// Save the token. // Save the token.
this.authStore.saveToken(data.token, this.remember) this.authStore.setToken(data.token, this.remember)
// Fetch the user. // Fetch the user.
await this.authStore.fetchUser() await this.authStore.fetchUser()

View File

@ -142,7 +142,7 @@ export default {
if (this.isQuick) { if (this.isQuick) {
this.$emit('afterQuickLogin') this.$emit('afterQuickLogin')
} else { } else {
this.$router.push({ name: 'forms.create' }) this.$router.push({ name: 'forms-create' })
} }
} }
} }

View File

@ -67,7 +67,7 @@
<!-- <v-button v-if="!authenticated" class="mr-2 block" :to="{ name: 'forms-create-guest' }" :arrow="true">--> <!-- <v-button v-if="!authenticated" class="mr-2 block" :to="{ name: 'forms-create-guest' }" :arrow="true">-->
<!-- Get started for free--> <!-- Get started for free-->
<!-- </v-button>--> <!-- </v-button>-->
<!-- <v-button v-else class="mr-2 block" :to="{ name: 'forms.create' }" :arrow="true">--> <!-- <v-button v-else class="mr-2 block" :to="{ name: 'forms-create' }" :arrow="true">-->
<!-- Get started for free--> <!-- Get started for free-->
<!-- </v-button>--> <!-- </v-button>-->
<!-- <v-button color="light-gray" class="mr-1 block" :to="{ name: 'aiformbuilder' }">--> <!-- <v-button color="light-gray" class="mr-1 block" :to="{ name: 'aiformbuilder' }">-->

75
client/composables/lib/vForm/Errors.js vendored Normal file
View File

@ -0,0 +1,75 @@
function arrayWrap(value) {
return Array.isArray(value) ? value : [value];
}
export default class Errors {
constructor() {
this.errors = {};
}
set(field, messages = undefined) {
if (typeof field === 'object') {
this.errors = field;
} else {
this.set({ ...this.errors, [field]: arrayWrap(messages) });
}
}
all() {
return this.errors;
}
has(field) {
return Object.prototype.hasOwnProperty.call(this.errors, field);
}
hasAny(...fields) {
return fields.some(field => this.has(field));
}
any() {
return Object.keys(this.errors).length > 0;
}
get(field) {
if (this.has(field)) {
return this.getAll(field)[0];
}
}
getAll(field) {
return arrayWrap(this.errors[field] || []);
}
only(...fields) {
const messages = [];
fields.forEach((field) => {
const message = this.get(field);
if (message) {
messages.push(message);
}
});
return messages;
}
flatten() {
return Object.values(this.errors).reduce((a, b) => a.concat(b), []);
}
clear(field = undefined) {
const errors = {};
if (field) {
Object.keys(this.errors).forEach((key) => {
if (key !== field) {
errors[key] = this.errors[key];
}
});
}
this.set(errors);
}
}

175
client/composables/lib/vForm/Form.js vendored Normal file
View File

@ -0,0 +1,175 @@
import {serialize} from 'object-to-formdata';
import Errors from './Errors';
import cloneDeep from 'clone-deep';
import {useOpnFetch} from "~/composables/useOpnFetch.js";
function hasFiles(data) {
return data instanceof File ||
data instanceof Blob ||
data instanceof FileList ||
(typeof data === 'object' && data !== null && Object.values(data).find(value => hasFiles(value)) !== undefined);
}
class Form {
constructor(data = {}) {
this.originalData = {};
this.busy = false;
this.successful = false;
this.recentlySuccessful = false;
this.recentlySuccessfulTimeoutId = undefined;
this.errors = new Errors();
this.update(data);
}
static errorMessage = 'Something went wrong. Please try again.';
static recentlySuccessfulTimeout = 2000;
static ignore = ['busy', 'successful', 'errors', 'originalData', 'recentlySuccessful', 'recentlySuccessfulTimeoutId'];
static make(augment) {
return new this(augment);
}
update(data) {
this.originalData = Object.assign({}, this.originalData, cloneDeep(data));
Object.assign(this, data);
}
fill(data = {}) {
this.keys().forEach((key) => {
this[key] = data[key];
});
}
data() {
return this.keys().reduce((data, key) => (
{...data, [key]: this[key]}
), {});
}
keys() {
return Object.keys(this).filter(key => !Form.ignore.includes(key));
}
startProcessing() {
this.errors.clear();
this.busy = true;
this.successful = false;
this.recentlySuccessful = false;
clearTimeout(this.recentlySuccessfulTimeoutId);
}
finishProcessing() {
this.busy = false;
this.successful = true;
this.recentlySuccessful = true;
this.recentlySuccessfulTimeoutId = setTimeout(() => {
this.recentlySuccessful = false;
}, Form.recentlySuccessfulTimeout);
}
clear() {
this.errors.clear();
this.successful = false;
this.recentlySuccessful = false;
clearTimeout(this.recentlySuccessfulTimeoutId);
}
reset() {
Object.keys(this)
.filter(key => !Form.ignore.includes(key))
.forEach((key) => {
this[key] = deepCopy(this.originalData[key]);
});
}
get(url, config = {}) {
return this.submit('get', url, config);
}
post(url, config = {}) {
return this.submit('post', url, config);
}
patch(url, config = {}) {
return this.submit('patch', url, config);
}
put(url, config = {}) {
return this.submit('put', url, config);
}
delete(url, config = {}) {
return this.submit('delete', url, config);
}
submit(method, url, config = {}) {
this.startProcessing();
config = {
body: {},
params: {},
url: url,
method: method,
...config
};
if (method.toLowerCase() === 'get') {
config.params = {...this.data(), ...config.params};
} else {
config.body = {...this.data(), ...config.data};
if (hasFiles(config.data) && !config.transformRequest) {
config.transformRequest = [data => serialize(data)];
}
}
return new Promise((resolve, reject) => {
useOpnFetch(config.url, config)
.then(({data, error}) => {
if (error.value) {
this.handleErrors(error);
reject(error);
return;
}
this.finishProcessing();
resolve(data.value);
})
});
}
handleErrors(error) {
this.busy = false;
if (error.value) {
this.errors.set(this.extractErrors(error.value.data));
}
}
extractErrors(data) {
if (!data || typeof data !== 'object') {
return {error: Form.errorMessage};
}
if (data.errors) {
return {...data.errors};
}
if (data.message) {
return {error: data.message};
}
return {...data};
}
onKeydown(event) {
const target = event.target;
if (target.name) {
this.errors.clear(target.name);
}
}
}
export default Form;

5
client/composables/useForm.js vendored Normal file
View File

@ -0,0 +1,5 @@
import Form from "~/composables/lib/vForm/Form.js"
export const useForm = (formData) => {
return new Form(formData)
}

5
client/composables/useOpnFetch.js vendored Normal file
View File

@ -0,0 +1,5 @@
import config from "~/opnform.config.js";
export const useOpnFetch = (request, opts) => {
return useFetch(request, { baseURL: config.api_url, ...opts })
}

8
client/middleware/admin.js vendored Normal file
View File

@ -0,0 +1,8 @@
import {useAuthStore} from "../../resources/js/stores/auth.js";
export default defineNuxtRouteMiddleware((to, from) => {
const authStore = useAuthStore()
if (!authStore.user?.admin) {
navigateTo({ name: 'home' })
}
})

11
client/middleware/auth.js vendored Normal file
View File

@ -0,0 +1,11 @@
import {useAuthStore} from "../../resources/js/stores/auth.js";
export default defineNuxtRouteMiddleware((to, from) => {
const authStore = useAuthStore()
if (!authStore.check) {
useCookie('intended_url').value = to.path
navigateTo({ name: 'login' })
}
})

View File

@ -0,0 +1,5 @@
export default defineNuxtRouteMiddleware((to, from) => {
const authStore = useAuthStore()
authStore.loadTokenFromCookie()
useAuthStore().fetchUserIfNotFetched()
})

View File

@ -1,6 +1,6 @@
export default { export default {
"appName": "OpnForm", "app_ame": "OpnForm",
"app_url": "https://opnform.test", "api_url": "https://opnform.test/api",
"locale": "en", "locale": "en",
"locales": {"en": "EN"}, "locales": {"en": "EN"},
"githubAuth": null, "githubAuth": null,

View File

@ -24,14 +24,13 @@
"date-fns": "^2.28.0", "date-fns": "^2.28.0",
"debounce": "^1.2.1", "debounce": "^1.2.1",
"fuse.js": "^6.4.6", "fuse.js": "^6.4.6",
"js-cookie": "^2.2.1",
"js-sha256": "^0.9.0", "js-sha256": "^0.9.0",
"libphonenumber-js": "^1.10.44", "libphonenumber-js": "^1.10.44",
"object-to-formdata": "^4.5.1",
"prismjs": "^1.24.1", "prismjs": "^1.24.1",
"qrcode": "^1.5.1", "qrcode": "^1.5.1",
"query-builder-vue-3": "^1.0.1", "query-builder-vue-3": "^1.0.1",
"tinymotion": "^0.2.0", "tinymotion": "^0.2.0",
"vform": "^2.1.1",
"vue": "^3.2.13", "vue": "^3.2.13",
"vue-chartjs": "^5.2.0", "vue-chartjs": "^5.2.0",
"vue-codemirror": "^4.0.6", "vue-codemirror": "^4.0.6",
@ -6751,11 +6750,6 @@
"jiti": "bin/jiti.js" "jiti": "bin/jiti.js"
} }
}, },
"node_modules/js-cookie": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz",
"integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ=="
},
"node_modules/js-sha256": { "node_modules/js-sha256": {
"version": "0.9.0", "version": "0.9.0",
"resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz", "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz",
@ -8094,6 +8088,11 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/object-to-formdata": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/object-to-formdata/-/object-to-formdata-4.5.1.tgz",
"integrity": "sha512-QiM9D0NiU5jV6J6tjE1g7b4Z2tcUnKs1OPUi4iMb2zH+7jwlcUrASghgkFk9GtzqNNq8rTQJtT8AzjBAvLoNMw=="
},
"node_modules/ofetch": { "node_modules/ofetch": {
"version": "1.3.3", "version": "1.3.3",
"resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.3.3.tgz", "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.3.3.tgz",
@ -10950,15 +10949,6 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0" "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
} }
}, },
"node_modules/vform": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/vform/-/vform-2.1.2.tgz",
"integrity": "sha512-Nobg/0ckWHYQAjJJqucOyRv/a3enO40f087KBpkSUCl0eRQTE8qmUqk/l5gmbxhD8UO3A8dDiSRWKmKF8+l4YQ==",
"peerDependencies": {
"axios": "*",
"vue": "*"
}
},
"node_modules/vite": { "node_modules/vite": {
"version": "5.0.7", "version": "5.0.7",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.0.7.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.7.tgz",

View File

@ -36,14 +36,13 @@
"date-fns": "^2.28.0", "date-fns": "^2.28.0",
"debounce": "^1.2.1", "debounce": "^1.2.1",
"fuse.js": "^6.4.6", "fuse.js": "^6.4.6",
"js-cookie": "^2.2.1",
"js-sha256": "^0.9.0", "js-sha256": "^0.9.0",
"libphonenumber-js": "^1.10.44", "libphonenumber-js": "^1.10.44",
"object-to-formdata": "^4.5.1",
"prismjs": "^1.24.1", "prismjs": "^1.24.1",
"qrcode": "^1.5.1", "qrcode": "^1.5.1",
"query-builder-vue-3": "^1.0.1", "query-builder-vue-3": "^1.0.1",
"tinymotion": "^0.2.0", "tinymotion": "^0.2.0",
"vform": "^2.1.1",
"vue": "^3.2.13", "vue": "^3.2.13",
"vue-chartjs": "^5.2.0", "vue-chartjs": "^5.2.0",
"vue-codemirror": "^4.0.6", "vue-codemirror": "^4.0.6",

View File

@ -22,7 +22,7 @@
<v-button v-if="!authenticated" class="mr-1" :to="{ name: 'forms-create-guest' }" :arrow="true"> <v-button v-if="!authenticated" class="mr-1" :to="{ name: 'forms-create-guest' }" :arrow="true">
Get started for free Get started for free
</v-button> </v-button>
<v-button v-else class="mr-1" :to="{ name: 'forms.create' }" :arrow="true"> <v-button v-else class="mr-1" :to="{ name: 'forms-create' }" :arrow="true">
Get started for free Get started for free
</v-button> </v-button>
</div> </div>
@ -273,7 +273,7 @@
<!--&lt;!&ndash; <v-button v-if="!authenticated" class="mr-1" :to="{ name: 'forms-create-guest' }" :arrow="true">&ndash;&gt;--> <!--&lt;!&ndash; <v-button v-if="!authenticated" class="mr-1" :to="{ name: 'forms-create-guest' }" :arrow="true">&ndash;&gt;-->
<!--&lt;!&ndash; Get started for free&ndash;&gt;--> <!--&lt;!&ndash; Get started for free&ndash;&gt;-->
<!--&lt;!&ndash; </v-button>&ndash;&gt;--> <!--&lt;!&ndash; </v-button>&ndash;&gt;-->
<!--&lt;!&ndash; <v-button v-else class="mr-1" :to="{ name: 'forms.create' }" :arrow="true">&ndash;&gt;--> <!--&lt;!&ndash; <v-button v-else class="mr-1" :to="{ name: 'forms-create' }" :arrow="true">&ndash;&gt;-->
<!--&lt;!&ndash; Get started for free&ndash;&gt;--> <!--&lt;!&ndash; Get started for free&ndash;&gt;-->
<!--&lt;!&ndash; </v-button>&ndash;&gt;--> <!--&lt;!&ndash; </v-button>&ndash;&gt;-->
<!--&lt;!&ndash; </div>&ndash;&gt;--> <!--&lt;!&ndash; </div>&ndash;&gt;-->
@ -446,7 +446,7 @@
<v-button v-if="!authenticated" class="mr-1" :to="{ name: 'forms-create-guest' }" :arrow="true"> <v-button v-if="!authenticated" class="mr-1" :to="{ name: 'forms-create-guest' }" :arrow="true">
Get started for free Get started for free
</v-button> </v-button>
<v-button v-else class="mr-1" :to="{ name: 'forms.create' }" :arrow="true"> <v-button v-else class="mr-1" :to="{ name: 'forms-create' }" :arrow="true">
Get started for free Get started for free
</v-button> </v-button>
</div> </div>

View File

@ -7,7 +7,7 @@
<h2 class="flex-grow text-gray-900"> <h2 class="flex-grow text-gray-900">
Your Forms Your Forms
</h2> </h2>
<v-button v-track.create_form_click :to="{name:'forms.create'}"> <v-button v-track.create_form_click :to="{name:'forms-create'}">
<svg class="w-4 h-4 text-white inline mr-1 -mt-1" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg class="w-4 h-4 text-white inline mr-1 -mt-1" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.99996 1.1665V12.8332M1.16663 6.99984H12.8333" stroke="currentColor" stroke-width="1.67" stroke-linecap="round" stroke-linejoin="round" /> <path d="M6.99996 1.1665V12.8332M1.16663 6.99984H12.8333" stroke="currentColor" stroke-width="1.67" stroke-linecap="round" stroke-linejoin="round" />
</svg> </svg>
@ -48,7 +48,7 @@
<div v-if="isFilteringForms && enrichedForms.length === 0 && searchForm.search" class="mt-2 w-full text-center"> <div v-if="isFilteringForms && enrichedForms.length === 0 && searchForm.search" class="mt-2 w-full text-center">
Your search "{{ searchForm.search }}" did not match any forms. Please try again. Your search "{{ searchForm.search }}" did not match any forms. Please try again.
</div> </div>
<v-button v-if="forms.length === 0" v-track.create_form_click class="mt-4" :to="{name:'forms.create'}"> <v-button v-if="forms.length === 0" v-track.create_form_click class="mt-4" :to="{name:'forms-create'}">
<svg class="w-4 h-4 text-white inline mr-1 -mt-1" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg class="w-4 h-4 text-white inline mr-1 -mt-1" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.99996 1.1665V12.8332M1.16663 6.99984H12.8333" stroke="currentColor" stroke-width="1.67" stroke-linecap="round" stroke-linejoin="round" /> <path d="M6.99996 1.1665V12.8332M1.16663 6.99984H12.8333" stroke="currentColor" stroke-width="1.67" stroke-linecap="round" stroke-linejoin="round" />
</svg> </svg>
@ -119,7 +119,7 @@ const loadForms = function () {
const formsStore = useFormsStore() const formsStore = useFormsStore()
const workspacesStore = useWorkspacesStore() const workspacesStore = useWorkspacesStore()
formsStore.startLoading() formsStore.startLoading()
workspacesStore.loadIfEmpty().then(() => { return workspacesStore.loadIfEmpty().then(() => {
formsStore.loadIfEmpty(workspacesStore.currentId) formsStore.loadIfEmpty(workspacesStore.currentId)
}) })
} }
@ -127,10 +127,6 @@ const loadForms = function () {
export default { export default {
components: { OpenFormFooter, TextInput, ExtraMenu }, components: { OpenFormFooter, TextInput, ExtraMenu },
beforeRouteEnter (to, from, next) {
loadForms()
next()
},
middleware: 'auth', middleware: 'auth',
props: { props: {
@ -138,10 +134,11 @@ export default {
metaDescription: { type: String, default: 'All of your OpnForm are here. Create new forms, or update your existing one!' } metaDescription: { type: String, default: 'All of your OpnForm are here. Create new forms, or update your existing one!' }
}, },
setup () { async setup () {
const authStore = useAuthStore() const authStore = useAuthStore()
const formsStore = useFormsStore() const formsStore = useFormsStore()
const workspacesStore = useWorkspacesStore() const workspacesStore = useWorkspacesStore()
loadForms()
return { return {
formsStore, formsStore,
workspacesStore, workspacesStore,

View File

@ -47,7 +47,7 @@
<v-button v-if="!authenticated" class="mr-1" :to="{ name: 'forms-create-guest' }" :arrow="true"> <v-button v-if="!authenticated" class="mr-1" :to="{ name: 'forms-create-guest' }" :arrow="true">
Create a form for FREE Create a form for FREE
</v-button> </v-button>
<v-button v-else class="mr-1" :to="{ name: 'forms.create' }" :arrow="true"> <v-button v-else class="mr-1" :to="{ name: 'forms-create' }" :arrow="true">
Create a form for FREE Create a form for FREE
</v-button> </v-button>
</div> </div>

45
client/plugins/fetch.js vendored Normal file
View File

@ -0,0 +1,45 @@
import { ofetch } from 'ofetch'
import {useAuthStore} from "~/stores/auth.js";
function addAuthHeader(request, options) {
const authStore = useAuthStore()
if (authStore.check) {
options.headers = { Authorization: `Bearer ${authStore.token}` }
console.log('addidng auth',options)
}
}
function addPasswordToFormRequest (request) {
const url = request.url
if (!url || !url.startsWith('/api/forms/')) return
const slug = url.split('/')[3]
const passwordCookie = useCookie('password-' + slug, { maxAge: 60 * 60 * 24 * 30 }) // 30 days
if (slug !== undefined && slug !== '' && passwordCookie.value !== undefined) {
request.headers['form-password'] = passwordCookie.value
}
}
export default defineNuxtPlugin((_nuxtApp) => {
globalThis.$fetch = ofetch.create({
onRequest ({ request, options }) {
// TODO: check that it's our own domain called
addAuthHeader(request, options)
addPasswordToFormRequest(request)
},
onResponseError ({ response }) {
const authStore = useAuthStore()
const { status } = response
if (status === 401 && authStore.check) {
// TODO: check that it's our own domain called
authStore.logout()
useRouter().push({ name: 'login' })
}
if (status >= 500) {
console.error('Request error', status)
}
}
})
})

74
client/stores/auth.js vendored
View File

@ -1,53 +1,64 @@
import { defineStore } from 'pinia' import {defineStore} from 'pinia'
import axios from 'axios' import axios from 'axios'
import Cookies from 'js-cookie'
export const useAuthStore = defineStore('auth', { export const useAuthStore = defineStore('auth', {
state: () => ({ state: () => {
return {
token: null,
admin_token: null,
user: null, user: null,
token: Cookies.get('token'), }
},
// For admin impersonation
admin_token: Cookies.get('admin_token') ?? null
}),
getters: { getters: {
check: (state) => (state.user !== null && state.user !== undefined), check: (state) => (state.user !== null && state.user !== undefined),
isImpersonating: (state) => (state.admin_token !== null && state.admin_token !== undefined) isImpersonating: (state) => (state.admin_token !== null && state.admin_token !== undefined)
}, },
actions: { actions: {
// Stores admin token temporarily for impersonation // Stores admin token temporarily for impersonation
startImpersonating () { startImpersonating() {
this.admin_token = this.token this.setAdminToken(this.token)
Cookies.set('admin_token', this.token, { expires: 365 })
}, },
// Stop admin impersonation // Stop admin impersonation
stopImpersonating () { stopImpersonating() {
this.token = this.admin_token this.token = this.admin_token
this.admin_token = null this.admin_token = null
Cookies.set('token', this.token, { expires: 365 })
Cookies.remove('admin_token')
this.fetchUser() this.fetchUser()
}, },
saveToken (token, remember) { setToken(token) {
useCookie('token', {maxAge: 60 * 60 * 24 * 30}).value = token
this.token = token this.token = token
Cookies.set('token', token, { expires: remember ? 365 : null })
}, },
async fetchUser () { setAdminToken(token) {
useCookie('admin_token', {maxAge: 60 * 60 * 24 * 30}).value = token
this.admin_token = token
},
loadTokenFromCookie() {
this.token = useCookie('token').value
this.admin_token = useCookie('admin_token').value
},
async fetchUser() {
try { try {
const { data } = await axios.get('/api/user') const {data} = await axios.get('/api/user')
this.user = data this.user = data
this.initServiceClients() this.initServiceClients()
return data return data
} catch (e) { } catch (e) {
this.token = null this.setToken(null)
Cookies.remove('token')
} }
}, },
updateUser (payload) { async fetchUserIfNotFetched() {
if (this.user === null && this.token) {
await this.fetchUser()
}
},
updateUser(payload) {
this.user = payload this.user = payload
this.initServiceClients() this.initServiceClients()
}, },
@ -56,20 +67,29 @@ export const useAuthStore = defineStore('auth', {
if (!this.user) return if (!this.user) return
useAmplitude().setUser(this.user) useAmplitude().setUser(this.user)
useCrisp().setUser(this.user) useCrisp().setUser(this.user)
// Init sentry
Sentry.configureScope((scope) => {
scope.setUser({
id: this.user.id,
email: this.user.email,
subscription: this.user?.is_subscribed
})
})
}, },
async logout () { async logout() {
try { try {
await axios.post('/api/logout') await axios.post('/api/logout')
} catch (e) { } } catch (e) {
}
this.user = null this.user = null
this.token = null this.setToken(null)
Cookies.remove('token')
}, },
async fetchOauthUrl (provider) { async fetchOauthUrl(provider) {
const { data } = await axios.post(`/api/oauth/${provider}`) const {data} = await axios.post(`/api/oauth/${provider}`)
return data.url return data.url
} }
} }

View File

@ -1,7 +1,7 @@
import { defineStore } from 'pinia' import {defineStore} from 'pinia'
import axios from 'axios' import {useOpnFetch} from "~/composables/useOpnFetch.js";
export const formsEndpoint = '/api/open/workspaces/{workspaceId}/forms' export const formsEndpoint = '/open/workspaces/{workspaceId}/forms'
export let currentPage = 1 export let currentPage = 1
export const useFormsStore = defineStore('forms', { export const useFormsStore = defineStore('forms', {
@ -22,7 +22,7 @@ export const useFormsStore = defineStore('forms', {
if (state.content.length === 0) return [] if (state.content.length === 0) return []
let allTags = [] let allTags = []
state.content.forEach(form => { state.content.forEach(form => {
if(form.tags && form.tags.length > 0){ if (form.tags && form.tags.length > 0) {
allTags = allTags.concat(form.tags) allTags = allTags.concat(form.tags)
} }
}) })
@ -30,34 +30,34 @@ export const useFormsStore = defineStore('forms', {
} }
}, },
actions: { actions: {
set (items) { set(items) {
this.content = items this.content = items
}, },
append (items) { append(items) {
this.content = this.content.concat(items) this.content = this.content.concat(items)
}, },
addOrUpdate (item) { addOrUpdate(item) {
this.content = this.content.filter((val) => val.id !== item.id) this.content = this.content.filter((val) => val.id !== item.id)
this.content.push(item) this.content.push(item)
}, },
remove (item) { remove(item) {
this.content = this.content.filter((val) => val.id !== item.id) this.content = this.content.filter((val) => val.id !== item.id)
}, },
startLoading () { startLoading() {
this.loading = true this.loading = true
}, },
stopLoading () { stopLoading() {
this.loading = false this.loading = false
}, },
resetState () { resetState() {
this.set([]) this.set([])
this.stopLoading() this.stopLoading()
currentPage = 1 currentPage = 1
}, },
load (workspaceId) { load(workspaceId) {
this.startLoading() this.startLoading()
return axios.get(formsEndpoint.replace('{workspaceId}', workspaceId)+'?page='+currentPage).then((response) => { return useOpnFetch(formsEndpoint.replace('{workspaceId}', workspaceId) + '?page=' + currentPage).get().then((response) => {
if (currentPage == 1) { if (currentPage === 1) {
this.set(response.data.data) this.set(response.data.data)
} else { } else {
this.append(response.data.data) this.append(response.data.data)
@ -71,7 +71,7 @@ export const useFormsStore = defineStore('forms', {
} }
}) })
}, },
loadIfEmpty (workspaceId) { loadIfEmpty(workspaceId) {
if (this.content.length === 0) { if (this.content.length === 0) {
return this.load(workspaceId) return this.load(workspaceId)
} }

View File

@ -1,8 +1,10 @@
import axios from 'axios' import {defineStore} from 'pinia'
import { defineStore } from 'pinia' import {useOpnFetch} from "~/composables/useOpnFetch.js"
export const workspaceEndpoint = '/api/open/workspaces/' import {useStorage} from "@vueuse/core"
const localStorageCurrentWorkspaceKey = 'currentWorkspace' export const workspaceEndpoint = 'open/workspaces/'
const storedWorkspaceId = useStorage('currentWorkspace', 0)
export const useWorkspacesStore = defineStore('workspaces', { export const useWorkspacesStore = defineStore('workspaces', {
state: () => ({ state: () => ({
@ -21,72 +23,68 @@ export const useWorkspacesStore = defineStore('workspaces', {
} }
}, },
actions: { actions: {
set (items) { set(items) {
this.content = items this.content = items
if (this.currentId == null && this.content.length > 0) { if (this.currentId == null && this.content.length > 0) {
// If one only, set it // If one only, set it
if (this.content.length === 1) { if (this.content.length === 1) {
this.currentId = items[0].id this.setCurrentId(items[0].id)
localStorage.setItem(localStorageCurrentWorkspaceKey, this.currentId) } else if (storedWorkspaceId && this.content.find(item => item.id === parseInt(storedWorkspaceId.value))) {
} else if (localStorage.getItem(localStorageCurrentWorkspaceKey) && this.content.find(item => item.id === parseInt(localStorage.getItem(localStorageCurrentWorkspaceKey)))) {
// Check local storage for current workspace, or take first // Check local storage for current workspace, or take first
this.currentId = parseInt(localStorage.getItem(localStorageCurrentWorkspaceKey)) this.setCurrentId(parseInt(storedWorkspaceId.value))
localStorage.setItem(localStorageCurrentWorkspaceKey, this.currentId)
} else { } else {
// Else, take first // Else, take first
this.currentId = items[0].id this.setCurrentId(items[0].id)
localStorage.setItem(localStorageCurrentWorkspaceKey, this.currentId)
} }
} else { } else {
localStorage.removeItem(localStorageCurrentWorkspaceKey) this.setCurrentId(null)
} }
}, },
setCurrentId (id) { setCurrentId(id) {
this.currentId = id this.currentId = id
localStorage.setItem(localStorageCurrentWorkspaceKey, id) storedWorkspaceId.value = id
}, },
addOrUpdate (item) { addOrUpdate(item) {
this.content = this.content.filter((val) => val.id !== item.id) this.content = this.content.filter((val) => val.id !== item.id)
this.content.push(item) this.content.push(item)
if (this.currentId == null) { if (this.currentId == null) {
this.currentId = item.id this.currentId = item.id
localStorage.setItem(localStorageCurrentWorkspaceKey, this.currentId) storedWorkspaceId.value = this.currentId
} }
}, },
remove (itemId) { remove(itemId) {
this.content = this.content.filter((val) => val.id !== itemId) this.content = this.content.filter((val) => val.id !== itemId)
if (this.currentId === itemId) { if (this.currentId === itemId) {
this.currentId = this.content.length > 0 ? this.content[0].id : null this.setCurrentId(this.content.length > 0 ? this.content[0].id : null)
localStorage.setItem(localStorageCurrentWorkspaceKey, this.currentId)
} }
}, },
startLoading () { startLoading() {
this.loading = true this.loading = true
}, },
stopLoading () { stopLoading() {
this.loading = false this.loading = false
}, },
resetState () { resetState() {
this.set([]) this.set([])
this.stopLoading() this.stopLoading()
}, },
load () { load() {
this.set([]) this.set([])
this.startLoading() this.startLoading()
return axios.get(workspaceEndpoint).then((response) => { return useOpnFetch(workspaceEndpoint).then((response) => {
this.set(response.data) this.set(response.data)
this.stopLoading() this.stopLoading()
}) })
}, },
loadIfEmpty () { loadIfEmpty() {
if (this.content.length === 0) { if (this.content.length === 0) {
return this.load() return this.load()
} }
return Promise.resolve() return Promise.resolve()
}, },
delete (id) { delete(id) {
this.startLoading() this.startLoading()
return axios.delete(workspaceEndpoint + id).then((response) => { return useOpnFetch(workspaceEndpoint + id, {method: 'DELETE'}).then((response) => {
this.remove(response.data.workspace_id) this.remove(response.data.workspace_id)
this.stopLoading() this.stopLoading()
}) })