Compare commits

...

11 Commits

Author SHA1 Message Date
7836bf2bb1 add concurrently and loca.lt configuration 2024-06-09 11:48:56 +01:00
37d32d7d14 first commit 2024-06-09 08:31:08 +01:00
Michael Dausmann
460c859ab3 remove console.logs 2024-02-25 12:13:19 +11:00
Michael Dausmann
0abc4ec624 refactors 2024-02-25 00:31:55 +11:00
Michael Dausmann
786665e84e version 1.4.3 changelog 2024-02-19 02:57:35 +11:00
Michael Dausmann
d3391c21a6 update cookie consent and add contact page 2024-02-19 02:43:30 +11:00
Michael Dausmann
0219723a07 update daisyui 2024-02-19 01:54:49 +11:00
Michael Dausmann
e7d1f35777 update stripe and stripe api 2024-02-19 01:48:51 +11:00
Michael Dausmann
463cf7f194 update superjson and node types 2024-02-19 00:20:07 +11:00
Michael Dausmann
9daea204e5 more type cleanup 2024-02-17 14:54:45 +11:00
Michael Dausmann
fcb8071cec update vitest 2024-02-17 14:38:22 +11:00
20 changed files with 1155 additions and 903 deletions

1
.gitignore vendored
View File

@@ -7,3 +7,4 @@ node_modules
.env .env
dist dist
junk junk
.DS_Store

View File

@@ -1,5 +1,18 @@
# Changelog # Changelog
## Version 1.4.3
### Update All Dependencies to latest
- openai (3.3.0 -> 4.28.0)
- superjson (1.12.2 -> 2.2.1)
- node types (18.15.11 -> 20.11.19)
- stripe lib (11.12.0 -> 14.17.0)
- stripe api version (2022-11-15 -> 2023-10-16)
- cookie consent (2.9.2 -> 3.0.0)
- daisyui (2.51.5 -> 4.7.2)
- vitest (0.33.0 -> 1.3.0)
- other minor and patch versions
## Version 1.4.2 ## Version 1.4.2
- Added Favicons and web manifest and referenced in nuxt.config (I used https://favicon.io/favicon-converter/ to generate the icon assets, seems to work well) - Added Favicons and web manifest and referenced in nuxt.config (I used https://favicon.io/favicon-converter/ to generate the icon assets, seems to work well)
- Added patch folder to hold patch files, should make it easier to update repos based on earlier versions - Added patch folder to hold patch files, should make it easier to update repos based on earlier versions

View File

@@ -4,6 +4,8 @@
<span class="px-2">|</span> <span class="px-2">|</span>
<NuxtLink to="/privacy">Privacy</NuxtLink> <NuxtLink to="/privacy">Privacy</NuxtLink>
<span class="px-2">|</span> <span class="px-2">|</span>
<button type="button" data-cc="c-settings">Cookie settings</button> <NuxtLink to="/contact">Contact Us</NuxtLink>
<span class="px-2">|</span>
<button type="button" data-cc="show-preferencesModal">Cookie settings</button>
</div> </div>
</template> </template>

View File

@@ -101,13 +101,13 @@ export namespace AccountService {
account_id: number, account_id: number,
membership_id: number membership_id: number
): Promise<MembershipWithAccount> { ): Promise<MembershipWithAccount> {
const membership = prisma_client.membership.findFirstOrThrow({ const membership = await prisma_client.membership.findFirstOrThrow({
where: { where: {
id: membership_id id: membership_id
} }
}); });
if ((await membership).account_id != account_id) { if (membership.account_id != account_id) {
throw new Error(`Membership does not belong to current account`); throw new Error(`Membership does not belong to current account`);
} }
@@ -126,13 +126,13 @@ export namespace AccountService {
account_id: number, account_id: number,
membership_id: number membership_id: number
): Promise<MembershipWithAccount> { ): Promise<MembershipWithAccount> {
const membership = prisma_client.membership.findFirstOrThrow({ const membership = await prisma_client.membership.findFirstOrThrow({
where: { where: {
id: membership_id id: membership_id
} }
}); });
if ((await membership).account_id != account_id) { if (membership.account_id != account_id) {
throw new Error(`Membership does not belong to current account`); throw new Error(`Membership does not belong to current account`);
} }
@@ -214,7 +214,7 @@ export namespace AccountService {
length: 10, length: 10,
numbers: true numbers: true
}); });
return prisma_client.account.update({ return await prisma_client.account.update({
where: { id: account_id }, where: { id: account_id },
data: { join_password } data: { join_password }
}); });

View File

