service worked updates. Needs testing
This commit is contained in:
1 parent
5bebe072b2
commit
2d5d23b104
10 files changed
+3196
-81
No files matched your search
@@ -2,7 +2,26 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||||
|
<meta name="description" content="Encrypted notes and scratch pad" />
|
||||||
|
|
||||||
|
<!-- PWA. The manifest is a real static file: it used to be injected at
|
||||||
|
runtime as a base64 data: URI from App.vue, with the production origin
|
||||||
|
hardcoded, which broke icons and scope outside production. -->
|
||||||
|
<link rel="manifest" href="/manifest.json" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
|
||||||
|
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/img/icons/logo.svg" />
|
||||||
|
|
||||||
|
<!-- iOS has no share-target support, but still installs to the home screen.
|
||||||
|
The apple-touch icon must be the opaque variant -- iOS composites
|
||||||
|
transparent PNGs onto black and the dark half of the mark vanishes. -->
|
||||||
|
<link rel="apple-touch-icon" href="/img/icons/apple-touch-icon-180.png" />
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-title" content="Scribe" />
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
|
|
||||||
<title>Solid Scribe</title>
|
<title>Solid Scribe</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -25,6 +25,8 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^6.0.1",
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
"vite": "^7.1.2"
|
"vite": "^7.1.2",
|
||||||
|
"vite-plugin-pwa": "^2.0.0",
|
||||||
|
"workbox-window": "^7.4.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Generated
+3035
-8
File diff suppressed because it is too large.
Load diff
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -51,6 +51,8 @@
|
|||||||
|
|
||||||
<global-notification />
|
<global-notification />
|
||||||
|
|
||||||
|
<update-prompt />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -59,12 +61,14 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import GlobalSiteMenu from '@/components/GlobalSiteMenu.vue'
|
import GlobalSiteMenu from '@/components/GlobalSiteMenu.vue'
|
||||||
import GlobalNotification from '@/components/GlobalNotificationComponent.vue'
|
import GlobalNotification from '@/components/GlobalNotificationComponent.vue'
|
||||||
|
import UpdatePrompt from '@/components/UpdatePrompt.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'App',
|
name: 'App',
|
||||||
components: {
|
components: {
|
||||||
'global-site-menu': GlobalSiteMenu,
|
'global-site-menu': GlobalSiteMenu,
|
||||||
'global-notification': GlobalNotification,
|
'global-notification': GlobalNotification,
|
||||||
|
'update-prompt': UpdatePrompt,
|
||||||
},
|
},
|
||||||
data: function(){
|
data: function(){
|
||||||
return {
|
return {
|
||||||
@@ -83,74 +87,6 @@ export default {
|
|||||||
// - Gets new session tokens from server and uses them in app
|
// - Gets new session tokens from server and uses them in app
|
||||||
beforeCreate: function(){
|
beforeCreate: function(){
|
||||||
|
|
||||||
const root = 'https://www.solidscribe.com'
|
|
||||||
// const root = 'https://192.168.1.164'
|
|
||||||
const manifest = {
|
|
||||||
"theme_color":"#000",
|
|
||||||
"background_color": "#000",
|
|
||||||
"description": "Take Notes",
|
|
||||||
"display_override": ["fullscreen", "minimal-ui"],
|
|
||||||
"display": "standalone",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": root+"/api/static/assets/logo.png",
|
|
||||||
"sizes": "495x495",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "any"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": root+"/api/static/assets/maskable_icon.png",
|
|
||||||
"sizes": "826x826",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "maskable"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"screenshots" : [
|
|
||||||
{
|
|
||||||
"src": root+"/api/static/assets/lightmode.png",
|
|
||||||
"sizes": "1000x1080",
|
|
||||||
"type": "image/png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": root+"/api/static/assets/darkmode.png",
|
|
||||||
"sizes": "1000x1081",
|
|
||||||
"type": "image/png"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"shortcuts" : [
|
|
||||||
{
|
|
||||||
"name": "Files & Links",
|
|
||||||
"url": " /attachments",
|
|
||||||
"description": "All Note files and links",
|
|
||||||
"icons": [{
|
|
||||||
"src": root+"/api/static/assets/icon1.png",
|
|
||||||
"sizes": "96x96",
|
|
||||||
"type": "image/png"
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Metric Track",
|
|
||||||
"url": "/metrictrack",
|
|
||||||
"description": "Metric Tracking",
|
|
||||||
"icons": [{
|
|
||||||
"src": root+"/api/static/assets/icon2.png",
|
|
||||||
"sizes": "96x96",
|
|
||||||
"type": "image/png"
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"name": "Solid Scribe",
|
|
||||||
"short_name": "Solid Scribe",
|
|
||||||
"author": "Max",
|
|
||||||
"scope": root,
|
|
||||||
"start_url": window.location.href
|
|
||||||
}
|
|
||||||
|
|
||||||
const link = document.createElement('link')
|
|
||||||
link.rel = 'manifest'
|
|
||||||
link.href = `data:application/json;base64,${btoa(JSON.stringify(manifest))}`
|
|
||||||
document.head.appendChild(link)
|
|
||||||
|
|
||||||
//Before all requests going out
|
//Before all requests going out
|
||||||
axios.interceptors.request.use(
|
axios.interceptors.request.use(
|
||||||
(config) => {
|
(config) => {
|
||||||
|
|||||||
@@ -153,8 +153,10 @@
|
|||||||
this.$io.emit('user_connect', token)
|
this.$io.emit('user_connect', token)
|
||||||
localStorage.setItem('loginToken', token)
|
localStorage.setItem('loginToken', token)
|
||||||
|
|
||||||
//Redirect user to notes section after login
|
//Redirect user to notes section after login, or back to
|
||||||
this.$router.push('/notes')
|
//wherever they were headed (e.g. an Android share waiting to
|
||||||
|
//be saved -- see ShareTargetPage.vue)
|
||||||
|
this.$router.push(this.$route.query.redirect || '/notes')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
register(){
|
register(){
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import './tailwind.css'
|
|||||||
import io from 'socket.io-client'
|
import io from 'socket.io-client'
|
||||||
import EventBus from './EventBus'
|
import EventBus from './EventBus'
|
||||||
import Helpers from './Helpers'
|
import Helpers from './Helpers'
|
||||||
|
import { installLaunchRedirect } from './LaunchPage'
|
||||||
|
|
||||||
const socket = io({ path: '/socket' })
|
const socket = io({ path: '/socket' })
|
||||||
|
|
||||||
@@ -31,6 +32,10 @@ router.beforeEach((to, from, next) => {
|
|||||||
next()
|
next()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Installed launches land on "/" and are redirected to the user's chosen
|
||||||
|
// launch page -- see LaunchPage.js
|
||||||
|
installLaunchRedirect(router)
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
app.config.globalProperties.$io = socket
|
app.config.globalProperties.$io = socket
|
||||||
@@ -41,3 +46,6 @@ app.use(store)
|
|||||||
app.use(Helpers)
|
app.use(Helpers)
|
||||||
|
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
|
|
||||||
|
// After mount, so a failed registration can never block the app rendering
|
||||||
|
import('./registerServiceWorker')
|
||||||
@@ -44,6 +44,42 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- App / PWA -->
|
||||||
|
<h4 class="ui header">
|
||||||
|
App
|
||||||
|
</h4>
|
||||||
|
<div class="ui segment">
|
||||||
|
<div class="ui doubling grid">
|
||||||
|
<div class="sixteen wide column">
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Which page the installed app opens to. Set automatically to
|
||||||
|
whichever page you installed from, and changeable here.
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
v-for="option in launchOptions"
|
||||||
|
:key="option.path"
|
||||||
|
class="ui compact basic button"
|
||||||
|
:class="{'green':(launchPage == option.path)}"
|
||||||
|
v-on:click="chooseLaunchPage(option.path)">
|
||||||
|
{{ option.label }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ui divider"></div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
If the app gets stuck on an old version, this clears the stored
|
||||||
|
copies of its files and reloads. Your notes are not affected.
|
||||||
|
</p>
|
||||||
|
<div class="ui compact basic button" v-on:click="clearAppCache()">
|
||||||
|
<i class="redo icon"></i>
|
||||||
|
Reset App Cache
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Enable Two Factor -->
|
<!-- Enable Two Factor -->
|
||||||
<h4>Two Factor Authentication</h4>
|
<h4>Two Factor Authentication</h4>
|
||||||
<div class="ui segment">
|
<div class="ui segment">
|
||||||
@@ -165,6 +201,9 @@ import { defineAsyncComponent } from 'vue'
|
|||||||
|
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
|
import { LAUNCH_OPTIONS, getLaunchPage, setLaunchPage } from '@/LaunchPage'
|
||||||
|
import { resetAppCache } from '@/registerServiceWorker'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'SettingsPage',
|
name: 'SettingsPage',
|
||||||
components: {
|
components: {
|
||||||
@@ -177,6 +216,8 @@ import { defineAsyncComponent } from 'vue'
|
|||||||
verificationToken: '',
|
verificationToken: '',
|
||||||
showNewNoteConfirm: false,
|
showNewNoteConfirm: false,
|
||||||
|
|
||||||
|
launchOptions: LAUNCH_OPTIONS,
|
||||||
|
launchPage: getLaunchPage(),
|
||||||
themeColors: [
|
themeColors: [
|
||||||
'#21BA45', //Green
|
'#21BA45', //Green
|
||||||
'#b5cc18', //Lime
|
'#b5cc18', //Lime
|
||||||
@@ -198,6 +239,14 @@ import { defineAsyncComponent } from 'vue'
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
chooseLaunchPage(path){
|
||||||
|
setLaunchPage(path)
|
||||||
|
this.launchPage = getLaunchPage()
|
||||||
|
this.$bus.$emit('notification', 'App will open to ' + path)
|
||||||
|
},
|
||||||
|
clearAppCache(){
|
||||||
|
resetAppCache()
|
||||||
|
},
|
||||||
newQuickNote(){
|
newQuickNote(){
|
||||||
|
|
||||||
this.showNewNoteConfirm = true
|
this.showNewNoteConfirm = true
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const OverviewPage = () => import('@/pages/OverviewPage.vue')
|
|||||||
const BookmarkletPage = () => import('@/pages/BookmarkletPage.vue')
|
const BookmarkletPage = () => import('@/pages/BookmarkletPage.vue')
|
||||||
const NotFoundPage = () => import('@/pages/NotFoundPage.vue')
|
const NotFoundPage = () => import('@/pages/NotFoundPage.vue')
|
||||||
const MetrictrackingPage = () => import('@/pages/MetrictrackingPage.vue')
|
const MetrictrackingPage = () => import('@/pages/MetrictrackingPage.vue')
|
||||||
|
const ShareTargetPage = () => import('@/pages/ShareTargetPage.vue')
|
||||||
|
|
||||||
export default createRouter({
|
export default createRouter({
|
||||||
history: createWebHistory(),
|
history: createWebHistory(),
|
||||||
@@ -28,6 +29,7 @@ export default createRouter({
|
|||||||
{ path: '/bookmarklet', name: 'Bookmarklet', meta: { title: 'Bookmarklet' }, component: BookmarkletPage },
|
{ path: '/bookmarklet', name: 'Bookmarklet', meta: { title: 'Bookmarklet' }, component: BookmarkletPage },
|
||||||
{ path: '/settings', name: 'Settings', meta: { title: 'Settings' }, component: SettingsPage },
|
{ path: '/settings', name: 'Settings', meta: { title: 'Settings' }, component: SettingsPage },
|
||||||
{ path: '/public/note/:id/:token', name: 'Share', meta: { title: 'Shared' }, component: SharePage },
|
{ path: '/public/note/:id/:token', name: 'Share', meta: { title: 'Shared' }, component: SharePage },
|
||||||
|
{ path: '/share-target', name: 'Share Target', meta: { title: 'Saving Share' }, component: ShareTargetPage },
|
||||||
{ path: '/quick', name: 'Quick', meta: { title: 'Scratch Pad' }, component: QuickPage },
|
{ path: '/quick', name: 'Quick', meta: { title: 'Scratch Pad' }, component: QuickPage },
|
||||||
{ path: '/attachments', name: 'Attachments', meta: { title: 'Attachments' }, component: AttachmentsPage },
|
{ path: '/attachments', name: 'Attachments', meta: { title: 'Attachments' }, component: AttachmentsPage },
|
||||||
{ path: '/attachments/note/:id', name: 'Attachments for Note', meta: { title: 'Attachments for Note' }, component: AttachmentsPage },
|
{ path: '/attachments/note/:id', name: 'Attachments for Note', meta: { title: 'Attachments for Note' }, component: AttachmentsPage },
|
||||||
|
|||||||
@@ -1,8 +1,23 @@
|
|||||||
|
import fs from 'node:fs'
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
import tailwindcss from '@tailwindcss/vite'
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
|
import { VitePWA } from 'vite-plugin-pwa'
|
||||||
import { fileURLToPath, URL } from 'node:url'
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
|
|
||||||
|
// Opt-in HTTPS for the dev server, off by default so the usual
|
||||||
|
// http://localhost:5173 workflow is unchanged. Only needed if you want to test
|
||||||
|
// the installed PWA from a phone over the LAN *without* using
|
||||||
|
// chrome://flags/#unsafely-treat-insecure-origin-as-secure -- and note Chrome
|
||||||
|
// needs the mkcert root CA installed on the device for the origin to be
|
||||||
|
// service-worker eligible; clicking through a cert warning is not enough.
|
||||||
|
// DEV_HTTPS=1 pnpm dev
|
||||||
|
const certDir = fileURLToPath(new URL('../client/certs', import.meta.url))
|
||||||
|
const keyPath = `${certDir}/192.168.1.164+4-key.pem`
|
||||||
|
const certPath = `${certDir}/192.168.1.164+4.pem`
|
||||||
|
const useHttps =
|
||||||
|
process.env.DEV_HTTPS === '1' && fs.existsSync(keyPath) && fs.existsSync(certPath)
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
vue({
|
vue({
|
||||||
@@ -11,6 +26,59 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
tailwindcss(),
|
tailwindcss(),
|
||||||
|
VitePWA({
|
||||||
|
// Updates are user-driven via the prompt in src/registerServiceWorker.js
|
||||||
|
registerType: 'prompt',
|
||||||
|
injectRegister: false,
|
||||||
|
// Lands at dist/sw.js -- root of the served directory, so the worker gets
|
||||||
|
// root scope without needing a Service-Worker-Allowed header.
|
||||||
|
filename: 'sw.js',
|
||||||
|
// We ship public/manifest.json by hand rather than generating one.
|
||||||
|
manifest: false,
|
||||||
|
workbox: {
|
||||||
|
//
|
||||||
|
// SECURITY INVARIANT -- do not relax without thinking it through.
|
||||||
|
//
|
||||||
|
// Solid Scribe is end-to-end encrypted. The service worker precaches
|
||||||
|
// ONLY static build output and is network-only for /api/*. Caching an
|
||||||
|
// API response would write decrypted note content into Cache Storage on
|
||||||
|
// disk, where it survives logout and sits outside the encryption model.
|
||||||
|
//
|
||||||
|
// Structurally this holds because globPatterns only ever reads dist/,
|
||||||
|
// and user content is served by Express from staticFiles/ at
|
||||||
|
// /api/static -- which never enters dist. The NetworkOnly rule below is
|
||||||
|
// belt-and-braces: it documents the invariant and stops a future
|
||||||
|
// runtimeCaching entry from silently widening the net.
|
||||||
|
//
|
||||||
|
// woff2 is NOT in Workbox's default globPatterns. Without it the Roboto
|
||||||
|
// files are skipped and the offline shell renders in a fallback face.
|
||||||
|
globPatterns: ['**/*.{js,css,html,woff2,png,svg,ico,json}'],
|
||||||
|
// Vendor chunks (fomantic, chart.js) exceed the 2MiB default. An
|
||||||
|
// oversized file is silently OMITTED from the precache, which half
|
||||||
|
// breaks the offline shell -- watch the build output for a warning.
|
||||||
|
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
|
||||||
|
navigateFallback: '/index.html',
|
||||||
|
// Matters for top-level navigations to /api/static/<file> (clicking an
|
||||||
|
// attachment link) -- without this the worker answers with index.html.
|
||||||
|
navigateFallbackDenylist: [/^\/api\//, /^\/socket/],
|
||||||
|
cleanupOutdatedCaches: true,
|
||||||
|
// Let the user decide when to swap assets, rather than doing it
|
||||||
|
// mid-session underneath them.
|
||||||
|
clientsClaim: false,
|
||||||
|
skipWaiting: false,
|
||||||
|
runtimeCaching: [
|
||||||
|
{
|
||||||
|
urlPattern: ({ url }) =>
|
||||||
|
url.pathname.startsWith('/api/') || url.pathname.startsWith('/socket'),
|
||||||
|
handler: 'NetworkOnly',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// Flip to true temporarily to exercise the worker on the dev server.
|
||||||
|
// Remember to unregister it afterwards -- a lingering dev service worker
|
||||||
|
// is a classic "why is my phone showing an ancient build".
|
||||||
|
devOptions: { enabled: false, type: 'module' },
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
@@ -19,6 +87,9 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
|
...(useHttps
|
||||||
|
? { https: { key: fs.readFileSync(keyPath), cert: fs.readFileSync(certPath) } }
|
||||||
|
: {}),
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': 'http://localhost:3000',
|
'/api': 'http://localhost:3000',
|
||||||
'/socket': { target: 'http://localhost:3001', ws: true },
|
'/socket': { target: 'http://localhost:3001', ws: true },
|
||||||
|
|||||||
Reference in new issue
Block a user