Fosi Fossil Service
Small teams. Open source. Built on Fossil.

IaC / SwiftFlow

High-performance WMS with wave picking, smart routing, and real-time inventory visibility.

Public | Owner: IaC | Default branch: trunk | Last update: 2026-10-23 14:37

Artifact 1bb3cc6e23b0bee3db1e97493ddc083d8b909a252932ae408f8898d5a061ae67:


import database.{type Conn, type DbError, IntParam, TextParam}
import gleam/dynamic/decode
import gleam/list
import gleam/option.{type Option, None, Some}
import gleam/result
import gleam/string
import ql361api/json_codec

pub type User {
  User(
    id: String,
    username: String,
    display_name: String,
    enabled: Bool,
    roles: List(String),
    warehouse_ids: List(Int),
    created_at: String,
    updated_at: String,
  )
}

pub type Credential {
  Credential(
    id: String,
    username: String,
    display_name: String,
    password_hash: String,
    enabled: Bool,
    failed_attempts: Int,
    locked_until: String,
  )
}

pub type SessionRow {
  SessionRow(
    user_id: String,
    terminal: String,
    warehouse_id: Int,
    expires_at: String,
  )
}

pub type AuditEntry {
  AuditEntry(
    id: String,
    user_id: String,
    username: String,
    action: String,
    target: String,
    warehouse_id: Int,
    detail: String,
    created_at: String,
  )
}

pub type RoleRow {
  RoleRow(
    code: String,
    name: String,
    terminals: List(String),
    permissions: List(String),
  )
}

pub fn count_users(conn: Conn) -> Result(Int, DbError) {
  count(conn, "SELECT COUNT(*) FROM users", [])
}

pub fn username_taken(conn: Conn, username: String) -> Result(Bool, DbError) {
  use n <- result.try(count(
    conn,
    "SELECT COUNT(*) FROM users WHERE username = ?",
    [TextParam(username)],
  ))
  Ok(n > 0)
}

pub fn find_credential(
  conn: Conn,
  username: String,
) -> Result(Option(Credential), DbError) {
  use rows <- result.try(database.query_on(
    conn,
    "SELECT id, username, display_name, password_hash, enabled,
            failed_attempts, locked_until
     FROM users WHERE username = ?",
    [TextParam(username)],
    {
      use id <- decode.field(0, decode.string)
      use username <- decode.field(1, decode.string)
      use display_name <- decode.field(2, decode.string)
      use password_hash <- decode.field(3, decode.string)
      use enabled <- decode.field(4, json_codec.int())
      use failed_attempts <- decode.field(5, json_codec.int())
      use locked_until <- decode.field(6, decode.string)
      decode.success(Credential(
        id: id,
        username: username,
        display_name: display_name,
        password_hash: password_hash,
        enabled: enabled != 0,
        failed_attempts: failed_attempts,
        locked_until: locked_until,
      ))
    },
  ))
  Ok(first(rows))
}

pub fn password_hash_of(conn: Conn, user_id: String) -> Result(Option(String), DbError) {
  use rows <- result.try(database.query_on(
    conn,
    "SELECT password_hash FROM users WHERE id = ?",
    [TextParam(user_id)],
    {
      use hash <- decode.field(0, decode.string)
      decode.success(hash)
    },
  ))
  Ok(first(rows))
}

pub fn record_login_failure(
  conn: Conn,
  user_id: String,
  attempts: Int,
  locked_until: String,
  now: String,
) -> Result(Nil, DbError) {
  database.exec_on(
    conn,
    "UPDATE users SET failed_attempts = ?, locked_until = ?, updated_at = ? WHERE id = ?",
    [IntParam(attempts), TextParam(locked_until), TextParam(now), TextParam(user_id)],
  )
}

pub fn reset_login_failures(conn: Conn, user_id: String) -> Result(Nil, DbError) {
  database.exec_on(
    conn,
    "UPDATE users SET failed_attempts = 0, locked_until = '' WHERE id = ?",
    [TextParam(user_id)],
  )
}

