glacier-web-cli 0.5.0

Command-line tool for creating and running Glacier applications.
# A token a user makes for a script or another app to use their account,
# limited to what its scopes allow: `Authorization: Bearer glc_…`. An
# action checks a scope with `glacier::auth::require_scope(&ctx, "write")?`;
# a browser's session may do anything its user may.
module ApiToken do
  implements Model

  field name: String, required
  field token_digest: String, required, unique, protected
  field scopes: String, required
  field last_used_at: Option<Timestamp>, protected
  field expires_at: Option<Timestamp>, protected
  belongs_to :user

  # What a token may be allowed to do. Add your own (`notes:read`).
  SCOPES: [&str; 2] = ["read", "write"]

  # Whether these are scopes a token may have: one or more of `SCOPES`.
  def valid_scopes(scopes: &Array<String>) -> bool do
    !scopes.is_empty() and scopes.iter().all(fn s -> ApiToken.SCOPES.contains(&s.as_str()) end)
  end

  # A new token for the user, returned once: only its digest is kept. The
  # `glc_` prefix lets anyone (and secret scanners) see what it is.
  def issue(user_id: i64, name: &str, scopes: Array<String>, expires_in_days: Option<i64>) -> String do
    if !ApiToken.valid_scopes(&scopes) do
      raise Error.unprocessable("choose one or more scopes")
    end
    token = "glc_#{glacier::auth::new_token()}"
    expires_at = expires_in_days.map(fn days -> days.days().from_now() end)
    api_token = ApiToken.new(user_id: user_id, name: name, scopes: scopes.join(" "), token_digest: glacier::auth::token_digest(&token), expires_at: expires_at)
    api_token.save()!
    token
  end

  # The token, and its user, unless it's expired; marks it used, at most
  # once an hour.
  def find_by_token(token: &str) -> Option<ApiToken> do
    found = ApiToken.where(token_digest: glacier::auth::token_digest(token))
      .preload(:user)
      .first()!
      .filter(fn t -> t.expires_at.is_none_or(fn at -> at > Timestamp.now() end) end)
    if let Some(api_token) = &found do
      if api_token.last_used_at.is_none_or(fn at -> at < 1.hours().ago() end) do
        # A copy: saving reloads the record, which would drop its user.
        api_token.clone().update(last_used_at: Some(Timestamp.now()))!
      end
    end
    found
  end

  def scope_list(self) -> Array<String> do
    self.scopes.split_whitespace().map(str::to_owned).collect()
  end
end