diff --git a/components/Modal.vue b/components/Modal.vue
index f60ade4..dc383ac 100644
--- a/components/Modal.vue
+++ b/components/Modal.vue
@@ -21,18 +21,18 @@
// props for which buttons to show
interface Props {
- showOk?: boolean
- showCancel?: boolean
+ showOk?: boolean;
+ showCancel?: boolean;
}
const props = withDefaults(defineProps(), {
showOk: true,
- showCancel: false,
- })
+ showCancel: false
+ });
// open event (exposed to parent)
const open = () => {
modalIsVisible.value = true;
- }
+ };
defineExpose({ open });
// close events emitted on modal close
@@ -40,16 +40,20 @@
const closeOk = () => {
emit('closeOk');
modalIsVisible.value = false;
- }
+ };
const closeCancel = () => {
emit('closeCancel');
modalIsVisible.value = false;
- }
+ };
-
+
@@ -64,4 +68,4 @@
-
\ No newline at end of file
+
diff --git a/components/Notifications.client.vue b/components/Notifications.client.vue
index face861..32795e6 100644
--- a/components/Notifications.client.vue
+++ b/components/Notifications.client.vue
@@ -1,4 +1,3 @@
-
-
\ No newline at end of file
+
diff --git a/components/UserAccount/UserAccount.vue b/components/UserAccount/UserAccount.vue
index 2bfaef3..34cee75 100644
--- a/components/UserAccount/UserAccount.vue
+++ b/components/UserAccount/UserAccount.vue
@@ -2,8 +2,8 @@
const props = defineProps({
user: {
type: Object,
- required: true,
- },
+ required: true
+ }
});
const { user } = props;
@@ -13,16 +13,24 @@
-
+
{{ user.email }}
Account
-
-
+
+
-
\ No newline at end of file
+
diff --git a/components/UserAccount/UserAccountSignout.client.vue b/components/UserAccount/UserAccountSignout.client.vue
index 509133b..aaa9fdf 100644
--- a/components/UserAccount/UserAccountSignout.client.vue
+++ b/components/UserAccount/UserAccountSignout.client.vue
@@ -1,20 +1,19 @@
-
Signout
-
\ No newline at end of file
+
diff --git a/components/UserAccount/UserAccountSwitch.client.vue b/components/UserAccount/UserAccountSwitch.client.vue
index 2046b8d..32d2aa2 100644
--- a/components/UserAccount/UserAccountSwitch.client.vue
+++ b/components/UserAccount/UserAccountSwitch.client.vue
@@ -1,21 +1,27 @@
@@ -13,57 +13,72 @@ const user = useSupabaseUser()
-
- Build Your Next SaaS Faster
-
+
Build Your Next SaaS Faster
- With SupaNuxt SaaS, you can easily get started building your
- next web application. Our pre-configured tech stack and
- industry leading features make it easy to get up and running in no time. Look! this guy is working so fast,
- his hands are just a blur.. you could be this fast.
+ With SupaNuxt SaaS, you can easily get started building your next
+ web application. Our pre-configured tech stack and industry
+ leading features make it easy to get up and running in no time.
+ Look! this guy is working so fast, his hands are just a blur.. you
+ could be this fast.
- Get Started
+ Get Started
-
+
-
-
+
+
Tech Stack
-
Nuxt 3
-
The Progressive Vue.js Framework
+
+ The Progressive Vue.js Framework
+
Supabase
-
Auth including OAuth + Postgresql instance
+
+ Auth including OAuth + Postgresql instance
+
-
+
PostgreSQL
Relational Database
Prisma
-
Schema management + Strongly typed client
+
+ Schema management + Strongly typed client
+
TRPC
-
Server/Client communication with Strong types, SSR compatible
+
+ Server/Client communication with Strong types, SSR compatible
+
@@ -73,104 +88,148 @@ const user = useSupabaseUser()
Stripe
-
Payments including Webhook integration
+
+ Payments including Webhook integration
+
-
+
Tailwind
-
A utility-first CSS framework
+
+ A utility-first CSS framework
+
Vue.js
-
The Progressive JavaScript Framework
+
+ The Progressive JavaScript Framework
+
OpenAI
-
AI Completions including Note generation from prompt
+
+ AI Completions including Note generation from prompt
+
-
+
Features
-
+
User Management
-
SupaNuxt SaaS includes robust user management features, including
- authentication with social login (oauth) or email/password, management of user roles and permissions, and
- multi-user/team accounts that permit multiple users to share plan features including a team administration
- facility and user roles within team. This is a great feature for businesses or community groups who want to
- share the cost of the plan.
+
+ SupaNuxt SaaS includes robust user management features, including
+ authentication with social login (oauth) or email/password,
+ management of user roles and permissions, and multi-user/team
+ accounts that permit multiple users to share plan features
+ including a team administration facility and user roles within
+ team. This is a great feature for businesses or community groups
+ who want to share the cost of the plan.
+
-
+
DB Schema Management
-
We use Prisma for schema management to make sure you can easily
- manage and keep track of your database schema. We also utilise Prisma based strong types which, with some help from TRPC, penetrate the entire stack all
- the way to the web front end. This ensures that you can move fast with your feature development, alter schema and have those
- type changes instantly available and validated everywhere.
+
+ We use Prisma for schema management to make sure you can easily
+ manage and keep track of your database schema. We also utilise
+ Prisma based strong types which, with some help from TRPC,
+ penetrate the entire stack all the way to the web front end. This
+ ensures that you can move fast with your feature development,
+ alter schema and have those type changes instantly available and
+ validated everywhere.
+
-
+
Config and Environment
-
SupaNuxt SaaS includes an approach to config and environment
- management that enables customisation and management of api keys.
+
+ SupaNuxt SaaS includes an approach to config and environment
+ management that enables customisation and management of api keys.
+
-
+
State Management
-
SupaNuxt SaaS includes multi modal state management that supports both Single Page Application (SPA)
- pages such as dashboards and Server Side Rendered (SSR) style pages for public content that are crawlable by Search
- engines like google and facilitate excellent Search Engine Optimisation (SEO).
+
+ SupaNuxt SaaS includes multi modal state management that supports
+ both Single Page Application (SPA) pages such as dashboards and
+ Server Side Rendered (SSR) style pages for public content that are
+ crawlable by Search engines like google and facilitate excellent
+ Search Engine Optimisation (SEO).
+
-
+
Stripe Integration
-
SupaNuxt SaaS includes Stripe integration for subscription payments including
- Subscription based support for multi pricing and multiple plans.
+
+ SupaNuxt SaaS includes Stripe integration for subscription
+ payments including Subscription based support for multi pricing
+ and multiple plans.
+
-
+
Style System
-
SupaNuxt SaaS includes Tailwind integration for site styling including a themable UI components with daisyUI
+
+ SupaNuxt SaaS includes Tailwind integration for site styling
+ including a themable UI components with daisyUI
+
diff --git a/pages/join/[join_password].vue b/pages/join/[join_password].vue
index 09988a7..301faae 100644
--- a/pages/join/[join_password].vue
+++ b/pages/join/[join_password].vue
@@ -10,7 +10,9 @@
// this could probably be an elegant destructure here but I lost patience
let account: AccountWithMembers | undefined;
if (join_password) {
- const result = await $client.account.getAccountByJoinPassword.useQuery({ join_password });
+ const result = await $client.account.getAccountByJoinPassword.useQuery({
+ join_password
+ });
account = result.data.value?.account;
}
@@ -19,45 +21,51 @@
async function doJoin() {
if (account) {
await accountStore.joinUserToAccountPending(account.id);
- navigateTo('/dashboard', {replace: true})
+ navigateTo('/dashboard', { replace: true });
} else {
- console.log(`Unable to Join`)
+ console.log(`Unable to Join`);
}
}
-
Request to Join {{ account?.name }}
+
+ Request to Join {{ account?.name }}
+
- Click below to request to Join the team.
- Your request to join will remain as 'Pending'
- untill the team administrators complete their review.
-
- If your requeste is approved, you will become a member of the team and
- will be able to switch to the team account at any time in order to share
- the benefits of the team plan.
+ Click below to request to Join the team. Your request to join will
+ remain as 'Pending' untill the team administrators complete their
+ review.
+
+
+ If your requeste is approved, you will become a member of the team and
+ will be able to switch to the team account at any time in order to
+ share the benefits of the team plan.
-
+
Join
-
Only signed in users can join a team. Please either Signup or Signin and then return to this page using the join link.
-
+
+ Only signed in users can join a team. Please either Signup or Signin
+ and then return to this page using the join link.
+
+
Sign Up
-
+
Sign In
diff --git a/pages/notes/[note_id].vue b/pages/notes/[note_id].vue
index f946ba5..eda3b4b 100644
--- a/pages/notes/[note_id].vue
+++ b/pages/notes/[note_id].vue
@@ -1,12 +1,13 @@
Your privacy is important to us. This privacy statement explains what personal data we collect from you and how we
- use it. By using our website, you agree to the terms of this privacy statement.
+
+ Your privacy is important to us. This privacy statement explains what
+ personal data we collect from you and how we use it. By using our website,
+ you agree to the terms of this privacy statement.
+
Information we collect
-
We collect personal information that you voluntarily provide to us when you use our website, including your email
- and full name. We also collect non-personal information about your use of our website, such as your IP address,
- browser type, and the pages you visit.
+
+ We collect personal information that you voluntarily provide to us when
+ you use our website, including your email and full name. We also collect
+ non-personal information about your use of our website, such as your IP
+ address, browser type, and the pages you visit.
+
-
In addition to the personal data that we collect directly from you, we also use a third-party authentication
- provider called Supabase to manage user authentication. When you use our website, Supabase may collect personal data
- about you, such as your email address and authentication credentials. For more information about Supabase's data
- practices, please refer to their privacy policy at https://supabase.com/privacy.
+
+ In addition to the personal data that we collect directly from you, we
+ also use a third-party authentication provider called Supabase to manage
+ user authentication. When you use our website, Supabase may collect
+ personal data about you, such as your email address and authentication
+ credentials. For more information about Supabase's data practices, please
+ refer to their privacy policy at
+ https://supabase.com/privacy.
+
How we use your information
-
We use your personal information to provide you with the products and services you request, to communicate with
- you, and to improve our website. We may also use your information for marketing purposes, but we will always give
- you the option to opt-out of receiving marketing communications from us.
+
+ We use your personal information to provide you with the products and
+ services you request, to communicate with you, and to improve our website.
+ We may also use your information for marketing purposes, but we will
+ always give you the option to opt-out of receiving marketing
+ communications from us.
+
Disclosure of your information
-
We may disclose your personal information to third parties who provide services to us, such as website hosting,
- data analysis, and customer service. We may also disclose your information if we believe it is necessary to comply
- with the law or to protect our rights or the rights of others.
+
+ We may disclose your personal information to third parties who provide
+ services to us, such as website hosting, data analysis, and customer
+ service. We may also disclose your information if we believe it is
+ necessary to comply with the law or to protect our rights or the rights of
+ others.
+
-
As mentioned above, we use a third-party authentication provider called Supabase to manage user authentication.
- Supabase may share your personal data with other third-party service providers that they use to provide their
- services, such as hosting and cloud storage providers. For more information about Supabase's data sharing practices,
- please refer to their privacy policy at https://supabase.com/privacy.
+
+ As mentioned above, we use a third-party authentication provider called
+ Supabase to manage user authentication. Supabase may share your personal
+ data with other third-party service providers that they use to provide
+ their services, such as hosting and cloud storage providers. For more
+ information about Supabase's data sharing practices, please refer to their
+ privacy policy at
+ https://supabase.com/privacy.
+
Security of your information
-
We take reasonable measures to protect your personal information from unauthorized access, use, or disclosure.
- However, no data transmission over the internet or electronic storage is completely secure, so we cannot guarantee
- the absolute security of your information.
+
+ We take reasonable measures to protect your personal information from
+ unauthorized access, use, or disclosure. However, no data transmission
+ over the internet or electronic storage is completely secure, so we cannot
+ guarantee the absolute security of your information.
+
Changes to this privacy statement
-
We may update this privacy statement from time to time. Any changes will be posted on this page, so please check
- back periodically to review the most current version of the statement.
+
+ We may update this privacy statement from time to time. Any changes will
+ be posted on this page, so please check back periodically to review the
+ most current version of the statement.
+
Contact us
-
If you have any questions or concerns about our privacy practices, please contact us at [insert contact
- information].
-
+
+ If you have any questions or concerns about our privacy practices, please
+ contact us at [insert contact information].
+
-
\ No newline at end of file
+
diff --git a/pages/resetpassword.vue b/pages/resetpassword.vue
index c5d3a98..8a72ebf 100644
--- a/pages/resetpassword.vue
+++ b/pages/resetpassword.vue
@@ -3,27 +3,27 @@
const notifyStore = useNotifyStore();
- const loading = ref(false)
- const password = ref('')
- const confirmPassword = ref('')
+ const loading = ref(false);
+ const password = ref('');
+ const confirmPassword = ref('');
const changePassword = async () => {
try {
- loading.value = true
+ loading.value = true;
const { data, error } = await supabase.auth.updateUser({
password: password.value
});
- if (error) throw error
+ if (error) throw error;
else {
- notifyStore.notify("password changed", NotificationType.Success);
- navigateTo('/signin', {replace: true}); // navigate to signin because it is best practice although the auth session seems to be valid so it immediately redirects to dashboard
+ notifyStore.notify('password changed', NotificationType.Success);
+ navigateTo('/signin', { replace: true }); // navigate to signin because it is best practice although the auth session seems to be valid so it immediately redirects to dashboard
}
} catch (error) {
notifyStore.notify(error, NotificationType.Error);
} finally {
- loading.value = false
+ loading.value = false;
}
- }
+ };
- We appreciate your business {{customer.name}}!
- It appears your stripe customer information has been deleted!
+
+ We appreciate your business {{ customer.name }}!
+
+
+ It appears your stripe customer information has been deleted!
+
Go to Your Dashboard
-
\ No newline at end of file
+
diff --git a/pages/terms.vue b/pages/terms.vue
index 4ca801a..9eb9d04 100644
--- a/pages/terms.vue
+++ b/pages/terms.vue
@@ -2,57 +2,95 @@
Terms of Service
-
These terms of service (the "Agreement") govern your use of our website and services (collectively, the
- "Service"). By using the Service, you agree to be bound by the terms of this Agreement. If you do not agree to
- these terms, you may not use the Service.
+
+ These terms of service (the "Agreement") govern your use of our website
+ and services (collectively, the "Service"). By using the Service, you
+ agree to be bound by the terms of this Agreement. If you do not agree to
+ these terms, you may not use the Service.
+
Use of the Service
-
You may use the Service only for lawful purposes and in accordance with this Agreement. You agree not to use the
- Service:
+
+ You may use the Service only for lawful purposes and in accordance with
+ this Agreement. You agree not to use the Service:
+
-
In any way that violates any applicable federal, state, local, or international law or regulation
-
To impersonate or attempt to impersonate us, our employees, another user, or any other person or entity
-
To engage in any other conduct that restricts or inhibits anyone's use or enjoyment of the Service, or which,
- as determined by us, may harm us or users of the Service or expose them to liability
+
+ In any way that violates any applicable federal, state, local, or
+ international law or regulation
+
+
+ To impersonate or attempt to impersonate us, our employees, another
+ user, or any other person or entity
+
+
+ To engage in any other conduct that restricts or inhibits anyone's use
+ or enjoyment of the Service, or which, as determined by us, may harm us
+ or users of the Service or expose them to liability
+
Intellectual Property
-
The Service and its entire contents, features, and functionality (including but not limited to all information,
- software, text, displays, images, video, and audio, and the design, selection, and arrangement thereof) are owned
- by us, our licensors, or other providers of such material and are protected by United States and international
- copyright, trademark, patent, trade secret, and other intellectual property or proprietary rights laws.
+
+ The Service and its entire contents, features, and functionality
+ (including but not limited to all information, software, text, displays,
+ images, video, and audio, and the design, selection, and arrangement
+ thereof) are owned by us, our licensors, or other providers of such
+ material and are protected by United States and international copyright,
+ trademark, patent, trade secret, and other intellectual property or
+ proprietary rights laws.
+
-
You may not reproduce, distribute, modify, create derivative works of, publicly display, publicly perform,
- republish, download, store, or transmit any of the material on our website, except as follows:
+
+ You may not reproduce, distribute, modify, create derivative works of,
+ publicly display, publicly perform, republish, download, store, or
+ transmit any of the material on our website, except as follows:
+
-
Your computer may temporarily store copies of such materials in RAM incidental to your accessing and viewing
- those materials
-
You may store files that are automatically cached by your Web browser for display enhancement purposes
-
You may print or download one copy of a reasonable number of pages of the website for your own personal,
- non-commercial use and not for further reproduction, publication, or distribution
+
+ Your computer may temporarily store copies of such materials in RAM
+ incidental to your accessing and viewing those materials
+
+
+ You may store files that are automatically cached by your Web browser
+ for display enhancement purposes
+
+
+ You may print or download one copy of a reasonable number of pages of
+ the website for your own personal, non-commercial use and not for
+ further reproduction, publication, or distribution
+
Disclaimer of Warranties
-
The Service is provided on an "as is" and "as available" basis, without any warranties of any kind, either
- express or implied, including but not limited to warranties of merchantability, fitness for a particular purpose,
- or non-infringement. We make no warranty that the Service will meet your requirements, be available on an
- uninterrupted, secure, or error-free basis, or be free from viruses or other harmful components.
+
+ The Service is provided on an "as is" and "as available" basis, without
+ any warranties of any kind, either express or implied, including but not
+ limited to warranties of merchantability, fitness for a particular
+ purpose, or non-infringement. We make no warranty that the Service will
+ meet your requirements, be available on an uninterrupted, secure, or
+ error-free basis, or be free from viruses or other harmful components.
+
Limitation of Liability
-
In no event shall we be liable for any direct, indirect, incidental, special, or consequential damages arising
- out of or in any way connected with the use of the Service, whether based on contract, tort, strict liability, or
- any other theory of liability.
+
+ In no event shall we be liable for any direct, indirect, incidental,
+ special, or consequential damages arising out of or in any way connected
+ with the use of the Service, whether based on contract, tort, strict
+ liability, or any other theory of liability.
+
Indemnification
-
You agree to defend, indemnify, and hold us harmless from and against any claims, liabilities, damages,
- judgments, fines, costs, and expenses.
-
+
+ You agree to defend, indemnify, and hold us harmless from and against any
+ claims, liabilities, damages, judgments, fines, costs, and expenses.
+
diff --git a/plugins/cookieconsent.client.ts b/plugins/cookieconsent.client.ts
index 383566d..8419079 100644
--- a/plugins/cookieconsent.client.ts
+++ b/plugins/cookieconsent.client.ts
@@ -1,12 +1,12 @@
-import "vanilla-cookieconsent/dist/cookieconsent.css";
-import "vanilla-cookieconsent/src/cookieconsent.js";
+import 'vanilla-cookieconsent/dist/cookieconsent.css';
+import 'vanilla-cookieconsent/src/cookieconsent.js';
-export default defineNuxtPlugin((nuxtApp) => {
+export default defineNuxtPlugin(nuxtApp => {
// @ts-ignore
const cookieConsent = window.initCookieConsent();
cookieConsent.run({
- current_lang: "en",
+ current_lang: 'en',
autoclear_cookies: true, // default: false
page_scripts: true, // default: false
@@ -41,91 +41,91 @@ export default defineNuxtPlugin((nuxtApp) => {
languages: {
en: {
consent_modal: {
- title: "We use cookies!",
+ title: 'We use cookies!',
description:
'Hi, this website uses essential cookies to ensure its proper operation and tracking cookies to understand how you interact with it. The latter will be set only after consent. Let me choose',
primary_btn: {
- text: "Accept all",
- role: "accept_all", // 'accept_selected' or 'accept_all'
+ text: 'Accept all',
+ role: 'accept_all' // 'accept_selected' or 'accept_all'
},
secondary_btn: {
- text: "Reject all",
- role: "accept_necessary", // 'settings' or 'accept_necessary'
- },
+ text: 'Reject all',
+ role: 'accept_necessary' // 'settings' or 'accept_necessary'
+ }
},
settings_modal: {
- title: "Cookie preferences",
- save_settings_btn: "Save settings",
- accept_all_btn: "Accept all",
- reject_all_btn: "Reject all",
- close_btn_label: "Close",
+ title: 'Cookie preferences',
+ save_settings_btn: 'Save settings',
+ accept_all_btn: 'Accept all',
+ reject_all_btn: 'Reject all',
+ close_btn_label: 'Close',
// cookie_table_caption: 'Cookie list',
cookie_table_headers: [
- { col1: "Name" },
- { col2: "Domain" },
- { col3: "Expiration" },
- { col4: "Description" },
+ { col1: 'Name' },
+ { col2: 'Domain' },
+ { col3: 'Expiration' },
+ { col4: 'Description' }
],
blocks: [
{
- title: "Cookie usage 📢",
+ title: 'Cookie usage 📢',
description:
- 'I use cookies to ensure the basic functionalities of the website and to enhance your online experience. You can choose for each category to opt-in/out whenever you want. For more details relative to cookies and other sensitive data, please read the full privacy policy.',
+ 'I use cookies to ensure the basic functionalities of the website and to enhance your online experience. You can choose for each category to opt-in/out whenever you want. For more details relative to cookies and other sensitive data, please read the full privacy policy.'
},
{
- title: "Strictly necessary cookies",
+ title: 'Strictly necessary cookies',
description:
- "These cookies are essential for the proper functioning of my website. Without these cookies, the website would not work properly",
+ 'These cookies are essential for the proper functioning of my website. Without these cookies, the website would not work properly',
toggle: {
- value: "necessary",
+ value: 'necessary',
enabled: true,
- readonly: true, // cookie categories with readonly=true are all treated as "necessary cookies"
- },
+ readonly: true // cookie categories with readonly=true are all treated as "necessary cookies"
+ }
},
{
- title: "Performance and Analytics cookies",
+ title: 'Performance and Analytics cookies',
description:
- "These cookies allow the website to remember the choices you have made in the past",
+ 'These cookies allow the website to remember the choices you have made in the past',
toggle: {
- value: "analytics", // your cookie category
+ value: 'analytics', // your cookie category
enabled: false,
- readonly: false,
+ readonly: false
},
cookie_table: [
// list of all expected cookies
{
- col1: "^_ga", // match all cookies starting with "_ga"
- col2: "google.com",
- col3: "2 years",
- col4: "description ...",
- is_regex: true,
+ col1: '^_ga', // match all cookies starting with "_ga"
+ col2: 'google.com',
+ col3: '2 years',
+ col4: 'description ...',
+ is_regex: true
},
{
- col1: "_gid",
- col2: "google.com",
- col3: "1 day",
- col4: "description ...",
- },
- ],
+ col1: '_gid',
+ col2: 'google.com',
+ col3: '1 day',
+ col4: 'description ...'
+ }
+ ]
},
{
- title: "Advertisement and Targeting cookies",
+ title: 'Advertisement and Targeting cookies',
description:
- "These cookies collect information about how you use the website, which pages you visited and which links you clicked on. All of the data is anonymized and cannot be used to identify you",
+ 'These cookies collect information about how you use the website, which pages you visited and which links you clicked on. All of the data is anonymized and cannot be used to identify you',
toggle: {
- value: "targeting",
+ value: 'targeting',
enabled: false,
- readonly: false,
- },
+ readonly: false
+ }
},
{
- title: "More information",
+ title: 'More information',
description:
- 'For any queries in relation to our policy on cookies and your choices, please contact us.',
- },
- ],
- },
- },
- },
+ 'For any queries in relation to our policy on cookies and your choices, please contact us.'
+ }
+ ]
+ }
+ }
+ }
});
});
diff --git a/plugins/trpcClient.ts b/plugins/trpcClient.ts
index f07151f..69e779e 100644
--- a/plugins/trpcClient.ts
+++ b/plugins/trpcClient.ts
@@ -1,6 +1,6 @@
-import { createTRPCNuxtClient, httpBatchLink } from "trpc-nuxt/client";
-import type { AppRouter } from "~/server/trpc/routers/app.router";
-import superjson from "superjson";
+import { createTRPCNuxtClient, httpBatchLink } from 'trpc-nuxt/client';
+import type { AppRouter } from '~/server/trpc/routers/app.router';
+import superjson from 'superjson';
export default defineNuxtPlugin(() => {
/**
@@ -10,15 +10,15 @@ export default defineNuxtPlugin(() => {
const client = createTRPCNuxtClient({
links: [
httpBatchLink({
- url: "/api/trpc",
- }),
+ url: '/api/trpc'
+ })
],
- transformer: superjson,
+ transformer: superjson
});
return {
provide: {
- client,
- },
+ client
+ }
};
});
diff --git a/prisma/account-access-enum.ts b/prisma/account-access-enum.ts
index acb4e86..c957645 100644
--- a/prisma/account-access-enum.ts
+++ b/prisma/account-access-enum.ts
@@ -1,15 +1,15 @@
// Workaround for prisma issue (https://github.com/prisma/prisma/issues/12504#issuecomment-1147356141)
// Import original enum as type
-import type { ACCOUNT_ACCESS as ACCOUNT_ACCESS_ORIGINAL } from '@prisma/client'
+import type { ACCOUNT_ACCESS as ACCOUNT_ACCESS_ORIGINAL } from '@prisma/client';
// Guarantee that the implementation corresponds to the original type
-export const ACCOUNT_ACCESS: { [k in ACCOUNT_ACCESS_ORIGINAL ]: k } = {
+export const ACCOUNT_ACCESS: { [k in ACCOUNT_ACCESS_ORIGINAL]: k } = {
READ_ONLY: 'READ_ONLY',
READ_WRITE: 'READ_WRITE',
ADMIN: 'ADMIN',
OWNER: 'OWNER'
-} as const
+} as const;
// Re-exporting the original type with the original name
-export type ACCOUNT_ACCESS = ACCOUNT_ACCESS_ORIGINAL
\ No newline at end of file
+export type ACCOUNT_ACCESS = ACCOUNT_ACCESS_ORIGINAL;
diff --git a/prisma/prisma.client.ts b/prisma/prisma.client.ts
index c1ce66a..2a25d2e 100644
--- a/prisma/prisma.client.ts
+++ b/prisma/prisma.client.ts
@@ -1,5 +1,5 @@
-import pkg from "@prisma/client";
+import pkg from '@prisma/client';
const { PrismaClient } = pkg;
-const prisma_client = new PrismaClient()
-export default prisma_client
\ No newline at end of file
+const prisma_client = new PrismaClient();
+export default prisma_client;
diff --git a/prisma/seed.ts b/prisma/seed.ts
index 570a4b3..b0a790f 100644
--- a/prisma/seed.ts
+++ b/prisma/seed.ts
@@ -1,5 +1,5 @@
-import { PrismaClient } from '@prisma/client'
-const prisma = new PrismaClient()
+import { PrismaClient } from '@prisma/client';
+const prisma = new PrismaClient();
async function main() {
const freeTrial = await prisma.plan.upsert({
where: { name: 'Free Trial' },
@@ -9,8 +9,8 @@ async function main() {
features: ['ADD_NOTES', 'EDIT_NOTES', 'VIEW_NOTES'],
max_notes: 10,
max_members: 1,
- ai_gen_max_pm: 7,
- },
+ ai_gen_max_pm: 7
+ }
});
const individualPlan = await prisma.plan.upsert({
where: { name: 'Individual Plan' },
@@ -22,29 +22,35 @@ async function main() {
max_members: 1,
ai_gen_max_pm: 50,
stripe_product_id: 'prod_NQR7vwUulvIeqW'
- },
+ }
});
const teamPlan = await prisma.plan.upsert({
where: { name: 'Team Plan' },
update: {},
create: {
name: 'Team Plan',
- features: ['ADD_NOTES', 'EDIT_NOTES', 'VIEW_NOTES', 'SPECIAL_FEATURE', 'SPECIAL_TEAM_FEATURE'],
+ features: [
+ 'ADD_NOTES',
+ 'EDIT_NOTES',
+ 'VIEW_NOTES',
+ 'SPECIAL_FEATURE',
+ 'SPECIAL_TEAM_FEATURE'
+ ],
max_notes: 200,
max_members: 10,
ai_gen_max_pm: 500,
stripe_product_id: 'prod_NQR8IkkdhqBwu2'
- },
+ }
});
- console.log({ freeTrial, individualPlan, teamPlan })
+ console.log({ freeTrial, individualPlan, teamPlan });
}
main()
.then(async () => {
- await prisma.$disconnect()
+ await prisma.$disconnect();
})
- .catch(async (e) => {
- console.error(e)
- await prisma.$disconnect()
- process.exit(1)
- })
\ No newline at end of file
+ .catch(async e => {
+ console.error(e);
+ await prisma.$disconnect();
+ process.exit(1);
+ });
diff --git a/server/api/note.ts b/server/api/note.ts
index e5defb7..19e0edb 100644
--- a/server/api/note.ts
+++ b/server/api/note.ts
@@ -4,10 +4,10 @@ import NotesService from '~/lib/services/notes.service';
// Example API Route with query params ... /api/note?note_id=41
export default defineProtectedEventHandler(async (event: H3Event) => {
- const queryParams = getQuery(event)
+ const queryParams = getQuery(event);
let note_id: string = '';
- if(queryParams.note_id){
- if (Array.isArray( queryParams.note_id)) {
+ if (queryParams.note_id) {
+ if (Array.isArray(queryParams.note_id)) {
note_id = queryParams.note_id[0];
} else {
note_id = queryParams.note_id.toString();
@@ -15,9 +15,9 @@ export default defineProtectedEventHandler(async (event: H3Event) => {
}
const notesService = new NotesService();
- const note = await notesService.getNoteById(+note_id);
+ const note = await notesService.getNoteById(+note_id);
return {
- note,
- }
-})
\ No newline at end of file
+ note
+ };
+});
diff --git a/server/api/trpc/[trpc].ts b/server/api/trpc/[trpc].ts
index 3cd6ad5..26689c9 100644
--- a/server/api/trpc/[trpc].ts
+++ b/server/api/trpc/[trpc].ts
@@ -2,9 +2,9 @@
* This is the API-handler of your app that contains all your API routes.
* On a bigger app, you will probably want to split this file up into multiple files.
*/
-import { createNuxtApiHandler } from "trpc-nuxt";
-import { createContext } from "~~/server/trpc/context";
-import { appRouter } from "~~/server/trpc/routers/app.router";
+import { createNuxtApiHandler } from 'trpc-nuxt';
+import { createContext } from '~~/server/trpc/context';
+import { appRouter } from '~~/server/trpc/routers/app.router';
// export API handler
export default createNuxtApiHandler({
@@ -12,5 +12,5 @@ export default createNuxtApiHandler({
createContext: createContext,
onError({ error }) {
console.error(error);
- },
+ }
});
diff --git a/server/defineProtectedEventHandler.ts b/server/defineProtectedEventHandler.ts
index 0701b46..1c5bbd1 100644
--- a/server/defineProtectedEventHandler.ts
+++ b/server/defineProtectedEventHandler.ts
@@ -1,4 +1,4 @@
-import { EventHandler, EventHandlerRequest, H3Event, eventHandler } from "h3";
+import { EventHandler, EventHandlerRequest, H3Event, eventHandler } from 'h3';
export const defineProtectedEventHandler = (
handler: EventHandler
@@ -8,7 +8,7 @@ export const defineProtectedEventHandler = (
return eventHandler((event: H3Event) => {
const user = event.context.user;
if (!user) {
- throw createError({ statusCode: 401, statusMessage: "Unauthenticated" });
+ throw createError({ statusCode: 401, statusMessage: 'Unauthenticated' });
}
return handler(event);
});
diff --git a/server/middleware/authContext.ts b/server/middleware/authContext.ts
index 16aec84..086fb78 100644
--- a/server/middleware/authContext.ts
+++ b/server/middleware/authContext.ts
@@ -1,49 +1,65 @@
-import { defineEventHandler, parseCookies, setCookie, getCookie } from 'h3'
-import { serverSupabaseUser } from "#supabase/server";
+import { defineEventHandler, parseCookies, setCookie, getCookie } from 'h3';
+import { serverSupabaseUser } from '#supabase/server';
import AuthService from '~/lib/services/auth.service';
import { User } from '@supabase/supabase-js';
-import { FullDBUser } from "~~/lib/services/service.types";
+import { FullDBUser } from '~~/lib/services/service.types';
// Explicitly type our context by 'Merging' our custom types with the H3EventContext (https://stackoverflow.com/a/76349232/95242)
declare module 'h3' {
interface H3EventContext {
- user?: User; // the Supabase User
- dbUser?: FullDBUser; // the corresponding Database User
+ user?: User; // the Supabase User
+ dbUser?: FullDBUser; // the corresponding Database User
activeAccountId?: number; // the account ID that is active for the user
}
}
-export default defineEventHandler(async (event) => {
- const cookies = parseCookies(event)
- if(cookies && cookies['sb-access-token']){
+export default defineEventHandler(async event => {
+ const cookies = parseCookies(event);
+ if (cookies && cookies['sb-access-token']) {
const user = await serverSupabaseUser(event);
if (user) {
event.context.user = user;
const authService = new AuthService();
let dbUser = await authService.getFullUserBySupabaseId(user.id);
-
+
if (!dbUser && user) {
- dbUser = await authService.createUser(user.id, user.user_metadata.full_name?user.user_metadata.full_name:"no name supplied", user.email?user.email:"no@email.supplied" );
+ dbUser = await authService.createUser(
+ user.id,
+ user.user_metadata.full_name
+ ? user.user_metadata.full_name
+ : 'no name supplied',
+ user.email ? user.email : 'no@email.supplied'
+ );
console.log(`\n Created DB User \n ${JSON.stringify(dbUser)}\n`);
}
-
- if(dbUser){
+
+ if (dbUser) {
event.context.dbUser = dbUser;
let activeAccountId;
- const preferredAccountId = getCookie(event, 'preferred-active-account-id')
- if(preferredAccountId && dbUser?.memberships.find(m => m.account_id === +preferredAccountId && !m.pending)){
- activeAccountId = +preferredAccountId
+ const preferredAccountId = getCookie(
+ event,
+ 'preferred-active-account-id'
+ );
+ if (
+ preferredAccountId &&
+ dbUser?.memberships.find(
+ m => m.account_id === +preferredAccountId && !m.pending
+ )
+ ) {
+ activeAccountId = +preferredAccountId;
} else {
const defaultActive = dbUser.memberships[0].account_id.toString();
- setCookie(event, 'preferred-active-account-id', defaultActive, {expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365 * 10)});
+ setCookie(event, 'preferred-active-account-id', defaultActive, {
+ expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365 * 10)
+ });
activeAccountId = +defaultActive;
}
- if(activeAccountId){
+ if (activeAccountId) {
event.context.activeAccountId = activeAccountId;
}
}
}
}
-});
\ No newline at end of file
+});
diff --git a/server/routes/create-checkout-session.post.ts b/server/routes/create-checkout-session.post.ts
index 701fb8b..aafe408 100644
--- a/server/routes/create-checkout-session.post.ts
+++ b/server/routes/create-checkout-session.post.ts
@@ -6,20 +6,31 @@ import { AccountWithMembers } from '~~/lib/services/service.types';
const config = useRuntimeConfig();
const stripe = new Stripe(config.stripeSecretKey, { apiVersion: '2022-11-15' });
-export default defineEventHandler(async (event) => {
- const body = await readBody(event)
- let { price_id, account_id} = body;
- account_id = +account_id
- console.log(`session.post.ts recieved price_id:${price_id}, account_id:${account_id}`);
+export default defineEventHandler(async event => {
+ const body = await readBody(event);
+ let { price_id, account_id } = body;
+ account_id = +account_id;
+ console.log(
+ `session.post.ts recieved price_id:${price_id}, account_id:${account_id}`
+ );
const accountService = new AccountService();
- const account: AccountWithMembers = await accountService.getAccountById(account_id);
- let customer_id: string
- if(!account.stripe_customer_id){
+ const account: AccountWithMembers = await accountService.getAccountById(
+ account_id
+ );
+ let customer_id: string;
+ if (!account.stripe_customer_id) {
// need to pre-emptively create a Stripe user for this account so we know who they are when the webhook comes back
- const owner = account.members.find(member => (member.access == ACCOUNT_ACCESS.OWNER))
- console.log(`Creating account with name ${account.name} and email ${owner?.user.email}`);
- const customer = await stripe.customers.create({ name: account.name, email: owner?.user.email });
+ const owner = account.members.find(
+ member => member.access == ACCOUNT_ACCESS.OWNER
+ );
+ console.log(
+ `Creating account with name ${account.name} and email ${owner?.user.email}`
+ );
+ const customer = await stripe.customers.create({
+ name: account.name,
+ email: owner?.user.email
+ });
customer_id = customer.id;
accountService.updateAccountStipeCustomerId(account_id, customer.id);
} else {
@@ -31,8 +42,8 @@ export default defineEventHandler(async (event) => {
line_items: [
{
price: price_id,
- quantity: 1,
- },
+ quantity: 1
+ }
],
// {CHECKOUT_SESSION_ID} is a string literal; do not change it!
// the actual Session ID is returned in the query parameter when your customer
@@ -42,11 +53,9 @@ export default defineEventHandler(async (event) => {
customer: customer_id
});
- if(session?.url){
+ if (session?.url) {
return sendRedirect(event, session.url, 303);
} else {
return sendRedirect(event, `${config.public.siteRootUrl}/fail`, 303);
}
});
-
-
diff --git a/server/routes/webhook.post.ts b/server/routes/webhook.post.ts
index 7b6029b..ca226b8 100644
--- a/server/routes/webhook.post.ts
+++ b/server/routes/webhook.post.ts
@@ -4,46 +4,77 @@ import AccountService from '~~/lib/services/account.service';
const config = useRuntimeConfig();
const stripe = new Stripe(config.stripeSecretKey, { apiVersion: '2022-11-15' });
-export default defineEventHandler(async (event) => {
+export default defineEventHandler(async event => {
const stripeSignature = getRequestHeader(event, 'stripe-signature');
- if(!stripeSignature){
- throw createError({ statusCode: 400, statusMessage: 'Webhook Error: No stripe signature in header' });
+ if (!stripeSignature) {
+ throw createError({
+ statusCode: 400,
+ statusMessage: 'Webhook Error: No stripe signature in header'
+ });
}
- const rawBody = await readRawBody(event)
- if(!rawBody){
- throw createError({ statusCode: 400, statusMessage: 'Webhook Error: No body' });
+ const rawBody = await readRawBody(event);
+ if (!rawBody) {
+ throw createError({
+ statusCode: 400,
+ statusMessage: 'Webhook Error: No body'
+ });
}
let stripeEvent: Stripe.Event;
try {
- stripeEvent = stripe.webhooks.constructEvent(rawBody, stripeSignature, config.stripeEndpointSecret);
- }
- catch (err) {
+ stripeEvent = stripe.webhooks.constructEvent(
+ rawBody,
+ stripeSignature,
+ config.stripeEndpointSecret
+ );
+ } catch (err) {
console.log(err);
- throw createError({ statusCode: 400, statusMessage: `Error validating Webhook Event` });
+ throw createError({
+ statusCode: 400,
+ statusMessage: `Error validating Webhook Event`
+ });
}
- if(stripeEvent.type && stripeEvent.type.startsWith('customer.subscription')){
+ if (
+ stripeEvent.type &&
+ stripeEvent.type.startsWith('customer.subscription')
+ ) {
console.log(`****** Web Hook Recieved (${stripeEvent.type}) ******`);
- let subscription = stripeEvent.data.object as Stripe.Subscription;
- if(subscription.status == 'active'){
- const sub_item = subscription.items.data.find(item => item?.object && item?.object == 'subscription_item')
-
+ let subscription = stripeEvent.data.object as Stripe.Subscription;
+ if (subscription.status == 'active') {
+ const sub_item = subscription.items.data.find(
+ item => item?.object && item?.object == 'subscription_item'
+ );
+
const stripe_product_id = sub_item?.plan.product?.toString(); // TODO - is the product ever a product object and in that case should I check for deleted?
- if(!stripe_product_id){
- throw createError({ statusCode: 400, statusMessage: `Error validating Webhook Event` });
+ if (!stripe_product_id) {
+ throw createError({
+ statusCode: 400,
+ statusMessage: `Error validating Webhook Event`
+ });
}
-
+
const accountService = new AccountService();
-
- let current_period_ends: Date = new Date(subscription.current_period_end * 1000);
- current_period_ends.setDate(current_period_ends.getDate() + config.subscriptionGraceDays);
-
- console.log(`updating stripe sub details subscription.current_period_end:${subscription.current_period_end}, subscription.id:${subscription.id}, stripe_product_id:${stripe_product_id}`);
- accountService.updateStripeSubscriptionDetailsForAccount(subscription.customer.toString(), subscription.id, current_period_ends, stripe_product_id);
+
+ let current_period_ends: Date = new Date(
+ subscription.current_period_end * 1000
+ );
+ current_period_ends.setDate(
+ current_period_ends.getDate() + config.subscriptionGraceDays
+ );
+
+ console.log(
+ `updating stripe sub details subscription.current_period_end:${subscription.current_period_end}, subscription.id:${subscription.id}, stripe_product_id:${stripe_product_id}`
+ );
+ accountService.updateStripeSubscriptionDetailsForAccount(
+ subscription.customer.toString(),
+ subscription.id,
+ current_period_ends,
+ stripe_product_id
+ );
}
}
return `handled ${stripeEvent.type}.`;
-});
\ No newline at end of file
+});
diff --git a/server/trpc/context.ts b/server/trpc/context.ts
index 0459c2d..3d4a05e 100644
--- a/server/trpc/context.ts
+++ b/server/trpc/context.ts
@@ -1,13 +1,13 @@
-import { inferAsyncReturnType } from '@trpc/server'
+import { inferAsyncReturnType } from '@trpc/server';
import { H3Event } from 'h3';
-export async function createContext(event: H3Event){
+export async function createContext(event: H3Event) {
return {
- user: event.context.user, // the Supabase User
- dbUser: event.context.dbUser, // the corresponding Database User
+ user: event.context.user, // the Supabase User
+ dbUser: event.context.dbUser, // the corresponding Database User
activeAccountId: event.context.activeAccountId, // the account ID that is active for the user
- event, // required to enable setCookie in accountRouter
- }
-};
+ event // required to enable setCookie in accountRouter
+ };
+}
-export type Context = inferAsyncReturnType
\ No newline at end of file
+export type Context = inferAsyncReturnType;
diff --git a/server/trpc/routers/account.router.ts b/server/trpc/routers/account.router.ts
index 7feef54..eb03b6b 100644
--- a/server/trpc/routers/account.router.ts
+++ b/server/trpc/routers/account.router.ts
@@ -1,5 +1,11 @@
import { TRPCError } from '@trpc/server';
-import { router, adminProcedure, publicProcedure, protectedProcedure, ownerProcedure } from '../trpc'
+import {
+ router,
+ adminProcedure,
+ publicProcedure,
+ protectedProcedure,
+ ownerProcedure
+} from '../trpc';
import { ACCOUNT_ACCESS } from '~~/prisma/account-access-enum';
import { z } from 'zod';
import AccountService from '~~/lib/services/account.service';
@@ -9,113 +15,161 @@ import { MembershipWithAccount } from '~~/lib/services/service.types';
Note on proliferation of Bang syntax... adminProcedure throws if either the ctx.dbUser or the ctx.activeAccountId is not available but the compiler can't figure that out so bang quiesces the null warning
*/
export const accountRouter = router({
- getDBUser: publicProcedure
- .query(({ ctx }) => {
- return {
- dbUser: ctx.dbUser,
- }
- }),
- getActiveAccountId: publicProcedure
- .query(({ ctx }) => {
- return {
- activeAccountId: ctx.activeAccountId,
- }
- }),
+ getDBUser: publicProcedure.query(({ ctx }) => {
+ return {
+ dbUser: ctx.dbUser
+ };
+ }),
+ getActiveAccountId: publicProcedure.query(({ ctx }) => {
+ return {
+ activeAccountId: ctx.activeAccountId
+ };
+ }),
changeActiveAccount: protectedProcedure
.input(z.object({ account_id: z.number() }))
.mutation(async ({ ctx, input }) => {
- const activeMembership = ctx.dbUser?.memberships.find(membership => membership.account_id == input.account_id);
- if(activeMembership?.pending){
- throw new TRPCError({ code: 'BAD_REQUEST', message:`membership ${activeMembership?.id} is not active so cannot be switched to` });
+ const activeMembership = ctx.dbUser?.memberships.find(
+ membership => membership.account_id == input.account_id
+ );
+ if (activeMembership?.pending) {
+ throw new TRPCError({
+ code: 'BAD_REQUEST',
+ message: `membership ${activeMembership?.id} is not active so cannot be switched to`
+ });
}
ctx.activeAccountId = input.account_id;
- setCookie(ctx.event, 'preferred-active-account-id', input.account_id.toString(), {expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365 * 10)});
+ setCookie(
+ ctx.event,
+ 'preferred-active-account-id',
+ input.account_id.toString(),
+ { expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365 * 10) }
+ );
}),
changeAccountName: adminProcedure
.input(z.object({ new_name: z.string() }))
.mutation(async ({ ctx, input }) => {
const accountService = new AccountService();
- const account = await accountService.changeAccountName(ctx.activeAccountId!, input.new_name);
+ const account = await accountService.changeAccountName(
+ ctx.activeAccountId!,
+ input.new_name
+ );
return {
- account,
- }
- }),
- rotateJoinPassword: adminProcedure
- .mutation(async ({ ctx }) => {
- const accountService = new AccountService();
- const account = await accountService.rotateJoinPassword(ctx.activeAccountId!);
- return {
- account,
- }
+ account
+ };
}),
+ rotateJoinPassword: adminProcedure.mutation(async ({ ctx }) => {
+ const accountService = new AccountService();
+ const account = await accountService.rotateJoinPassword(
+ ctx.activeAccountId!
+ );
+ return {
+ account
+ };
+ }),
getAccountByJoinPassword: publicProcedure
.input(z.object({ join_password: z.string() }))
.query(async ({ input }) => {
const accountService = new AccountService();
- const account = await accountService.getAccountByJoinPassword(input.join_password);
+ const account = await accountService.getAccountByJoinPassword(
+ input.join_password
+ );
return {
- account,
- }
+ account
+ };
}),
joinUserToAccountPending: publicProcedure // this uses a passed account id rather than using the active account because user is usually active on their personal or some other account when they attempt to join a new account
.input(z.object({ account_id: z.number(), user_id: z.number() }))
.mutation(async ({ input }) => {
const accountService = new AccountService();
- const membership: MembershipWithAccount = await accountService.joinUserToAccount(input.user_id, input.account_id, true);
+ const membership: MembershipWithAccount =
+ await accountService.joinUserToAccount(
+ input.user_id,
+ input.account_id,
+ true
+ );
return {
- membership,
- }
+ membership
+ };
}),
acceptPendingMembership: adminProcedure
.input(z.object({ membership_id: z.number() }))
.query(async ({ ctx, input }) => {
const accountService = new AccountService();
- const membership: MembershipWithAccount = await accountService.acceptPendingMembership(ctx.activeAccountId!, input.membership_id);
+ const membership: MembershipWithAccount =
+ await accountService.acceptPendingMembership(
+ ctx.activeAccountId!,
+ input.membership_id
+ );
return {
- membership,
- }
+ membership
+ };
}),
rejectPendingMembership: adminProcedure
.input(z.object({ membership_id: z.number() }))
.query(async ({ ctx, input }) => {
const accountService = new AccountService();
- const membership: MembershipWithAccount = await accountService.deleteMembership(ctx.activeAccountId!, input.membership_id);
+ const membership: MembershipWithAccount =
+ await accountService.deleteMembership(
+ ctx.activeAccountId!,
+ input.membership_id
+ );
return {
- membership,
- }
+ membership
+ };
}),
deleteMembership: ownerProcedure
.input(z.object({ membership_id: z.number() }))
.query(async ({ ctx, input }) => {
const accountService = new AccountService();
- const membership: MembershipWithAccount = await accountService.deleteMembership(ctx.activeAccountId!, input.membership_id);
+ const membership: MembershipWithAccount =
+ await accountService.deleteMembership(
+ ctx.activeAccountId!,
+ input.membership_id
+ );
return {
- membership,
- }
+ membership
+ };
}),
changeUserAccessWithinAccount: adminProcedure
- .input(z.object({ user_id: z.number(), access: z.enum([ACCOUNT_ACCESS.ADMIN, ACCOUNT_ACCESS.OWNER, ACCOUNT_ACCESS.READ_ONLY, ACCOUNT_ACCESS.READ_WRITE]) }))
+ .input(
+ z.object({
+ user_id: z.number(),
+ access: z.enum([
+ ACCOUNT_ACCESS.ADMIN,
+ ACCOUNT_ACCESS.OWNER,
+ ACCOUNT_ACCESS.READ_ONLY,
+ ACCOUNT_ACCESS.READ_WRITE
+ ])
+ })
+ )
.mutation(async ({ ctx, input }) => {
const accountService = new AccountService();
- const membership = await accountService.changeUserAccessWithinAccount(input.user_id, ctx.activeAccountId!, input.access);
+ const membership = await accountService.changeUserAccessWithinAccount(
+ input.user_id,
+ ctx.activeAccountId!,
+ input.access
+ );
return {
- membership,
- }
+ membership
+ };
}),
- claimOwnershipOfAccount: adminProcedure
- .mutation(async ({ ctx }) => {
- const accountService = new AccountService();
- const memberships = await accountService.claimOwnershipOfAccount(ctx.dbUser!.id, ctx.activeAccountId!);
- return {
- memberships,
- }
- }),
- getAccountMembers: adminProcedure
- .query(async ({ ctx }) => {
- const accountService = new AccountService();
- const memberships = await accountService.getAccountMembers(ctx.activeAccountId!);
- return {
- memberships,
- }
- }),
-})
+ claimOwnershipOfAccount: adminProcedure.mutation(async ({ ctx }) => {
+ const accountService = new AccountService();
+ const memberships = await accountService.claimOwnershipOfAccount(
+ ctx.dbUser!.id,
+ ctx.activeAccountId!
+ );
+ return {
+ memberships
+ };
+ }),
+ getAccountMembers: adminProcedure.query(async ({ ctx }) => {
+ const accountService = new AccountService();
+ const memberships = await accountService.getAccountMembers(
+ ctx.activeAccountId!
+ );
+ return {
+ memberships
+ };
+ })
+});
diff --git a/server/trpc/routers/app.router.ts b/server/trpc/routers/app.router.ts
index 2ef772a..24c3880 100644
--- a/server/trpc/routers/app.router.ts
+++ b/server/trpc/routers/app.router.ts
@@ -1,12 +1,12 @@
-import { router } from "~/server/trpc/trpc";
-import { notesRouter } from "./notes.router";
-import { authRouter } from "./auth.router";
-import { accountRouter } from "./account.router";
+import { router } from '~/server/trpc/trpc';
+import { notesRouter } from './notes.router';
+import { authRouter } from './auth.router';
+import { accountRouter } from './account.router';
export const appRouter = router({
notes: notesRouter,
auth: authRouter,
- account: accountRouter,
+ account: accountRouter
});
// export only the type definition of the API
diff --git a/server/trpc/routers/auth.router.ts b/server/trpc/routers/auth.router.ts
index d49a4d6..aadbdb4 100644
--- a/server/trpc/routers/auth.router.ts
+++ b/server/trpc/routers/auth.router.ts
@@ -1,10 +1,9 @@
-import { publicProcedure, router } from '../trpc'
+import { publicProcedure, router } from '../trpc';
export const authRouter = router({
- getDBUser: publicProcedure
- .query(({ ctx }) => {
- return {
- dbUser: ctx.dbUser,
- }
- }),
-})
+ getDBUser: publicProcedure.query(({ ctx }) => {
+ return {
+ dbUser: ctx.dbUser
+ };
+ })
+});
diff --git a/server/trpc/routers/notes.router.ts b/server/trpc/routers/notes.router.ts
index 484d4f7..44f7d28 100644
--- a/server/trpc/routers/notes.router.ts
+++ b/server/trpc/routers/notes.router.ts
@@ -1,50 +1,68 @@
import NotesService from '~~/lib/services/notes.service';
-import { accountHasSpecialFeature, adminProcedure, memberProcedure, publicProcedure, readWriteProcedure, router } from '../trpc';
+import {
+ accountHasSpecialFeature,
+ adminProcedure,
+ memberProcedure,
+ publicProcedure,
+ readWriteProcedure,
+ router
+} from '../trpc';
import { z } from 'zod';
export const notesRouter = router({
- getForActiveAccount: memberProcedure
- .query(async ({ ctx, input }) => {
- const notesService = new NotesService();
- const notes = (ctx.activeAccountId)?await notesService.getNotesForAccountId(ctx.activeAccountId):[];
- return {
- notes,
- }
- }),
+ getForActiveAccount: memberProcedure.query(async ({ ctx, input }) => {
+ const notesService = new NotesService();
+ const notes = ctx.activeAccountId
+ ? await notesService.getNotesForAccountId(ctx.activeAccountId)
+ : [];
+ return {
+ notes
+ };
+ }),
getById: publicProcedure
.input(z.object({ note_id: z.number() }))
.query(async ({ ctx, input }) => {
const notesService = new NotesService();
- const note = await notesService.getNoteById(input.note_id);
+ const note = await notesService.getNoteById(input.note_id);
return {
- note,
- }
+ note
+ };
}),
createNote: readWriteProcedure
.input(z.object({ note_text: z.string() }))
.mutation(async ({ ctx, input }) => {
const notesService = new NotesService();
- const note = (ctx.activeAccountId)?await notesService.createNote(ctx.activeAccountId, input.note_text):null;
+ const note = ctx.activeAccountId
+ ? await notesService.createNote(ctx.activeAccountId, input.note_text)
+ : null;
return {
- note,
- }
+ note
+ };
}),
deleteNote: adminProcedure
.input(z.object({ note_id: z.number() }))
.mutation(async ({ ctx, input }) => {
const notesService = new NotesService();
- const note = (ctx.activeAccountId)?await notesService.deleteNote(input.note_id):null;
+ const note = ctx.activeAccountId
+ ? await notesService.deleteNote(input.note_id)
+ : null;
return {
- note,
- }
+ note
+ };
}),
- generateAINoteFromPrompt: readWriteProcedure.use(accountHasSpecialFeature)
+ generateAINoteFromPrompt: readWriteProcedure
+ .use(accountHasSpecialFeature)
.input(z.object({ user_prompt: z.string() }))
.query(async ({ ctx, input }) => {
const notesService = new NotesService();
- const noteText = (ctx.activeAccountId)?await notesService.generateAINoteFromPrompt(input.user_prompt, ctx.activeAccountId):null;
+ const noteText = ctx.activeAccountId
+ ? await notesService.generateAINoteFromPrompt(
+ input.user_prompt,
+ ctx.activeAccountId
+ )
+ : null;
return {
noteText
- }
- }),
-})
\ No newline at end of file
+ };
+ })
+});
diff --git a/server/trpc/trpc.ts b/server/trpc/trpc.ts
index cff0e19..9c56c49 100644
--- a/server/trpc/trpc.ts
+++ b/server/trpc/trpc.ts
@@ -7,7 +7,7 @@
* @see https://trpc.io/docs/v10/router
* @see https://trpc.io/docs/v10/procedures
*/
-import { initTRPC, TRPCError } from '@trpc/server'
+import { initTRPC, TRPCError } from '@trpc/server';
import { Context } from './context';
import { ACCOUNT_ACCESS } from '~~/prisma/account-access-enum';
import superjson from 'superjson';
@@ -15,7 +15,7 @@ import { AccountLimitError } from '~~/lib/services/errors';
const t = initTRPC.context().create({
transformer: superjson,
- errorFormatter: (opts)=> {
+ errorFormatter: opts => {
const { shape, error } = opts;
if (!(error.cause instanceof AccountLimitError)) {
return shape;
@@ -26,10 +26,10 @@ const t = initTRPC.context().create({
...shape.data,
httpStatus: 401,
code: 'UNAUTHORIZED'
- },
+ }
};
}
-})
+});
/**
* auth middlewares
@@ -40,58 +40,97 @@ const isAuthed = t.middleware(({ next, ctx }) => {
}
return next({
ctx: {
- user: ctx.user,
- },
+ user: ctx.user
+ }
});
});
+const isMemberWithAccessesForActiveAccountId = (access: ACCOUNT_ACCESS[]) =>
+ t.middleware(({ next, ctx }) => {
+ if (!ctx.dbUser || !ctx.activeAccountId) {
+ throw new TRPCError({
+ code: 'UNAUTHORIZED',
+ message: 'no user or active account information was found'
+ });
+ }
+ const activeMembership = ctx.dbUser.memberships.find(
+ membership => membership.account_id == ctx.activeAccountId
+ );
-const isMemberWithAccessesForActiveAccountId = (access: ACCOUNT_ACCESS[]) => t.middleware(({ next, ctx }) => {
- if (!ctx.dbUser || !ctx.activeAccountId) {
- throw new TRPCError({ code: 'UNAUTHORIZED', message: 'no user or active account information was found' });
- }
- const activeMembership = ctx.dbUser.memberships.find(membership => membership.account_id == ctx.activeAccountId);
+ console.log(
+ `isMemberWithAccessesForActiveAccountId(${access}) activeMembership?.access:${activeMembership?.access}`
+ );
- console.log(`isMemberWithAccessesForActiveAccountId(${access}) activeMembership?.access:${activeMembership?.access}`);
+ if (!activeMembership) {
+ throw new TRPCError({
+ code: 'UNAUTHORIZED',
+ message: `user is not a member of the active account`
+ });
+ }
- if(!activeMembership) {
- throw new TRPCError({ code: 'UNAUTHORIZED', message:`user is not a member of the active account` });
- }
+ if (activeMembership.pending) {
+ throw new TRPCError({
+ code: 'UNAUTHORIZED',
+ message: `membership ${activeMembership?.id} is pending approval`
+ });
+ }
- if(activeMembership.pending) {
- throw new TRPCError({ code: 'UNAUTHORIZED', message:`membership ${activeMembership?.id} is pending approval` });
- }
+ if (access.length > 0 && !access.includes(activeMembership.access)) {
+ throw new TRPCError({
+ code: 'UNAUTHORIZED',
+ message: `activeMembership ${activeMembership?.id} has insufficient access (${activeMembership?.access})`
+ });
+ }
- if(access.length > 0 && !access.includes(activeMembership.access)) {
- throw new TRPCError({ code: 'UNAUTHORIZED', message:`activeMembership ${activeMembership?.id} has insufficient access (${activeMembership?.access})` });
- }
-
- return next({ ctx });
-});
+ return next({ ctx });
+ });
-export const isAccountWithFeature = (feature: string) => t.middleware(({ next, ctx }) => {
- if (!ctx.dbUser || !ctx.activeAccountId) {
- throw new TRPCError({ code: 'UNAUTHORIZED' });
- }
- const activeMembership = ctx.dbUser.memberships.find(membership => membership.account_id == ctx.activeAccountId);
+export const isAccountWithFeature = (feature: string) =>
+ t.middleware(({ next, ctx }) => {
+ if (!ctx.dbUser || !ctx.activeAccountId) {
+ throw new TRPCError({ code: 'UNAUTHORIZED' });
+ }
+ const activeMembership = ctx.dbUser.memberships.find(
+ membership => membership.account_id == ctx.activeAccountId
+ );
- console.log(`isAccountWithFeature(${feature}) activeMembership?.account.features:${activeMembership?.account.features}`);
- if(!activeMembership?.account.features.includes(feature)){
- throw new TRPCError({ code: 'UNAUTHORIZED', message: `Account does not have the ${feature} feature` });
- }
-
- return next({ ctx });
-});
+ console.log(
+ `isAccountWithFeature(${feature}) activeMembership?.account.features:${activeMembership?.account.features}`
+ );
+ if (!activeMembership?.account.features.includes(feature)) {
+ throw new TRPCError({
+ code: 'UNAUTHORIZED',
+ message: `Account does not have the ${feature} feature`
+ });
+ }
+
+ return next({ ctx });
+ });
/**
* Procedures
**/
export const publicProcedure = t.procedure;
export const protectedProcedure = t.procedure.use(isAuthed);
-export const memberProcedure = protectedProcedure.use(isMemberWithAccessesForActiveAccountId([]));
-export const readWriteProcedure = protectedProcedure.use(isMemberWithAccessesForActiveAccountId([ACCOUNT_ACCESS.READ_WRITE, ACCOUNT_ACCESS.ADMIN, ACCOUNT_ACCESS.OWNER]));
-export const adminProcedure = protectedProcedure.use(isMemberWithAccessesForActiveAccountId([ACCOUNT_ACCESS.ADMIN, ACCOUNT_ACCESS.OWNER]));
-export const ownerProcedure = protectedProcedure.use(isMemberWithAccessesForActiveAccountId([ACCOUNT_ACCESS.OWNER]));
+export const memberProcedure = protectedProcedure.use(
+ isMemberWithAccessesForActiveAccountId([])
+);
+export const readWriteProcedure = protectedProcedure.use(
+ isMemberWithAccessesForActiveAccountId([
+ ACCOUNT_ACCESS.READ_WRITE,
+ ACCOUNT_ACCESS.ADMIN,
+ ACCOUNT_ACCESS.OWNER
+ ])
+);
+export const adminProcedure = protectedProcedure.use(
+ isMemberWithAccessesForActiveAccountId([
+ ACCOUNT_ACCESS.ADMIN,
+ ACCOUNT_ACCESS.OWNER
+ ])
+);
+export const ownerProcedure = protectedProcedure.use(
+ isMemberWithAccessesForActiveAccountId([ACCOUNT_ACCESS.OWNER])
+);
export const accountHasSpecialFeature = isAccountWithFeature('SPECIAL_FEATURE');
export const router = t.router;
diff --git a/stores/account.store.ts b/stores/account.store.ts
index 6b13372..6ca2319 100644
--- a/stores/account.store.ts
+++ b/stores/account.store.ts
@@ -1,6 +1,6 @@
import { ACCOUNT_ACCESS } from '~~/prisma/account-access-enum';
-import { defineStore } from "pinia"
-import { FullDBUser, MembershipWithUser } from "~~/lib/services/service.types";
+import { defineStore } from 'pinia';
+import { FullDBUser, MembershipWithUser } from '~~/lib/services/service.types';
/*
This store manages User and Account state including the ActiveAccount
@@ -23,9 +23,9 @@ so that other routers can use them to filter results to the active user and acco
account account acccount*
*/
interface State {
- dbUser: FullDBUser | null,
- activeAccountId: number | null,
- activeAccountMembers: MembershipWithUser[]
+ dbUser: FullDBUser | null;
+ activeAccountId: number | null;
+ activeAccountMembers: MembershipWithUser[];
}
export const useAccountStore = defineStore('account', {
@@ -33,117 +33,155 @@ export const useAccountStore = defineStore('account', {
return {
dbUser: null,
activeAccountId: null,
- activeAccountMembers: [],
- }
+ activeAccountMembers: []
+ };
},
getters: {
- activeMembership: (state) => state.dbUser?.memberships.find(m => m.account_id === state.activeAccountId)
+ activeMembership: state =>
+ state.dbUser?.memberships.find(
+ m => m.account_id === state.activeAccountId
+ )
},
actions: {
- async init(){
+ async init() {
const { $client } = useNuxtApp();
- if(!this.dbUser){
+ if (!this.dbUser) {
const { dbUser } = await $client.auth.getDBUser.query();
- if(dbUser){
+ if (dbUser) {
this.dbUser = dbUser;
}
}
- if(!this.activeAccountId){
- const { activeAccountId } = await $client.account.getActiveAccountId.query();
- if(activeAccountId){
+ if (!this.activeAccountId) {
+ const { activeAccountId } =
+ await $client.account.getActiveAccountId.query();
+ if (activeAccountId) {
this.activeAccountId = activeAccountId;
}
}
},
- signout(){
+ signout() {
this.dbUser = null;
this.activeAccountId = null;
this.activeAccountMembers = [];
},
- async getActiveAccountMembers(){
- if(this.activeMembership && (this.activeMembership.access === ACCOUNT_ACCESS.ADMIN || this.activeMembership.access === ACCOUNT_ACCESS.OWNER)){
+ async getActiveAccountMembers() {
+ if (
+ this.activeMembership &&
+ (this.activeMembership.access === ACCOUNT_ACCESS.ADMIN ||
+ this.activeMembership.access === ACCOUNT_ACCESS.OWNER)
+ ) {
const { $client } = useNuxtApp();
- const { data: memberships } = await $client.account.getAccountMembers.useQuery();
- if(memberships.value?.memberships){
+ const { data: memberships } =
+ await $client.account.getAccountMembers.useQuery();
+ if (memberships.value?.memberships) {
this.activeAccountMembers = memberships.value?.memberships;
}
}
},
- async changeActiveAccount(account_id: number){
+ async changeActiveAccount(account_id: number) {
const { $client } = useNuxtApp();
- await $client.account.changeActiveAccount.mutate({account_id}); // sets active account on context for other routers and sets the preference in a cookie
-
- this.activeAccountId = account_id; // because this is used as a trigger to some other components, NEEDS TO BE AFTER THE MUTATE CALL
+ await $client.account.changeActiveAccount.mutate({ account_id }); // sets active account on context for other routers and sets the preference in a cookie
+
+ this.activeAccountId = account_id; // because this is used as a trigger to some other components, NEEDS TO BE AFTER THE MUTATE CALL
await this.getActiveAccountMembers(); // these relate to the active account and need to ber re-fetched
},
- async changeAccountName(new_name: string){
- if(!this.activeMembership){ return; }
+ async changeAccountName(new_name: string) {
+ if (!this.activeMembership) {
+ return;
+ }
const { $client } = useNuxtApp();
- const { account } = await $client.account.changeAccountName.mutate({ new_name });
- if(account){
+ const { account } = await $client.account.changeAccountName.mutate({
+ new_name
+ });
+ if (account) {
this.activeMembership.account.name = account.name;
}
},
- async acceptPendingMembership(membership_id: number){
+ async acceptPendingMembership(membership_id: number) {
const { $client } = useNuxtApp();
- const { data: membership } = await $client.account.acceptPendingMembership.useQuery({ membership_id });
-
- if(membership.value && membership.value.membership?.pending === false){
- for(const m of this.activeAccountMembers){
- if(m.id === membership_id){
+ const { data: membership } =
+ await $client.account.acceptPendingMembership.useQuery({
+ membership_id
+ });
+
+ if (membership.value && membership.value.membership?.pending === false) {
+ for (const m of this.activeAccountMembers) {
+ if (m.id === membership_id) {
m.pending = false;
}
}
}
},
- async rejectPendingMembership(membership_id: number){
+ async rejectPendingMembership(membership_id: number) {
const { $client } = useNuxtApp();
- const { data: membership } = await $client.account.rejectPendingMembership.useQuery({ membership_id });
-
- if(membership.value){
- this.activeAccountMembers = this.activeAccountMembers.filter(m => m.id !== membership_id);
+ const { data: membership } =
+ await $client.account.rejectPendingMembership.useQuery({
+ membership_id
+ });
+
+ if (membership.value) {
+ this.activeAccountMembers = this.activeAccountMembers.filter(
+ m => m.id !== membership_id
+ );
}
},
- async deleteMembership(membership_id: number){
+ async deleteMembership(membership_id: number) {
const { $client } = useNuxtApp();
- const { data: membership } = await $client.account.deleteMembership.useQuery({ membership_id });
-
- if(membership.value){
- this.activeAccountMembers = this.activeAccountMembers.filter(m => m.id !== membership_id);
+ const { data: membership } =
+ await $client.account.deleteMembership.useQuery({ membership_id });
+
+ if (membership.value) {
+ this.activeAccountMembers = this.activeAccountMembers.filter(
+ m => m.id !== membership_id
+ );
}
},
- async rotateJoinPassword(){
+ async rotateJoinPassword() {
const { $client } = useNuxtApp();
const { account } = await $client.account.rotateJoinPassword.mutate();
- if(account && this.activeMembership){
+ if (account && this.activeMembership) {
this.activeMembership.account = account;
}
},
- async joinUserToAccountPending(account_id: number){
- if(!this.dbUser) { return; }
+ async joinUserToAccountPending(account_id: number) {
+ if (!this.dbUser) {
+ return;
+ }
const { $client } = useNuxtApp();
- const { membership } = await $client.account.joinUserToAccountPending.mutate({account_id, user_id: this.dbUser.id});
- if(membership && this.activeMembership){
+ const { membership } =
+ await $client.account.joinUserToAccountPending.mutate({
+ account_id,
+ user_id: this.dbUser.id
+ });
+ if (membership && this.activeMembership) {
this.dbUser?.memberships.push(membership);
}
},
- async changeUserAccessWithinAccount(user_id: number, access: ACCOUNT_ACCESS){
+ async changeUserAccessWithinAccount(
+ user_id: number,
+ access: ACCOUNT_ACCESS
+ ) {
const { $client } = useNuxtApp();
- const { membership } = await $client.account.changeUserAccessWithinAccount.mutate({ user_id, access });
- if(membership){
- for(const m of this.activeAccountMembers){
- if(m.id === membership.id){
+ const { membership } =
+ await $client.account.changeUserAccessWithinAccount.mutate({
+ user_id,
+ access
+ });
+ if (membership) {
+ for (const m of this.activeAccountMembers) {
+ if (m.id === membership.id) {
m.access = membership.access;
}
}
}
},
- async claimOwnershipOfAccount(){
+ async claimOwnershipOfAccount() {
const { $client } = useNuxtApp();
- const { memberships } = await $client.account.claimOwnershipOfAccount.mutate();
- if(memberships){
+ const { memberships } =
+ await $client.account.claimOwnershipOfAccount.mutate();
+ if (memberships) {
this.activeAccountMembers = memberships;
- this.activeMembership!.access = ACCOUNT_ACCESS.OWNER
+ this.activeMembership!.access = ACCOUNT_ACCESS.OWNER;
}
}
}
diff --git a/stores/notes.store.ts b/stores/notes.store.ts
index e317910..1bb5aab 100644
--- a/stores/notes.store.ts
+++ b/stores/notes.store.ts
@@ -1,6 +1,6 @@
-import { Note } from ".prisma/client"
-import { defineStore, storeToRefs } from "pinia"
-import { Ref } from "vue";
+import { Note } from '.prisma/client';
+import { defineStore, storeToRefs } from 'pinia';
+import { Ref } from 'vue';
/*
Note) the Notes Store needs to be a 'Setup Store' (https://pinia.vuejs.org/core-concepts/#setup-stores)
@@ -9,7 +9,7 @@ If the UI does not need to dynamically respond to a change in the active Account
then an Options store can be used.
*/
export const useNotesStore = defineStore('notes', () => {
- const accountStore = useAccountStore()
+ const accountStore = useAccountStore();
const { activeAccountId } = storeToRefs(accountStore);
let _notes: Ref = ref([]);
@@ -17,37 +17,45 @@ export const useNotesStore = defineStore('notes', () => {
async function fetchNotesForCurrentUser() {
const { $client } = useNuxtApp();
const { notes } = await $client.notes.getForActiveAccount.query();
- if(notes){
+ if (notes) {
_notes.value = notes;
- }
+ }
}
async function createNote(note_text: string) {
const { $client } = useNuxtApp();
- const { note } = await $client.notes.createNote.mutate({note_text});
- if(note){
+ const { note } = await $client.notes.createNote.mutate({ note_text });
+ if (note) {
_notes.value.push(note);
}
}
async function deleteNote(note_id: number) {
const { $client } = useNuxtApp();
- const { note } = await $client.notes.deleteNote.mutate({note_id});
- if(note){
+ const { note } = await $client.notes.deleteNote.mutate({ note_id });
+ if (note) {
_notes.value = _notes.value.filter(n => n.id !== note.id);
}
}
async function generateAINoteFromPrompt(user_prompt: string) {
const { $client } = useNuxtApp();
- const { noteText } = await $client.notes.generateAINoteFromPrompt.query({user_prompt});
- return noteText?noteText:'';
+ const { noteText } = await $client.notes.generateAINoteFromPrompt.query({
+ user_prompt
+ });
+ return noteText ? noteText : '';
}
// if the active account changes, fetch notes again (i.e dynamic.. probabl overkill)
- watch(activeAccountId, async (val, oldVal)=> {
- await fetchNotesForCurrentUser()
+ watch(activeAccountId, async (val, oldVal) => {
+ await fetchNotesForCurrentUser();
});
- return { notes: _notes, fetchNotesForCurrentUser, createNote, deleteNote, generateAINoteFromPrompt}
+ return {
+ notes: _notes,
+ fetchNotesForCurrentUser,
+ createNote,
+ deleteNote,
+ generateAINoteFromPrompt
+ };
});
diff --git a/stores/notify.store.ts b/stores/notify.store.ts
index 3df39c6..0c15fca 100644
--- a/stores/notify.store.ts
+++ b/stores/notify.store.ts
@@ -1,45 +1,51 @@
-import { defineStore } from "pinia"
+import { defineStore } from 'pinia';
/*
This store manages User and Account state including the ActiveAccount
It is used in the Account administration page and the header due to it's account switching features.
*/
-export interface Notification{
+export interface Notification {
message: string;
type: NotificationType;
notifyTime: number;
}
-export enum NotificationType{
+export enum NotificationType {
Info,
Success,
Warning,
- Error,
+ Error
}
interface State {
- notifications: Notification[],
- notificationsArchive: Notification[],
+ notifications: Notification[];
+ notificationsArchive: Notification[];
}
export const useNotifyStore = defineStore('notify', {
state: (): State => {
return {
notifications: [],
- notificationsArchive: [],
- }
+ notificationsArchive: []
+ };
},
actions: {
- notify(messageOrError: unknown, type:NotificationType){
- let message: string = "";
+ notify(messageOrError: unknown, type: NotificationType) {
+ let message: string = '';
if (messageOrError instanceof Error) message = messageOrError.message;
- if (typeof messageOrError === "string") message = messageOrError;
- const notification: Notification = {message, type, notifyTime: Date.now()};
+ if (typeof messageOrError === 'string') message = messageOrError;
+ const notification: Notification = {
+ message,
+ type,
+ notifyTime: Date.now()
+ };
this.notifications.push(notification);
setTimeout(this.removeNotification.bind(this), 5000, notification);
},
- removeNotification(notification: Notification){
- this.notifications = this.notifications.filter(n => n.notifyTime != notification.notifyTime);
- },
+ removeNotification(notification: Notification) {
+ this.notifications = this.notifications.filter(
+ n => n.notifyTime != notification.notifyTime
+ );
+ }
}
});
diff --git a/tailwind.config.js b/tailwind.config.js
index c11e569..21f9bb8 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -1,5 +1,5 @@
export default {
- plugins:[require("@tailwindcss/typography"), require("daisyui")],
+ plugins: [require("@tailwindcss/typography"), require("daisyui")],
daisyui: {
styled: true,
themes: ["acid", "night"],
@@ -9,5 +9,5 @@ export default {
rtl: false,
prefix: "",
darkTheme: "night",
- },
+ },
}