pub fn insert_user(
  conn: Conn,
  id: String,
  username: String,
  display_name: String,
  password_hash: String,
  now: String,
) -> Result(Nil, DbError) {
  database.exec_on(
    conn,
    "INSERT INTO users (id, username, display_name, password_hash, enabled,
                        failed_attempts, locked_until, created_at, updated_at)
     VALUES (?, ?, ?, ?, 1, 0, '', ?, ?)",
    [
      TextParam(id),
      TextParam(username),
      TextParam(display_name),
      TextParam(password_hash),
      TextParam(now),
      TextParam(now),
    ],
  )
}

pub fn update_profile(
  conn: Conn,
  user_id: String,
  display_name: String,
  enabled: Bool,
  now: String,
) -> Result(Nil, DbError) {
  database.exec_on(
    conn,
    "UPDATE users SET display_name = ?, enabled = ?, updated_at = ? WHERE id = ?",
    [
      TextParam(display_name),
      IntParam(case enabled {
        True -> 1
        False -> 0
      }),
      TextParam(now),
      TextParam(user_id),
    ],
  )
}

pub fn set_password(
  conn: Conn,
  user_id: String,
  password_hash: String,
  now: String,
) -> Result(Nil, DbError) {
  database.exec_on(
    conn,
    "UPDATE users SET password_hash = ?, failed_attempts = 0, locked_until = '',
                      updated_at = ?
     WHERE id = ?",
    [TextParam(password_hash), TextParam(now), TextParam(user_id)],
  )
}

pub fn set_roles(conn: Conn, user_id: String, roles: List(String)) -> Result(Nil, DbError) {
  use _ <- result.try(database.exec_on(
    conn,
    "DELETE FROM user_roles WHERE user_id = ?",
    [TextParam(user_id)],
  ))
  list.try_fold(roles, Nil, fn(_, role) {
    database.exec_on(
      conn,
      "INSERT INTO user_roles (user_id, role_code) VALUES (?, ?)",
      [TextParam(user_id), TextParam(role)],
    )
  })
}

pub fn set_warehouses(
  conn: Conn,
  user_id: String,
  warehouse_ids: List(Int),
) -> Result(Nil, DbError) {
  use _ <- result.try(database.exec_on(
    conn,
    "DELETE FROM user_warehouses WHERE user_id = ?",
    [TextParam(user_id)],
  ))
  list.try_fold(warehouse_ids, Nil, fn(_, warehouse_id) {
    database.exec_on(
      conn,
      "INSERT INTO user_warehouses (user_id, warehouse_id) VALUES (?, ?)",
      [TextParam(user_id), IntParam(warehouse_id)],
    )
  })
}

pub fn get_user(conn: Conn, user_id: String) -> Result(Option(User), DbError) {
  use rows <- result.try(database.query_on(
    conn,
    "SELECT id, username, display_name, enabled, created_at, updated_at
     FROM users WHERE id = ?",
    [TextParam(user_id)],
    user_header_decoder(),
  ))
  case rows {
    [] -> Ok(None)
    [header, ..] -> {
      use user <- result.try(fill_user(conn, header))
      Ok(Some(user))
    }
  }
}

pub fn list_users(conn: Conn) -> Result(List(User), DbError) {
  use rows <- result.try(database.query_on(
    conn,
    "SELECT id, username, display_name, enabled, created_at, updated_at
     FROM users ORDER BY username",
    [],
    user_header_decoder(),
  ))
  list.try_map(rows, fn(header) { fill_user(conn, header) })
}

pub fn user_roles(conn: Conn, user_id: String) -> Result(List(String), DbError) {
  database.query_on(
    conn,
    "SELECT role_code FROM user_roles WHERE user_id = ? ORDER BY role_code",
    [TextParam(user_id)],
    {
      use code <- decode.field(0, decode.string)
      decode.success(code)
    },
  )
}

pub fn user_warehouses(conn: Conn, user_id: String) -> Result(List(Int), DbError) {
  database.query_on(
    conn,
    "SELECT warehouse_id FROM user_warehouses WHERE user_id = ? ORDER BY warehouse_id",
    [TextParam(user_id)],
    {
      use id <- decode.field(0, json_codec.int())
      decode.success(id)
    },
  )
}

