Compare commits

..

10 Commits

Author SHA1 Message Date
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 912 additions and 864 deletions

1
.gitignore vendored
View File

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

View File

@@ -1,5 +1,18 @@
# 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
- 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

View File

@@ -4,6 +4,8 @@
<span class="px-2">|</span>
<NuxtLink to="/privacy">Privacy</NuxtLink>
<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>
</template>

View File

@@ -101,13 +101,13 @@ export namespace AccountService {
account_id: number,
membership_id: number
): Promise<MembershipWithAccount> {
const membership = prisma_client.membership.findFirstOrThrow({
const membership = await prisma_client.membership.findFirstOrThrow({
where: {
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`);
}
@@ -126,13 +126,13 @@ export namespace AccountService {
account_id: number,
membership_id: number
): Promise<MembershipWithAccount> {
const membership = prisma_client.membership.findFirstOrThrow({
const membership = await prisma_client.membership.findFirstOrThrow({
where: {
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`);
}
@@ -214,7 +214,7 @@ export namespace AccountService {
length: 10,
numbers: true
});
return prisma_client.account.update({
return await prisma_client.account.update({
where: { id: account_id },
data: { join_password }
});

View File

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

View File

@@ -19,7 +19,7 @@ export default defineNuxtConfig({
app: {
head: {
htmlAttrs: {
lang: 'en'
lang: 'fr'
},
title: 'SupaNuxt SaaS',
link: [
@@ -52,11 +52,18 @@ export default defineNuxtConfig({
initialPlanName: 'Free Trial',
initialPlanActiveMonths: 1,
openAIKey: process.env.OPENAI_API_KEY,
/*app: {
baseURL: 'https://vsc.arrondeau.fr/'
},*/
public: {
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: {
redirect: false,
redirectOptions: {

1506
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "supanuxt-saas",
"version": "1.4.2",
"version": "1.4.3",
"author": {
"name": "Michael Dausmann",
"email": "mdausmann@gmail.com",
@@ -23,28 +23,28 @@
"@nuxt/test-utils": "^3.11.0",
"@nuxtjs/supabase": "^1.1.6",
"@nuxtjs/tailwindcss": "^6.11.4",
"@prisma/client": "^5.9.1",
"@prisma/client": "^5.15.0",
"@tailwindcss/typography": "^0.5.10",
"@types/node": "^18.19.17",
"@types/node": "^20.11.19",
"nuxt": "^3.10.2",
"nuxt-icon": "^0.6.8",
"prisma": "^5.9.1",
"ts-node": "^10.9.2",
"typescript": "^5.3.3",
"vitest": "^0.34.6"
"vitest": "^1.3.0"
},
"dependencies": {
"@pinia/nuxt": "^0.5.1",
"@trpc/client": "^10.45.1",
"@trpc/server": "^10.45.1",
"daisyui": "^2.52.0",
"daisyui": "^4.7.2",
"generate-password-ts": "^1.6.5",
"openai": "^4.28.0",
"pinia": "^2.1.7",
"stripe": "^11.18.0",
"superjson": "^1.13.3",
"stripe": "^14.17.0",
"superjson": "^2.2.1",
"trpc-nuxt": "^0.10.19",
"vanilla-cookieconsent": "^2.9.2",
"vanilla-cookieconsent": "^3.0.0",
"zod": "^3.22.4"
},
"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">
import { AccountWithMembers } from '~~/lib/services/service.types';
import { type AccountWithMembers } from '~~/lib/services/service.types';
const route = useRoute();
const { join_password }: { join_password?: string } = route.params;

View File

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

View File

@@ -1,129 +1,63 @@
import 'vanilla-cookieconsent/dist/cookieconsent.css';
import 'vanilla-cookieconsent/src/cookieconsent.js';
import * as CookieConsent from 'vanilla-cookieconsent';
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({
current_lang: 'en',
autoclear_cookies: true, // default: false
page_scripts: true, // default: false
// mode: 'opt-in' // default: 'opt-in'; value: 'opt-in' or 'opt-out'
// delay: 0, // default: 0
// auto_language: '', // default: null; could also be 'browser' or 'document'
// autorun: true, // default: true
// force_consent: false, // default: false
// 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'
language: {
default: 'en',
translations: {
en: {
consentModal: {
title: 'We use cookies',
description: 'Cookie modal description',
acceptAllBtn: 'Accept all',
acceptNecessaryBtn: 'Reject all',
showPreferencesBtn: 'Manage Individual preferences'
},
secondary_btn: {
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',
// 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
preferencesModal: {
title: 'Manage cookie preferences',
acceptAllBtn: 'Accept all',
acceptNecessaryBtn: 'Reject all',
savePreferencesBtn: 'Accept current selection',
closeIconLabel: 'Close modal',
sections: [
{
title: 'Somebody said ... cookies?',
description: 'I want one!'
},
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: '_gid',
col2: 'google.com',
col3: '1 day',
col4: 'description ...'
}
]
},
{
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: 'Strictly Necessary cookies',
description:
'These cookies are essential for the proper functioning of the website and cannot be disabled.',
//this field will generate a toggle linked to the 'necessary' category
linkedCategory: 'necessary'
},
{
title: 'Performance and Analytics',
description:
'These cookies collect information about how you use our website. All of the data is anonymized and cannot be used to identify you.',
linkedCategory: 'analytics'
},
{
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: '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>(
handler: EventHandler<T>

View File

@@ -4,7 +4,7 @@ import { AccountService } from '~~/lib/services/account.service';
import type { AccountWithMembers } from '~~/lib/services/service.types';
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 => {
const body = await readBody(event);

View File

@@ -2,7 +2,7 @@ import Stripe from 'stripe';
import { AccountService } from '~~/lib/services/account.service';
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 => {
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';
export async function createContext(event: H3Event) {

View File

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

View File

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

View File

@@ -1,7 +1,10 @@
import { ACCOUNT_ACCESS } from '~~/prisma/account-access-enum';
import { defineStore } from 'pinia';
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

View File

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