yuki / okibi
焚き火の仲間だけの自前Gitホスティング(このサイト自身)
use rand::RngCore;
use sha2::{Digest, Sha256};
use std::time::{SystemTime, UNIX_EPOCH};
/// Current unix time in seconds.
pub fn now() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as i64
}
/// 32 random bytes as lowercase hex (64 chars).
pub fn random_token() -> String {
let mut buf = [0u8; 32];
rand::thread_rng().fill_bytes(&mut buf);
hex::encode(buf)
}
/// SHA-256 hex digest — we store only hashes of secrets (sessions, PATs, magic links).
pub fn sha256_hex(input: &str) -> String {
let mut h = Sha256::new();
h.update(input.as_bytes());
hex::encode(h.finalize())
}
/// Normalize an email for comparison (trim + lowercase).
pub fn norm_email(s: &str) -> String {
s.trim().to_lowercase()
}
/// Derive a default handle from an email local-part, kept to [a-z0-9-].
pub fn handle_from_email(email: &str) -> String {
let local = email.split('@').next().unwrap_or("user");
let mut out = String::new();
for c in local.chars() {
if c.is_ascii_alphanumeric() {
out.push(c.to_ascii_lowercase());
} else if c == '.' || c == '_' || c == '-' || c == '+' {
out.push('-');
}
}
let trimmed = out.trim_matches('-').to_string();
if trimmed.is_empty() {
"user".into()
} else {
trimmed
}
}
/// Validate a repo/handle name: lowercase alnum, dash, underscore, dot. 1..=64 chars.
pub fn valid_name(s: &str) -> bool {
!s.is_empty()
&& s.len() <= 64
&& s.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.')
&& s != "."
&& s != ".."
}