Compare commits

...

11 Commits

Author SHA1 Message Date
hobokenchicken 09a59c6124 fix(tauri): disable WebKitGTK sandbox inside AppImage on Linux and bump version to 0.2.6
Release Desktop Apps / build-linux (push) Successful in 2m40s
Release Desktop Apps / build-windows (push) Successful in 3h16m19s
Release Desktop Apps / release (push) Successful in 8s
2026-07-16 15:28:36 -04:00
hobokenchicken f20f4aa6fa fix(tauri): set WEBKIT_DISABLE_DMABUF_RENDERER=1 on Linux to prevent white screen
Release Desktop Apps / build-linux (push) Successful in 2m41s
Release Desktop Apps / build-windows (push) Successful in 3h16m41s
Release Desktop Apps / release (push) Successful in 8s
2026-07-16 15:03:10 -04:00
hobokenchicken 9491f3a831 fix: use SameSite=Lax for session cookie
Release Desktop Apps / build-linux (push) Successful in 2m59s
Release Desktop Apps / release (push) Has been cancelled
Release Desktop Apps / build-windows (push) Has been cancelled
SameSite=None requires Secure=true or modern browsers silently reject
the Set-Cookie header. Since the site is served over HTTPS via Caddy,
this was causing login to succeed (200) but the session cookie to be
dropped, making the subsequent /auth/me call fail with 401.

SameSite=Lax is the correct setting for same-origin session cookies.
2026-07-16 14:52:13 -04:00
hobokenchicken 08e5d92059 fix: handle 401 gracefully on web; add Bearer token auth for Tauri
- fetchMe() no longer surfaces 401 as a user-facing error (it just
  means 'no session', not a failure)
- API client auto-clears auth state on 401 mid-session so the user
  gets redirected to login instead of seeing 'ERR: Request failed: 401'
- Session middleware now accepts Authorization: Bearer <token> header
  as fallback when no cookie is present (for Tauri/native clients)
- Login, register, and WebAuthn endpoints expose X-Session-Token header
  so non-browser clients can capture the token
