feat: add auth guards and move legal pages to /legal

- Protected routes now redirect to timeline when not authenticated:
settings, edit-profile, create-gallery, notifications
- Moved terms, privacy, and copyright pages from /settings/* to /legal/*
so they remain publicly accessible
- Fixed logout to navigate before calling auth.logout()

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+3 -3
src/components/pages/grain-app.js
··· 57 57 .register('/profile/:handle', 'grain-profile') 58 58 .register('/settings', 'grain-settings') 59 59 .register('/settings/profile', 'grain-edit-profile') 60 - .register('/settings/terms', 'grain-terms') 61 - .register('/settings/privacy', 'grain-privacy') 62 - .register('/settings/copyright', 'grain-copyright') 60 + .register('/legal/terms', 'grain-terms') 61 + .register('/legal/privacy', 'grain-privacy') 62 + .register('/legal/copyright', 'grain-copyright') 63 63 .register('/create', 'grain-create-gallery') 64 64 .register('/explore', 'grain-explore') 65 65 .register('/notifications', 'grain-notifications')
+7
src/components/pages/grain-create-gallery.js
··· 140 140 141 141 connectedCallback() { 142 142 super.connectedCallback(); 143 + 144 + // Redirect to timeline if not authenticated 145 + if (!auth.isAuthenticated) { 146 + router.replace('/'); 147 + return; 148 + } 149 + 143 150 this._photos = draftGallery.getPhotos(); 144 151 if (!this._photos.length) { 145 152 router.push('/');
+7
src/components/pages/grain-edit-profile.js
··· 176 176 177 177 async connectedCallback() { 178 178 super.connectedCallback(); 179 + 180 + // Redirect to timeline if not authenticated 181 + if (!auth.isAuthenticated) { 182 + router.replace('/'); 183 + return; 184 + } 185 + 179 186 await this.#loadProfile(); 180 187 } 181 188
+7
src/components/pages/grain-notifications.js
··· 143 143 144 144 connectedCallback() { 145 145 super.connectedCallback(); 146 + 147 + // Redirect to timeline if not authenticated 148 + if (!auth.isAuthenticated) { 149 + router.replace('/'); 150 + return; 151 + } 152 + 146 153 this._unsubscribe = auth.subscribe(user => { 147 154 this._user = user; 148 155 if (user) {
+11 -4
src/components/pages/grain-settings.js
··· 91 91 92 92 connectedCallback() { 93 93 super.connectedCallback(); 94 + 95 + // Redirect to timeline if not authenticated 96 + if (!auth.isAuthenticated) { 97 + router.replace('/'); 98 + return; 99 + } 100 + 94 101 this._canInstall = pwa.canInstall; 95 102 this._showIOSInstructions = pwa.showIOSInstructions; 96 103 this.#unsubscribe = pwa.subscribe((canInstall) => { ··· 116 123 } 117 124 118 125 #signOut() { 119 - auth.logout(); 120 126 router.push('/'); 127 + auth.logout(); 121 128 } 122 129 123 130 #goToTerms() { 124 - router.push('/settings/terms'); 131 + router.push('/legal/terms'); 125 132 } 126 133 127 134 #goToPrivacy() { 128 - router.push('/settings/privacy'); 135 + router.push('/legal/privacy'); 129 136 } 130 137 131 138 #goToCopyright() { 132 - router.push('/settings/copyright'); 139 + router.push('/legal/copyright'); 133 140 } 134 141 135 142 render() {