# 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