@@ -4,10 +4,6 @@ import { AccountLimitError } from './errors';
import { AccountService } from './account.service'; import { AccountService } from './account.service';
export namespace NotesService { export namespace NotesService {
export async function getAllNotes() {
return prisma_client.note.findMany();
}
export async function getNoteById(id: number) { export async function getNoteById(id: number) {
return prisma_client.note.findUniqueOrThrow({ where: { id } }); return prisma_client.note.findUniqueOrThrow({ where: { id } });
} }

View File

@@ -19,7 +19,7 @@ export default defineNuxtConfig({
app: { app: {
head: { head: {
htmlAttrs: { htmlAttrs: {
lang: 'en' lang: 'fr'
}, },
title: 'SupaNuxt SaaS', title: 'SupaNuxt SaaS',
link: [ link: [
@@ -52,11 +52,18 @@ export default defineNuxtConfig({
initialPlanName: 'Free Trial', initialPlanName: 'Free Trial',
initialPlanActiveMonths: 1, initialPlanActiveMonths: 1,
openAIKey: process.env.OPENAI_API_KEY, openAIKey: process.env.OPENAI_API_KEY,
/*app: {
baseURL: 'https://vsc.arrondeau.fr/'
},*/
public: { public: {
debugMode: true, debugMode: true,
siteRootUrl: process.env.URL || 'http://localhost:3000' // URL env variable is provided by netlify by default //siteRootUrl: process.env.URL || 'http://localhost:5500' // URL env variable is provided by netlify by default
siteRootUrl: 'https://vsc.arrondeau.fr/'
} }
}, },
devServer: {
port: 3000
},
supabase: { supabase: {
redirect: false, redirect: false,
redirectOptions: { redirectOptions: {

1785
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "supanuxt-saas", "name": "supanuxt-saas",
"version": "1.4.2", "version": "1.4.3",
"author": { "author": {
"name": "Michael Dausmann", "name": "Michael Dausmann",
"email": "mdausmann@gmail.com", "email": "mdausmann@gmail.com",
@@ -13,7 +13,7 @@
}, },
"scripts": { "scripts": {
"build": "nuxt build", "build": "nuxt build",
"dev": "nuxt dev", "dev": "concurrently 'nuxt dev' 'sleep 12 && lt --port 3000'",
"generate": "nuxt generate", "generate": "nuxt generate",
"preview": "nuxt preview", "preview": "nuxt preview",
"postinstall": "prisma generate && nuxt prepare", "postinstall": "prisma generate && nuxt prepare",
@@ -23,28 +23,29 @@
"@nuxt/test-utils": "^3.11.0", "@nuxt/test-utils": "^3.11.0",
"@nuxtjs/supabase": "^1.1.6", "@nuxtjs/supabase": "^1.1.6",
"@nuxtjs/tailwindcss": "^6.11.4", "@nuxtjs/tailwindcss": "^6.11.4",
"@prisma/client": "^5.9.1", "@prisma/client": "^5.15.0",
"@tailwindcss/typography": "^0.5.10", "@tailwindcss/typography": "^0.5.10",
"@types/node": "^18.19.17", "@types/node": "^20.11.19",
"nuxt": "^3.10.2", "nuxt": "^3.10.2",
"nuxt-icon": "^0.6.8", "nuxt-icon": "^0.6.8",
"prisma": "^5.9.1", "prisma": "^5.9.1",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.3.3", "typescript": "^5.3.3",
"vitest": "^0.34.6" "vitest": "^1.3.0"
}, },
"dependencies": { "dependencies": {
"@pinia/nuxt": "^0.5.1", "@pinia/nuxt": "^0.5.1",
"@trpc/client": "^10.45.1", "@trpc/client": "^10.45.1",
"@trpc/server": "^10.45.1", "@trpc/server": "^10.45.1",
"daisyui": "^2.52.0", "concurrently": "^8.2.2",
"daisyui": "^4.7.2",
"generate-password-ts": "^1.6.5", "generate-password-ts": "^1.6.5",
"openai": "^4.28.0", "openai": "^4.28.0",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"stripe": "^11.18.0", "stripe": "^14.17.0",
"superjson": "^1.13.3", "superjson": "^2.2.1",
"trpc-nuxt": "^0.10.19", "trpc-nuxt": "^0.10.19",
"vanilla-cookieconsent": "^2.9.2", "vanilla-cookieconsent": "^3.0.0",
"zod": "^3.22.4" "zod": "^3.22.4"
}, },
"overrides": { "overrides": {

10
pages/contact.vue Normal file
View File

@@ -0,0 +1,10 @@
<template>
<div class="prose lg:prose-xl m-5">
<h1>Contact Us</h1>
<p>
Contact SupaNuxt SaaS on <a href="https://github.com/JavascriptMick/supanuxt-saas">github</a>
</p>
</div>
</template>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { AccountWithMembers } from '~~/lib/services/service.types'; import { type AccountWithMembers } from '~~/lib/services/service.types';
const route = useRoute(); const route = useRoute();
const { join_password }: { join_password?: string } = route.params; const { join_password }: { join_password?: string } = route.params;

View File

@@ -2,7 +2,7 @@
import Stripe from 'stripe'; import Stripe from 'stripe';
const config = useRuntimeConfig(); const config = useRuntimeConfig();
const stripe = new Stripe(config.stripeSecretKey, { const stripe = new Stripe(config.stripeSecretKey, {
apiVersion: '2022-11-15' apiVersion: '2023-10-16'
}); });
const route = useRoute(); const route = useRoute();
let customer: Stripe.Response<Stripe.Customer | Stripe.DeletedCustomer>; let customer: Stripe.Response<Stripe.Customer | Stripe.DeletedCustomer>;

View File

@@ -1,129 +1,63 @@
import 'vanilla-cookieconsent/dist/cookieconsent.css'; import 'vanilla-cookieconsent/dist/cookieconsent.css';
import 'vanilla-cookieconsent/src/cookieconsent.js'; import * as CookieConsent from 'vanilla-cookieconsent';
export default defineNuxtPlugin(nuxtApp => { export default defineNuxtPlugin(nuxtApp => {
// @ts-ignore /**
const cookieConsent = window.initCookieConsent(); * All config. options available here:
* https://cookieconsent.orestbida.com/reference/configuration-reference.html
*/
CookieConsent.run({
categories: {
necessary: {
enabled: true, // this category is enabled by default
readOnly: true // this category cannot be disabled
},
analytics: {}
},
cookieConsent.run({ language: {
current_lang: 'en', default: 'en',
autoclear_cookies: true, // default: false translations: {
page_scripts: true, // default: false en: {
consentModal: {
// mode: 'opt-in' // default: 'opt-in'; value: 'opt-in' or 'opt-out' title: 'We use cookies',
// delay: 0, // default: 0 description: 'Cookie modal description',
// auto_language: '', // default: null; could also be 'browser' or 'document' acceptAllBtn: 'Accept all',
// autorun: true, // default: true acceptNecessaryBtn: 'Reject all',
// force_consent: false, // default: false showPreferencesBtn: 'Manage Individual preferences'
// hide_from_bots: true, // default: true
// remove_cookie_tables: false // default: false
// cookie_name: 'cc_cookie', // default: 'cc_cookie'
// cookie_expiration: 182, // default: 182 (days)
// cookie_necessary_only_expiration: 182 // default: disabled
// cookie_domain: location.hostname, // default: current domain
// cookie_path: '/', // default: root
// cookie_same_site: 'Lax', // default: 'Lax'
// use_rfc_cookie: false, // default: false
// revision: 0, // default: 0
// onFirstAction: function(user_preferences, cookie){
// // callback triggered only once on the first accept/reject action
// },
// onAccept: function (cookie) {
// // callback triggered on the first accept/reject action, and after each page load
// },
// onChange: function (cookie, changed_categories) {
// // callback triggered when user changes preferences after consent has already been given
// },
languages: {
en: {
consent_modal: {
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. <button type="button" data-cc="c-settings" class="cc-link">Let me choose</button>',
primary_btn: {
text: 'Accept all',
role: 'accept_all' // 'accept_selected' or 'accept_all'
}, },
secondary_btn: { preferencesModal: {
text: 'Reject all', title: 'Manage cookie preferences',
role: 'accept_necessary' // 'settings' or 'accept_necessary' acceptAllBtn: 'Accept all',
} acceptNecessaryBtn: 'Reject all',
}, savePreferencesBtn: 'Accept current selection',
settings_modal: { closeIconLabel: 'Close modal',
title: 'Cookie preferences', sections: [
save_settings_btn: 'Save settings', {
accept_all_btn: 'Accept all', title: 'Somebody said ... cookies?',
reject_all_btn: 'Reject all', description: 'I want one!'
close_btn_label: 'Close',
// cookie_table_caption: 'Cookie list',
cookie_table_headers: [
{ col1: 'Name' },
{ col2: 'Domain' },
{ col3: 'Expiration' },
{ col4: 'Description' }
],
blocks: [
{
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 <a href="#" class="cc-link">privacy policy</a>.'
},
{
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',
toggle: {
value: 'necessary',
enabled: true,
readonly: true // cookie categories with readonly=true are all treated as "necessary cookies"
}
},
{
title: 'Performance and Analytics cookies',
description:
'These cookies allow the website to remember the choices you have made in the past',
toggle: {
value: 'analytics', // your cookie category
enabled: false,
readonly: false
}, },
cookie_table: [ {
// list of all expected cookies title: 'Strictly Necessary cookies',
{ description:
col1: '^_ga', // match all cookies starting with "_ga" 'These cookies are essential for the proper functioning of the website and cannot be disabled.',
col2: 'google.com',
col3: '2 years', //this field will generate a toggle linked to the 'necessary' category
col4: 'description ...', linkedCategory: 'necessary'
is_regex: true },
}, {
{ title: 'Performance and Analytics',
col1: '_gid', description:
col2: 'google.com', 'These cookies collect information about how you use our website. All of the data is anonymized and cannot be used to identify you.',
col3: '1 day', linkedCategory: 'analytics'
col4: 'description ...' },
} {
] title: 'More information',
}, description:
{ 'For any queries in relation to my policy on cookies and your choices, please <a href="/contact">contact us</a>'
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',
toggle: {
value: 'targeting',
enabled: false,
readonly: false
} }
}, ]
{ }
title: 'More information',
description:
'For any queries in relation to our policy on cookies and your choices, please <a class="cc-link" href="#yourcontactpage">contact us</a>.'
}
]
} }
} }
} }

View File

@@ -1,4 +1,9 @@
import { EventHandler, EventHandlerRequest, H3Event, eventHandler } from 'h3'; import {
type EventHandler,
type EventHandlerRequest,
H3Event,
eventHandler
} from 'h3';
export const defineProtectedEventHandler = <T extends EventHandlerRequest>( export const defineProtectedEventHandler = <T extends EventHandlerRequest>(
handler: EventHandler<T> handler: EventHandler<T>

View File

@@ -4,7 +4,7 @@ import { AccountService } from '~~/lib/services/account.service';
import type { AccountWithMembers } from '~~/lib/services/service.types'; import type { AccountWithMembers } from '~~/lib/services/service.types';
const config = useRuntimeConfig(); const config = useRuntimeConfig();
const stripe = new Stripe(config.stripeSecretKey, { apiVersion: '2022-11-15' }); const stripe = new Stripe(config.stripeSecretKey, { apiVersion: '2023-10-16' });
export default defineEventHandler(async event => { export default defineEventHandler(async event => {
const body = await readBody(event); const body = await readBody(event);

View File

@@ -2,7 +2,7 @@ import Stripe from 'stripe';
import { AccountService } from '~~/lib/services/account.service'; import { AccountService } from '~~/lib/services/account.service';
const config = useRuntimeConfig(); const config = useRuntimeConfig();
const stripe = new Stripe(config.stripeSecretKey, { apiVersion: '2022-11-15' }); const stripe = new Stripe(config.stripeSecretKey, { apiVersion: '2023-10-16' });
export default defineEventHandler(async event => { export default defineEventHandler(async event => {
const stripeSignature = getRequestHeader(event, 'stripe-signature'); const stripeSignature = getRequestHeader(event, 'stripe-signature');

View File

@@ -1,4 +1,4 @@
import { inferAsyncReturnType } from '@trpc/server'; import type { inferAsyncReturnType } from '@trpc/server';
import { H3Event } from 'h3'; import { H3Event } from 'h3';
export async function createContext(event: H3Event) { export async function createContext(event: H3Event) {

View File

@@ -1,4 +1,5 @@
import { TRPCError } from '@trpc/server'; import { TRPCError } from '@trpc/server';
import { setCookie } from 'h3';
import { import {
router, router,
adminProcedure, adminProcedure,

View File

@@ -8,7 +8,7 @@
* @see https://trpc.io/docs/v10/procedures * @see https://trpc.io/docs/v10/procedures
*/ */
import { initTRPC, TRPCError } from '@trpc/server'; import { initTRPC, TRPCError } from '@trpc/server';
import { Context } from './context'; import type { Context } from './context';
import { ACCOUNT_ACCESS } from '~~/prisma/account-access-enum'; import { ACCOUNT_ACCESS } from '~~/prisma/account-access-enum';
import superjson from 'superjson'; import superjson from 'superjson';
import { AccountLimitError } from '~~/lib/services/errors'; import { AccountLimitError } from '~~/lib/services/errors';

View File

@@ -1,7 +1,10 @@
import { ACCOUNT_ACCESS } from '~~/prisma/account-access-enum'; import { ACCOUNT_ACCESS } from '~~/prisma/account-access-enum';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { ref, computed } from 'vue'; import { ref, computed } from 'vue';
import { FullDBUser, MembershipWithUser } from '~~/lib/services/service.types'; import type {
FullDBUser,
MembershipWithUser
} from '~~/lib/services/service.types';
/* /*
This store manages User and Account state including the ActiveAccount This store manages User and Account state including the ActiveAccount

View File

@@ -1,6 +1,6 @@
import { Note } from '.prisma/client'; import type { Note } from '.prisma/client';
import { defineStore, storeToRefs } from 'pinia'; import { defineStore, storeToRefs } from 'pinia';
import { Ref } from 'vue'; import type { Ref } from 'vue';
export const useNotesStore = defineStore('notes', () => { export const useNotesStore = defineStore('notes', () => {
const accountStore = useAccountStore(); const accountStore = useAccountStore();