Skip to main content
All Examples

Auth

Authentication flow with Submodels, OutMessage, protected routes, and session management.

Auth
Routing
Submodels
OutMessage
/
import { BrowserKeyValueStore } from '@effect/platform-browser'
import { Effect, Match as M, Option, Schema as S } from 'effect'
import { KeyValueStore } from 'effect/unstable/persistence'
import { Command, Runtime } from 'foldkit'
import { replaceUrl } from 'foldkit/navigation'
import { Url } from 'foldkit/url'

import { SESSION_STORAGE_KEY } from './constant'
import { Session } from './domain/session'
import { CompletedNavigateInternal, Message } from './message'
import { LoggedIn, LoggedOut, Model } from './model'
import {
  DashboardRoute,
  LoginRoute,
  dashboardRouter,
  loginRouter,
  urlToAppRoute,
} from './route'

// FLAGS

export const Flags = S.Struct({
  maybeSession: S.Option(Session),
})

export const flags: Effect.Effect<Flags> = Effect.gen(function* () {
  const store = yield* KeyValueStore.KeyValueStore
  const sessionJson = yield* Effect.fromOption(
    Option.fromNullishOr(yield* store.get(SESSION_STORAGE_KEY)),
  )

  const decodeSession = S.decodeEffect(S.fromJsonString(Session))
  const session = yield* decodeSession(sessionJson)

  return Flags.make({ maybeSession: Option.some(session) })
}).pipe(
  Effect.catch(() =>
    Effect.succeed(Flags.make({ maybeSession: Option.none() })),
  ),
  Effect.provide(BrowserKeyValueStore.layerLocalStorage),
)

export type Flags = typeof Flags.Type

// COMMAND

const RedirectToLogin = Command.define(
  'RedirectToLogin',
  CompletedNavigateInternal,
)(replaceUrl(loginRouter()).pipe(Effect.as(CompletedNavigateInternal())))

const RedirectToDashboard = Command.define(
  'RedirectToDashboard',
  CompletedNavigateInternal,
)(replaceUrl(dashboardRouter()).pipe(Effect.as(CompletedNavigateInternal())))

// INIT

type InitReturn = [Model, ReadonlyArray<Command.Command<Message>>]
const withInitReturn = M.withReturnType<InitReturn>()

export const init: Runtime.RoutingApplicationInit<Model, Message, Flags> = (
  flags: Flags,
  url: Url,
): InitReturn => {
  const route = urlToAppRoute(url)

  return Option.match(flags.maybeSession, {
    onNone: () =>
      M.value(route).pipe(
        withInitReturn,
        M.tag('Home', 'Login', 'NotFound', route => [
          LoggedOut.init(route),
          [],
        ]),
        M.orElse(() => [LoggedOut.init(LoginRoute()), [RedirectToLogin()]]),
      ),

    onSome: session =>
      M.value(route).pipe(
        withInitReturn,
        M.tag('Dashboard', 'Settings', 'NotFound', route => [
          LoggedIn.init(route, session),
          [],
        ]),
        M.orElse(() => [
          LoggedIn.init(DashboardRoute(), session),
          [RedirectToDashboard()],
        ]),
      ),
  })
}