pub fn user_permissions(conn: Conn, user_id: String) -> Result(List(String), DbError) {
  database.query_on(
    conn,
    "SELECT DISTINCT rp.permission_code
     FROM user_roles ur
     JOIN role_permissions rp ON rp.role_code = ur.role_code
     WHERE ur.user_id = ?
     ORDER BY rp.permission_code",
    [TextParam(user_id)],
    {
      use code <- decode.field(0, decode.string)
      decode.success(code)
    },
  )
}

pub fn list_roles(conn: Conn) -> Result(List(RoleRow), DbError) {
  use roles <- result.try(database.query_on(
    conn,
    "SELECT code, name, terminals FROM roles ORDER BY code",
    [],
    {
      use code <- decode.field(0, decode.string)
      use name <- decode.field(1, decode.string)
      use terminals <- decode.field(2, decode.string)
      decode.success(#(code, name, terminals))
    },
  ))
  list.try_map(roles, fn(role) {
    let #(code, name, terminals) = role
    use permissions <- result.try(database.query_on(
      conn,
      "SELECT permission_code FROM role_permissions WHERE role_code = ?
       ORDER BY permission_code",
      [TextParam(code)],
      {
        use permission <- decode.field(0, decode.string)
        decode.success(permission)
      },
    ))
    Ok(RoleRow(
      code: code,
      name: name,
      terminals: split_list(terminals),
      permissions: permissions,
    ))
  })
}

pub fn insert_session(
  conn: Conn,
  token_hash: String,
  user_id: String,
  terminal: String,
  warehouse_id: Int,
  now: String,
  expires_at: String,
) -> Result(Nil, DbError) {
  database.exec_on(
    conn,
    "INSERT INTO user_sessions (token_hash, user_id, terminal, warehouse_id,
                                created_at, expires_at, last_seen_at)
     VALUES (?, ?, ?, ?, ?, ?, ?)",
    [
      TextParam(token_hash),
      TextParam(user_id),
      TextParam(terminal),
      IntParam(warehouse_id),
      TextParam(now),
      TextParam(expires_at),
      TextParam(now),
    ],
  )
}

pub fn find_session(
  conn: Conn,
  token_hash: String,
  now: String,
) -> Result(Option(SessionRow), DbError) {
  use rows <- result.try(database.query_on(
    conn,
    "SELECT s.user_id, s.terminal, s.warehouse_id, s.expires_at
     FROM user_sessions s
     JOIN users u ON u.id = s.user_id
     WHERE s.token_hash = ? AND s.expires_at > ? AND u.enabled = 1",
    [TextParam(token_hash), TextParam(now)],
    {
      use user_id <- decode.field(0, decode.string)
      use terminal <- decode.field(1, decode.string)
      use warehouse_id <- decode.field(2, json_codec.int())
      use expires_at <- decode.field(3, decode.string)
      decode.success(SessionRow(
        user_id: user_id,
        terminal: terminal,
        warehouse_id: warehouse_id,
        expires_at: expires_at,
      ))
    },
  ))
  Ok(first(rows))
}

pub fn touch_session(conn: Conn, token_hash: String, now: String) -> Result(Nil, DbError) {
  database.exec_on(
    conn,
    "UPDATE user_sessions SET last_seen_at = ? WHERE token_hash = ?",
    [TextParam(now), TextParam(token_hash)],
  )
}

pub fn set_session_warehouse(
  conn: Conn,
  token_hash: String,
  warehouse_id: Int,
) -> Result(Nil, DbError) {
  database.exec_on(
    conn,
    "UPDATE user_sessions SET warehouse_id = ? WHERE token_hash = ?",
    [IntParam(warehouse_id), TextParam(token_hash)],
  )
}

pub fn delete_session(conn: Conn, token_hash: String) -> Result(Nil, DbError) {
  database.exec_on(
    conn,
    "DELETE FROM user_sessions WHERE token_hash = ?",
    [TextParam(token_hash)],
  )
}