2026-07-16 14:46:17 -04:00
hobokenchicken 92be2a30d1 fix(desktop): implement bearer token auth for windows webview2
Release Desktop Apps / build-linux (push) Successful in 2m49s
Release Desktop Apps / release (push) Has been cancelled
Release Desktop Apps / build-windows (push) Has been cancelled
2026-07-16 14:30:28 -04:00
hobokenchicken f4f6e8560b fix(auth): set SameSite=None for session cookies to fix cross-origin session loss in Tauri apps 2026-07-16 14:26:18 -04:00
hobokenchicken 1900dd9cb1 fix(api): add CORS headers to allow cross-origin requests from Tauri desktop app 2026-07-16 14:21:15 -04:00
hobokenchicken 1226bd28aa added firebase json 2026-07-16 14:07:29 -04:00
hobokenchicken 7cdee73542 fix(tauri): unregister and prevent service worker to fix 404 cache routing issues
Release Desktop Apps / build-linux (push) Successful in 3m17s
Release Desktop Apps / build-windows (push) Successful in 3h17m8s
Release Desktop Apps / release (push) Successful in 13s
2026-07-16 13:58:06 -04:00
hobokenchicken e8ba8ffdba fix(tauri): redirect relative fetch and websocket urls to production backend for desktop app
Release Desktop Apps / build-linux (push) Successful in 3m2s
Release Desktop Apps / build-windows (push) Successful in 3h24m0s
Release Desktop Apps / release (push) Successful in 10s
2026-07-16 13:27:33 -04:00
hobokenchicken 52298d1d46 fix(ci): manually configure caching using actions/cache@v3 for gitea compatibility
Release Desktop Apps / build-linux (push) Successful in 10m35s
Release Desktop Apps / build-windows (push) Successful in 3h16m21s
Release Desktop Apps / release (push) Successful in 11s
2026-07-16 13:03:56 -04:00
18 changed files with 563 additions and 22 deletions
+21 -10
View File
@@ -10,14 +10,21 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: { node-version: 20 }
- name: Cache npm
uses: actions/cache@v3
with: with:
node-version: 20 path: ~/.npm
cache: 'npm' key: ${{ runner.os }}-npm-${{ hashFiles('web/package-lock.json') }}
cache-dependency-path: 'web/package-lock.json'
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2 - name: Cache Rust target and registry
uses: actions/cache@v3
with: with:
workspaces: 'web/src-tauri' path: |
~/.cargo/registry
~/.cargo/git
web/src-tauri/target
key: ${{ runner.os }}-cargo-${{ hashFiles('web/src-tauri/Cargo.lock') }}
- name: Install system deps - name: Install system deps
run: sudo apt-get update && sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libappindicator3-dev librsvg2-dev patchelf rpm run: sudo apt-get update && sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libappindicator3-dev librsvg2-dev patchelf rpm
- name: Build frontend - name: Build frontend
@@ -40,10 +47,12 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: { node-version: 20 }
- name: Cache npm
uses: actions/cache@v3
with: with:
node-version: 20 path: ~/.npm
cache: 'npm' key: ${{ runner.os }}-npm-${{ hashFiles('web/package-lock.json') }}
cache-dependency-path: 'web/package-lock.json'
- name: Install Rust - name: Install Rust
run: | run: |
curl.exe -sLo rustup-init.exe https://win.rustup.rs/x86_64 curl.exe -sLo rustup-init.exe https://win.rustup.rs/x86_64
@@ -51,9 +60,11 @@ jobs:
set PATH=%USERPROFILE%\.cargo\bin;%PATH% set PATH=%USERPROFILE%\.cargo\bin;%PATH%
rustc --version rustc --version
shell: cmd shell: cmd
- uses: Swatinem/rust-cache@v2 - name: Cache Rust target and registry
uses: Swatinem/rust-cache@v2
with: with:
workspaces: 'web/src-tauri' workspaces: |
web/src-tauri
- name: Build frontend - name: Build frontend
run: cd web && npm ci && npm run build run: cd web && npm ci && npm run build
shell: cmd shell: cmd
+12 -1
View File
@@ -37,6 +37,7 @@ import (
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/webhook" "git.dustin.coffee/hobokenchicken/dumpsterChat/internal/webhook"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
chimw "github.com/go-chi/chi/v5/middleware" chimw "github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/cors"
httpSwagger "github.com/swaggo/http-swagger" httpSwagger "github.com/swaggo/http-swagger"
) )
@@ -70,7 +71,7 @@ func main() {
sessionStore := auth.NewSessionStore(database.DB, cfg) sessionStore := auth.NewSessionStore(database.DB, cfg)
// WebSocket origin allowlist // WebSocket origin allowlist
wsOrigins := []string{"https://" + cfg.Host} wsOrigins := []string{"https://" + cfg.Host, "http://tauri.localhost", "https://tauri.localhost", "tauri://localhost"}
if cfg.Host == "localhost" { if cfg.Host == "localhost" {
wsOrigins = append(wsOrigins, "http://localhost:"+cfg.Port) wsOrigins = append(wsOrigins, "http://localhost:"+cfg.Port)
} }
@@ -119,6 +120,16 @@ func main() {
memberHandler := server.NewMemberHandler(database.DB) memberHandler := server.NewMemberHandler(database.DB)
r := chi.NewRouter() r := chi.NewRouter()
r.Use(cors.Handler(cors.Options{
AllowedOrigins: []string{"https://" + cfg.Host, "http://localhost:" + cfg.Port, "http://tauri.localhost", "https://tauri.localhost", "tauri://localhost"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
ExposedHeaders: []string{"Link", "X-Session-Token"},
AllowCredentials: true,
MaxAge: 300,
}))
r.Use(chimw.Logger) r.Use(chimw.Logger)
r.Use(chimw.Recoverer) r.Use(chimw.Recoverer)
r.Use(chimw.RequestID) r.Use(chimw.RequestID)
Binary file not shown.
Binary file not shown.
+369
View File
@@ -0,0 +1,369 @@
# Capacitor Android App — Implementation Plan
> **For Hermes:** Use subagent-driven-development skill to implement this plan task-by-task.
**Goal:** Wrap the existing dumpsterChat Vite/React PWA in a Capacitor shell and publish to Google Play.
**Architecture:** Capacitor loads the Vite build output as local assets in an Android WebView. `@capacitor/core` bridges native APIs (push, status bar, etc.). No UI rewrite — the web app IS the app.
**Tech Stack:** Vite, React 18, Capacitor 6, FCM (push), Gradle (Android build)
---
### Phase 1: Capacitor Init
#### Task 1: Add Capacitor dependencies
**Objective:** Install Capacitor core + CLI in the web project.
**Files:**
- Modify: `web/package.json`
**Steps:**
```bash
cd web
npm install @capacitor/core @capacitor/cli @capacitor/android
```
Then init Capacitor:
```bash
npx cap init "Dumpster Chat" "coffee.dustin.dumpster" --web-dir dist
```
This creates `capacitor.config.ts` at the web root.
**Verify:** `cat capacitor.config.ts` shows appId `coffee.dustin.dumpster`, webDir `dist`.
---
#### Task 2: Configure capacitor.config.ts
**Objective:** Set server URL for dev, configure Android-specific settings.
**Files:**
- Modify: `web/capacitor.config.ts`
**Content:**
```ts
import type { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'coffee.dustin.dumpster',
appName: 'Dumpster Chat',
webDir: 'dist',
server: {
// ponytail: no server.url — serve local assets. API calls go to absolute URL from api.ts.
androidScheme: 'https', // cookies work over https scheme in WebView
},
plugins: {
PushNotifications: {
presentationOptions: ['badge', 'sound', 'alert'],
},
},
};
export default config;
```
**Key decisions:**
- `androidScheme: 'https'` makes `credentials: 'include'` cookies work in the WebView (http scheme blocks them).
- No `server.url` — local assets load from the APK, not from the web. Faster, works offline.
---
#### Task 3: Add Android platform
**Objective:** Generate the native Android project.
**Files:**
- Create: `web/android/` (generated by Capacitor)
**Steps:**
```bash
cd web
npx cap add android
```
**Verify:** `ls web/android/app/src/main/AndroidManifest.xml` exists.
---
### Phase 2: API Client Fix
#### Task 4: Update API base URL for native
**Objective:** When running in Capacitor, API calls need an absolute URL (no origin in a WebView). Keep relative paths for web/PWA.
**Files:**
- Modify: `web/src/lib/api.ts`
**Changes:**
```ts
import { Capacitor } from '@capacitor/core';
// ponytail: single switch. native = absolute URL, web = relative (Caddy same-origin).
const API_BASE = Capacitor.isNativePlatform()
? 'https://dumpster.dustin.coffee/api/v1'
: '/api/v1';
```
The rest of the file stays unchanged. `Capacitor.isNativePlatform()` returns `false` in browsers and `true` in the Android WebView.
**Skipped:** `@capacitor/http` plugin. Not needed — `androidScheme: 'https'` + absolute URL + `credentials: 'include'` works. Add the HTTP plugin only if cookies break.
---
### Phase 3: Push Notifications (FCM)
This is the only non-trivial part. VAPID web push does not work in Android WebViews. Need FCM.
#### Task 5: Create Firebase project
**Objective:** Set up FCM credentials for native push.
**Steps (manual, one-time):**
1. Go to https://console.firebase.google.com
2. Create project (or use existing) named `dumpster-chat`
3. Add Android app with package name `coffee.dustin.dumpster`
4. Download `google-services.json` → place in `web/android/app/google-services.json`
5. In Firebase Console → Project Settings → Cloud Messaging → note the **Server Key** (legacy) or set up **Firebase Admin SDK** service account
**Verify:** `google-services.json` exists in `web/android/app/`.
---
#### Task 6: Add Capacitor Push Notifications plugin
**Objective:** Register for FCM token on Android, send it to the server.
**Files:**
- Modify: `web/package.json` (install plugin)
- Modify: `web/src/stores/push.ts` (add native branch)
**Install:**
```bash
cd web
npm install @capacitor/push-notifications
```
**Modify `push.ts`** — add a native registration path alongside the existing web push:
```ts
import { Capacitor } from '@capacitor/core';
// Existing web push subscribe stays as-is for PWA.
// Add native branch:
async function subscribeNative() {
const { PushNotifications } = await import('@capacitor/push-notifications');
const permStatus = await PushNotifications.requestPermissions();
if (permStatus.receive !== 'granted') return;
await PushNotifications.register();
// Server sends us the FCM token via this event
PushNotifications.addListener('registration', async (token) => {
await api.post('/push/subscribe', {
endpoint: 'fcm:' + token.value, // ponytail: prefix to distinguish from web push endpoints
keys: { p256dh: '', auth: '' }, // not used for FCM, but server expects the shape
});
});
PushNotifications.addListener('pushNotificationReceived', (notification) => {
// Foreground notification — show in-app toast or badge
// ponytail: handled by existing in-app notification system
});
}
```
Then in the existing `subscribe()` function, branch:
```ts
if (Capacitor.isNativePlatform()) {
return subscribeNative();
}
// ... existing web push logic
```
---
#### Task 7: Server-side FCM send support
**Objective:** When a push subscription's endpoint starts with `fcm:`, send via FCM HTTP v1 API instead of VAPID.
**Files:**
- Modify: `internal/push/handlers.go`
**Changes:**
1. In `Subscribe()`: detect `fcm:` prefix on endpoint, store differently (or store as-is, the prefix distinguishes it).
2. In the send functions (`Send`, `SendToUser`): check if subscription endpoint starts with `fcm:` → use Firebase Admin SDK to send.
**Install Go Firebase Admin:**
```bash
go get firebase.google.com/go/v4
```
**Pattern:**
```go
// ponytail: one if/else in the send loop. endpoint prefix = routing key.
if strings.HasPrefix(sub.Endpoint, "fcm:") {
token := strings.TrimPrefix(sub.Endpoint, "fcm:")
msg := &messaging.Message{
Token: token,
Notification: &messaging.Notification{
Title: title,
Body: body,
},
Data: map[string]string{"url": url},
}
_, err = fcmClient.Send(ctx, msg)
} else {
// existing VAPID webpush send
}
```
**Config:** Add `FIREBASE_CREDENTIALS_FILE` env var (path to service account JSON) to the systemd unit / Docker compose.
**Skipped:** Topic-based broadcast. Per-device tokens is fine for now. Add topics when channel count grows.
---
### Phase 4: Gradle / Build Config
#### Task 8: Configure Android build
**Objective:** Set minimum SDK, app icon, theme.
**Files:**
- Modify: `web/android/app/build.gradle`
- Modify: `web/android/app/src/main/res/values/strings.xml`
**Changes in `build.gradle`:**
```gradle
minSdkVersion = 24 // ponytail: Android 7+ covers 99% of Play Store. lower = more compat bugs.
```
**App name in `strings.xml`:**
```xml
<string name="app_name">Dumpster Chat</string>
```
**App icon:** Copy existing PWA icons into Android mipmap directories:
```bash
# Capacitor can sync icons automatically if placed at web/public/icon.png (1024x1024)
# or manually: web/android/app/src/main/res/mipmap-*/
npx cap assets generate # if a 1024x1024 source icon exists
```
---
### Phase 5: Build & Publish
#### Task 9: Sync and build debug APK
**Objective:** Verify the app runs on a real device or emulator.
**Steps:**
```bash
cd web
npm run build # builds Vite → dist/
npx cap sync android # copies dist/ into android/assets, syncs plugins
cd android
./gradlew assembleDebug
```
**Output:** `web/android/app/build/outputs/apk/debug/app-debug.apk`
**Verify:** Install on Android device:
```bash
adb install app-debug.apk
```
---
#### Task 10: Build signed release AAB for Play Store
**Objective:** Create a signed Android App Bundle (.aab) for Google Play upload.
**Steps:**
1. Generate keystore (one-time):
```bash
keytool -genkey -v -keystore dumpster-release.jks -keyalg RSA -keysize 2048 -validity 10000 -alias dumpster
```
Store `dumpster-release.jks` securely. Back it up. Lose it = can't update the app.
2. Add signing config to `web/android/app/build.gradle`:
```gradle
android {
signingConfigs {
release {
storeFile file('dumpster-release.jks')
storePassword System.getenv('KEYSTORE_PASSWORD')
keyAlias 'dumpster'
keyPassword System.getenv('KEY_PASSWORD')
}
}
buildTypes {
release {
signingConfig signingConfigs.release
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
```
3. Build AAB:
```bash
cd web/android
KEYSTORE_PASSWORD=xxx KEY_PASSWORD=xxx ./gradlew bundleRelease
```
**Output:** `web/android/app/build/outputs/bundle/release/app-release.aab`
4. Upload to Google Play Console → your new developer account → Create app → Upload AAB.
---
#### Task 11: Clean up Tauri dependencies
**Objective:** Remove unused Tauri packages (pivoted away from Tauri).
**Files:**
- Modify: `web/package.json`
**Steps:**
```bash
cd web
npm uninstall @tauri-apps/api @tauri-apps/cli
```
---
### Summary
| Phase | What | Time estimate |
|-------|------|---------------|
| 1 | Capacitor init + Android platform | 10 min |
| 2 | API base URL native branch | 5 min |
| 3 | FCM push (plugin + server) | 1-2 hrs (incl. Firebase setup) |
| 4 | Gradle config / icons | 15 min |
| 5 | Build, test, publish | 30 min |
**Total:** ~2-3 hours end-to-end. Phase 3 is the only real work.
**Dependencies between tasks:**
- Tasks 1-3 sequential (Capacitor init)
- Task 4 independent of 5-7
- Tasks 5-7 sequential (FCM chain)
- Task 8 depends on 1-3
- Task 9 depends on all above
- Task 10 depends on 9
- Task 11 independent, do anytime
**After this plan:** Update the Makefile `build` target to include `npm run build && npx cap sync android` so deploys sync native assets too.
+1
View File
@@ -49,6 +49,7 @@ require (
github.com/fsnotify/fsnotify v1.10.1 // indirect github.com/fsnotify/fsnotify v1.10.1 // indirect
github.com/fxamacker/cbor/v2 v2.9.2 // indirect github.com/fxamacker/cbor/v2 v2.9.2 // indirect
github.com/gammazero/deque v1.2.1 // indirect github.com/gammazero/deque v1.2.1 // indirect
github.com/go-chi/cors v1.2.2 // indirect
github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect
+2
View File
@@ -87,6 +87,8 @@ github.com/gammazero/deque v1.2.1 h1:9fnQVFCCZ9/NOc7ccTNqzoKd1tCWOqeI05/lPqFPMGQ
github.com/gammazero/deque v1.2.1/go.mod h1:5nSFkzVm+afG9+gy0VIowlqVAW4N8zNcMne+CMQVD2g= github.com/gammazero/deque v1.2.1/go.mod h1:5nSFkzVm+afG9+gy0VIowlqVAW4N8zNcMne+CMQVD2g=
github.com/go-chi/chi/v5 v5.3.0 h1:halUjDxhshgXHMrao5bB8eNBXo/rnzwr8m5m36glehM= github.com/go-chi/chi/v5 v5.3.0 h1:halUjDxhshgXHMrao5bB8eNBXo/rnzwr8m5m36glehM=
github.com/go-chi/chi/v5 v5.3.0/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto= github.com/go-chi/chi/v5 v5.3.0/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+1
View File
@@ -21,3 +21,4 @@ func SetSessionCookie(w http.ResponseWriter, cookieName, token string, duration
MaxAge: int(duration.Seconds()), MaxAge: int(duration.Seconds()),
}) })
} }
+2
View File
@@ -331,6 +331,7 @@ func (h *Handler) Register(w http.ResponseWriter, r *http.Request) {
} }
h.setSessionCookie(w, token) h.setSessionCookie(w, token)
w.Header().Set("X-Session-Token", token)
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"id": userID}) json.NewEncoder(w).Encode(map[string]string{"id": userID})
} }
@@ -383,6 +384,7 @@ func (h *Handler) Login(w http.ResponseWriter, r *http.Request) {
} }
h.setSessionCookie(w, token) h.setSessionCookie(w, token)
w.Header().Set("X-Session-Token", token)
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"id": userID}) json.NewEncoder(w).Encode(map[string]string{"id": userID})
} }
+5 -2
View File
@@ -234,7 +234,8 @@ func (h *WebAuthnHandler) LoginBegin(w http.ResponseWriter, r *http.Request) {
Path: "/", Path: "/",
HttpOnly: true, HttpOnly: true,
MaxAge: 300, // 5 minutes MaxAge: 300, // 5 minutes
SameSite: http.SameSiteStrictMode, SameSite: http.SameSiteNoneMode,
Secure: true,
}) })
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
@@ -341,9 +342,11 @@ func (h *WebAuthnHandler) LoginFinish(w http.ResponseWriter, r *http.Request) {
Path: "/", Path: "/",
HttpOnly: true, HttpOnly: true,
MaxAge: -1, MaxAge: -1,
SameSite: http.SameSiteStrictMode, SameSite: http.SameSiteNoneMode,
Secure: true,
}) })
w.Header().Set("X-Session-Token", token)
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "authenticated"}) json.NewEncoder(w).Encode(map[string]string{"status": "authenticated"})
} }
+12 -2
View File
@@ -18,13 +18,23 @@ type SessionStore interface {
func Session(store SessionStore, cfg *config.Config) func(http.Handler) http.Handler { func Session(store SessionStore, cfg *config.Config) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var token string
cookie, err := r.Cookie(cfg.Session.CookieName) cookie, err := r.Cookie(cfg.Session.CookieName)
if err != nil { if err == nil {
token = cookie.Value
} else {
authHeader := r.Header.Get("Authorization")
if len(authHeader) > 7 && authHeader[:7] == "Bearer " {
token = authHeader[7:]
}
}
if token == "" {
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
return return
} }
userID, err := store.GetUserIDByToken(r.Context(), cookie.Value) userID, err := store.GetUserIDByToken(r.Context(), token)
if err != nil { if err != nil {
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
return return
+29
View File
@@ -0,0 +1,29 @@
{
"project_info": {
"project_number": "629643353973",
"project_id": "dumpster-chat",
"storage_bucket": "dumpster-chat.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:629643353973:android:29dcc703959dd306c0fd3c",
"android_client_info": {
"package_name": "coffee.dustin.dumpster"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyDt3h4G-imzD7IedRqYOUBIv8CtZQT2YyA"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
],
"configuration_version": "1"
}
+13 -4
View File
@@ -60,11 +60,20 @@
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>
<script> <script>
// Register service worker // Register service worker (only for web, avoid in Tauri to prevent 404 cache bugs)
if ('serviceWorker' in navigator) { if ('serviceWorker' in navigator) {
window.addEventListener('load', () => { if (!('__TAURI_INTERNALS__' in window) && !('__TAURI__' in window)) {
navigator.serviceWorker.register('/sw.js').catch(() => {}); window.addEventListener('load', () => {
}); navigator.serviceWorker.register('/sw.js').catch(() => {});
});
} else {
// In Tauri, aggressively unregister any old service workers that might be causing 404s
navigator.serviceWorker.getRegistrations().then((registrations) => {
for (let registration of registrations) {
registration.unregister();
}
});
}
} }
</script> </script>
</body> </body>
+8
View File
@@ -2,5 +2,13 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() { fn main() {
#[cfg(target_os = "linux")]
{
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
std::env::set_var("WEBKIT_DISABLE_SANDBOX_THIS_IS_DANGEROUS", "1");
}
app_lib::run(); app_lib::run();
} }
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "dumpsterChat", "productName": "dumpsterChat",
"version": "0.2.0", "version": "0.2.6",
"identifier": "coffee.dustin.dumpsterchat", "identifier": "coffee.dustin.dumpsterchat",
"build": { "build": {
"frontendDist": "../dist", "frontendDist": "../dist",
+13
View File
@@ -40,6 +40,19 @@ async function request<T>(method: string, path: string, body?: unknown): Promise
const ra = response.headers.get('Retry-After'); const ra = response.headers.get('Retry-After');
err.retryAfter = retryAfter ?? (ra ? parseInt(ra, 10) : undefined); err.retryAfter = retryAfter ?? (ra ? parseInt(ra, 10) : undefined);
} }
// Auto-logout on 401 for any non-login endpoint so expired sessions
// redirect to the login page instead of showing cryptic errors.
if (response.status === 401 && !path.startsWith('/auth/login') && !path.startsWith('/auth/register')) {
// Dynamically import to avoid circular deps
import('../stores/auth.ts').then(({ useAuthStore }) => {
const state = useAuthStore.getState();
if (state.isAuthenticated) {
useAuthStore.setState({ user: null, isAuthenticated: false, error: null });
}
});
}
throw err; throw err;
} }
+65
View File
@@ -3,6 +3,71 @@ import { createRoot } from 'react-dom/client';
import './styles/index.css'; import './styles/index.css';
import App from './App.tsx'; import App from './App.tsx';
const isTauri = 'window' in globalThis && '__TAURI_INTERNALS__' in window;
if (isTauri) {
const TARGET = 'https://dumpster.dustin.coffee';
const WS_TARGET = 'wss://dumpster.dustin.coffee';
let sessionToken = localStorage.getItem('dumpster_session_token') || '';
const originalFetch = window.fetch;
window.fetch = async (input, init) => {
let url = typeof input === 'string' ? input : input.toString();
if (url.startsWith('/')) {
url = TARGET + url;
}
let newInit = init ? { ...init } : {};
if (sessionToken) {
newInit.headers = {
...newInit.headers,
'Authorization': 'Bearer ' + sessionToken
};
}
const response = await originalFetch(url, newInit);
const token = response.headers.get('X-Session-Token');
if (token) {
sessionToken = token;
localStorage.setItem('dumpster_session_token', token);
}
if (url.endsWith('/auth/logout') && response.ok) {
sessionToken = '';
localStorage.removeItem('dumpster_session_token');
}
return response;
};
const OriginalWebSocket = window.WebSocket;
window.WebSocket = function(url: string | URL, protocols?: string | string[]) {
let urlStr = typeof url === 'string' ? url : url.toString();
if (urlStr.includes('tauri.localhost') || urlStr.includes('tauri://localhost')) {
urlStr = urlStr.replace(/^(wss?|https?|tauri):\/\/tauri\.localhost/, WS_TARGET);
urlStr = urlStr.replace(/^tauri:\/\/localhost/, WS_TARGET);
} else if (urlStr.startsWith('ws://localhost') && isTauri) {
urlStr = urlStr.replace('ws://localhost', WS_TARGET);
} else if (urlStr.startsWith('/')) {
urlStr = WS_TARGET + urlStr;
}
const ws = new OriginalWebSocket(urlStr, protocols);
if (sessionToken && urlStr.includes('/ws') && !urlStr.includes('/ws/bot')) {
const originalOnOpen = ws.onopen;
ws.onopen = function(ev) {
ws.send(JSON.stringify({ token: sessionToken }));
if (originalOnOpen) originalOnOpen.call(ws, ev);
};
}
return ws;
} as unknown as typeof WebSocket;
window.WebSocket.prototype = OriginalWebSocket.prototype;
}
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(
<StrictMode> <StrictMode>
<App /> <App />
+9 -2
View File
@@ -151,12 +151,19 @@ export const useAuthStore = create<AuthState>((set) => ({
set({ user, isAuthenticated: true, isLoading: false }); set({ user, isAuthenticated: true, isLoading: false });
autoSubscribePush(); autoSubscribePush();
} catch (error) { } catch (error) {
// 401 from /auth/me simply means "no active session" — not a
// user-facing error. Only surface non-auth failures.
const isAuthError =
error instanceof Error && (error as import('../lib/api.ts').ApiError).status === 401;
set({ set({
user: null, user: null,
isAuthenticated: false, isAuthenticated: false,
isLoading: false, isLoading: false,
error: error: isAuthError
error instanceof Error ? error.message : "Failed to fetch user", ? null
: error instanceof Error
? error.message
: "Failed to fetch user",
}); });
} }
}, },