create-top-secret-starter 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/templates/template-router/src/api/auth/guards.ts +7 -0
- package/templates/template-router/src/api/auth/session-store.ts +17 -7
- package/templates/template-router/src/providers/theme.tsx +2 -0
- package/templates/template-start/src/api/auth/guards.ts +7 -0
- package/templates/template-start/src/api/auth/session-store.ts +17 -7
- package/templates/template-start/src/providers/theme.tsx +2 -0
package/package.json
CHANGED
|
@@ -7,12 +7,19 @@ import { sessionStore } from '@/api/auth/session-store'
|
|
|
7
7
|
// never mirrors the session. Guards get these helpers so the redirect contract
|
|
8
8
|
// (`search.redirect` carries where to return after sign-in) lives in one module.
|
|
9
9
|
|
|
10
|
+
// Under SSR the session is unknowable (it lives in localStorage), so a server-side
|
|
11
|
+
// redirect would be wrong for signed-in users. Guards defer to the client: they
|
|
12
|
+
// no-op on the server and run again during hydration, where the session is real.
|
|
13
|
+
const isServer = typeof document === 'undefined'
|
|
14
|
+
|
|
10
15
|
export const requireSession = (location: { href: string }) => {
|
|
16
|
+
if (isServer) return
|
|
11
17
|
if (!sessionStore.get()) {
|
|
12
18
|
throw redirect({ to: '/sign-in', search: { redirect: location.href } })
|
|
13
19
|
}
|
|
14
20
|
}
|
|
15
21
|
|
|
16
22
|
export const redirectIfAuthenticated = (search: { redirect?: string }) => {
|
|
23
|
+
if (isServer) return
|
|
17
24
|
if (sessionStore.get()) throw redirect({ to: search.redirect ?? '/' })
|
|
18
25
|
}
|
|
@@ -17,23 +17,31 @@ const parseRaw = (raw: string | null): Session | null => {
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
// The session lives in localStorage, so it exists only in the browser. During SSR
|
|
21
|
+
// (TanStack Start) this module still gets imported by route guards: there the
|
|
22
|
+
// session is always null and writes are impossible — auth resolves after hydration.
|
|
23
|
+
const isBrowser = typeof window !== 'undefined'
|
|
24
|
+
|
|
25
|
+
let current = isBrowser ? parseRaw(localStorage.getItem(KEY)) : null
|
|
21
26
|
const listeners = new Set<Listener>()
|
|
22
27
|
|
|
23
28
|
const notify = () => listeners.forEach(l => l(current))
|
|
24
29
|
|
|
25
30
|
const commit = (next: Session | null) => {
|
|
31
|
+
if (!isBrowser) throw new Error('Session can only change in the browser')
|
|
26
32
|
current = next
|
|
27
33
|
if (next) localStorage.setItem(KEY, JSON.stringify(next))
|
|
28
34
|
else localStorage.removeItem(KEY)
|
|
29
35
|
notify()
|
|
30
36
|
}
|
|
31
37
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
38
|
+
if (isBrowser) {
|
|
39
|
+
window.addEventListener('storage', e => {
|
|
40
|
+
if (e.key !== KEY) return
|
|
41
|
+
current = parseRaw(e.newValue)
|
|
42
|
+
notify()
|
|
43
|
+
})
|
|
44
|
+
}
|
|
37
45
|
|
|
38
46
|
export const sessionStore = {
|
|
39
47
|
get: () => current,
|
|
@@ -53,4 +61,6 @@ export const sessionStore = {
|
|
|
53
61
|
|
|
54
62
|
// The React binding of the store's subscribe contract lives with the store: one
|
|
55
63
|
// concept, one file. Components use this; route guards use @/api/auth/guards.
|
|
56
|
-
|
|
64
|
+
// The server snapshot is always null: the session lives in localStorage (see above).
|
|
65
|
+
export const useSession = () =>
|
|
66
|
+
useSyncExternalStore(sessionStore.subscribe, sessionStore.get, () => null)
|
|
@@ -36,6 +36,8 @@ const applyTheme = (theme: Theme) => {
|
|
|
36
36
|
|
|
37
37
|
export const ThemeProvider = ({ children }: PropsWithChildren) => {
|
|
38
38
|
const [theme, setThemeState] = useState<Theme>(() => {
|
|
39
|
+
// the initializer also runs during SSR, where localStorage doesn't exist
|
|
40
|
+
if (typeof window === 'undefined') return DEFAULT_THEME
|
|
39
41
|
const stored = localStorage.getItem(STORAGE_KEY)
|
|
40
42
|
return isTheme(stored) ? stored : DEFAULT_THEME
|
|
41
43
|
})
|
|
@@ -7,12 +7,19 @@ import { sessionStore } from '@/api/auth/session-store'
|
|
|
7
7
|
// never mirrors the session. Guards get these helpers so the redirect contract
|
|
8
8
|
// (`search.redirect` carries where to return after sign-in) lives in one module.
|
|
9
9
|
|
|
10
|
+
// Under SSR the session is unknowable (it lives in localStorage), so a server-side
|
|
11
|
+
// redirect would be wrong for signed-in users. Guards defer to the client: they
|
|
12
|
+
// no-op on the server and run again during hydration, where the session is real.
|
|
13
|
+
const isServer = typeof document === 'undefined'
|
|
14
|
+
|
|
10
15
|
export const requireSession = (location: { href: string }) => {
|
|
16
|
+
if (isServer) return
|
|
11
17
|
if (!sessionStore.get()) {
|
|
12
18
|
throw redirect({ to: '/sign-in', search: { redirect: location.href } })
|
|
13
19
|
}
|
|
14
20
|
}
|
|
15
21
|
|
|
16
22
|
export const redirectIfAuthenticated = (search: { redirect?: string }) => {
|
|
23
|
+
if (isServer) return
|
|
17
24
|
if (sessionStore.get()) throw redirect({ to: search.redirect ?? '/' })
|
|
18
25
|
}
|
|
@@ -17,23 +17,31 @@ const parseRaw = (raw: string | null): Session | null => {
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
// The session lives in localStorage, so it exists only in the browser. During SSR
|
|
21
|
+
// (TanStack Start) this module still gets imported by route guards: there the
|
|
22
|
+
// session is always null and writes are impossible — auth resolves after hydration.
|
|
23
|
+
const isBrowser = typeof window !== 'undefined'
|
|
24
|
+
|
|
25
|
+
let current = isBrowser ? parseRaw(localStorage.getItem(KEY)) : null
|
|
21
26
|
const listeners = new Set<Listener>()
|
|
22
27
|
|
|
23
28
|
const notify = () => listeners.forEach(l => l(current))
|
|
24
29
|
|
|
25
30
|
const commit = (next: Session | null) => {
|
|
31
|
+
if (!isBrowser) throw new Error('Session can only change in the browser')
|
|
26
32
|
current = next
|
|
27
33
|
if (next) localStorage.setItem(KEY, JSON.stringify(next))
|
|
28
34
|
else localStorage.removeItem(KEY)
|
|
29
35
|
notify()
|
|
30
36
|
}
|
|
31
37
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
38
|
+
if (isBrowser) {
|
|
39
|
+
window.addEventListener('storage', e => {
|
|
40
|
+
if (e.key !== KEY) return
|
|
41
|
+
current = parseRaw(e.newValue)
|
|
42
|
+
notify()
|
|
43
|
+
})
|
|
44
|
+
}
|
|
37
45
|
|
|
38
46
|
export const sessionStore = {
|
|
39
47
|
get: () => current,
|
|
@@ -53,4 +61,6 @@ export const sessionStore = {
|
|
|
53
61
|
|
|
54
62
|
// The React binding of the store's subscribe contract lives with the store: one
|
|
55
63
|
// concept, one file. Components use this; route guards use @/api/auth/guards.
|
|
56
|
-
|
|
64
|
+
// The server snapshot is always null: the session lives in localStorage (see above).
|
|
65
|
+
export const useSession = () =>
|
|
66
|
+
useSyncExternalStore(sessionStore.subscribe, sessionStore.get, () => null)
|
|
@@ -36,6 +36,8 @@ const applyTheme = (theme: Theme) => {
|
|
|
36
36
|
|
|
37
37
|
export const ThemeProvider = ({ children }: PropsWithChildren) => {
|
|
38
38
|
const [theme, setThemeState] = useState<Theme>(() => {
|
|
39
|
+
// the initializer also runs during SSR, where localStorage doesn't exist
|
|
40
|
+
if (typeof window === 'undefined') return DEFAULT_THEME
|
|
39
41
|
const stored = localStorage.getItem(STORAGE_KEY)
|
|
40
42
|
return isTheme(stored) ? stored : DEFAULT_THEME
|
|
41
43
|
})
|