/// 撤销某用户的会话,`keep` 为要保留的当前会话(传空串则全部撤销)。
pub fn delete_user_sessions(
  conn: Conn,
  user_id: String,
  keep: String,
) -> Result(Nil, DbError) {
  database.exec_on(
    conn,
    "DELETE FROM user_sessions WHERE user_id = ? AND token_hash <> ?",
    [TextParam(user_id), TextParam(keep)],
  )
}

pub fn delete_expired_sessions(conn: Conn, now: String) -> Result(Nil, DbError) {
  database.exec_on(
    conn,
    "DELETE FROM user_sessions WHERE expires_at <= ?",
    [TextParam(now)],
  )
}

pub fn insert_audit(conn: Conn, entry: AuditEntry) -> Result(Nil, DbError) {
  database.exec_on(
    conn,
    "INSERT INTO audit_logs (id, user_id, username, action, target, warehouse_id,
                             detail, created_at)
     VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
    [
      TextParam(entry.id),
      TextParam(entry.user_id),
      TextParam(entry.username),
      TextParam(entry.action),
      TextParam(entry.target),
      IntParam(entry.warehouse_id),
      TextParam(entry.detail),
      TextParam(entry.created_at),
    ],
  )
}

pub fn list_audit(
  conn: Conn,
  limit: Int,
  offset: Int,
) -> Result(#(Int, List(AuditEntry)), DbError) {
  use total <- result.try(count(conn, "SELECT COUNT(*) FROM audit_logs", []))
  use rows <- result.try(database.query_on(
    conn,
    "SELECT id, user_id, username, action, target, warehouse_id, detail, created_at
     FROM audit_logs
     ORDER BY created_at DESC, id DESC
     LIMIT ? OFFSET ?",
    [IntParam(limit), IntParam(offset)],
    {
      use id <- decode.field(0, decode.string)
      use user_id <- decode.field(1, decode.string)
      use username <- decode.field(2, decode.string)
      use action <- decode.field(3, decode.string)
      use target <- decode.field(4, decode.string)
      use warehouse_id <- decode.field(5, json_codec.int())
      use detail <- decode.field(6, decode.string)
      use created_at <- decode.field(7, decode.string)
      decode.success(AuditEntry(
        id: id,
        user_id: user_id,
        username: username,
        action: action,
        target: target,
        warehouse_id: warehouse_id,
        detail: detail,
        created_at: created_at,
      ))
    },
  ))
  Ok(#(total, rows))
}

fn fill_user(conn: Conn, header: User) -> Result(User, DbError) {
  use roles <- result.try(user_roles(conn, header.id))
  use warehouses <- result.try(user_warehouses(conn, header.id))
  Ok(User(..header, roles: roles, warehouse_ids: warehouses))
}

fn user_header_decoder() -> decode.Decoder(User) {
  use id <- decode.field(0, decode.string)
  use username <- decode.field(1, decode.string)
  use display_name <- decode.field(2, decode.string)
  use enabled <- decode.field(3, json_codec.int())
  use created_at <- decode.field(4, decode.string)
  use updated_at <- decode.field(5, decode.string)
  decode.success(User(
    id: id,
    username: username,
    display_name: display_name,
    enabled: enabled != 0,
    roles: [],
    warehouse_ids: [],
    created_at: created_at,
    updated_at: updated_at,
  ))
}

fn count(
  conn: Conn,
  sql: String,
  params: List(database.Param),
) -> Result(Int, DbError) {
  use rows <- result.try(database.query_on(conn, sql, params, {
    use n <- decode.field(0, json_codec.int())
    decode.success(n)
  }))
  case rows {
    [n, ..] -> Ok(n)
    [] -> Ok(0)
  }
}

fn first(rows: List(a)) -> Option(a) {
  case rows {
    [row, ..] -> Some(row)
    [] -> None
  }
}

fn split_list(raw: String) -> List(String) {
  string.split(raw, ",")
  |> list.map(string.trim)
  |> list.filter(fn(item) { item != "" })
}

Clone

You can clone this repository with the following command:

fossil clone https://fosi.dev/IaC/SwiftFlow

Contribute

The best way to contribute is through e-mail. See the project documentation for more details.