fetch_ml/cli/src/utils/history.zig
Jeremie Fraeys ea15af1833 Fix multi-user authentication and clean up debug code
- Fix YAML tags in auth config struct (json -> yaml)
- Update CLI configs to use pre-hashed API keys
- Remove double hashing in WebSocket client
- Fix port mapping (9102 -> 9103) in CLI commands
- Update permission keys to use jobs:read, jobs:create, etc.
- Clean up all debug logging from CLI and server
- All user roles now authenticate correctly:
  * Admin: Can queue jobs and see all jobs
  * Researcher: Can queue jobs and see own jobs
  * Analyst: Can see status (read-only access)

Multi-user authentication is now fully functional.
2025-12-06 12:35:32 -05:00

101 lines
3 KiB
Zig

const std = @import("std");
pub const Entry = struct {
job_name: []const u8,
commit_id: []const u8,
queued_at: i64,
};
fn historyDir(allocator: std.mem.Allocator) ![]const u8 {
const home = std.posix.getenv("HOME") orelse return error.NoHomeDir;
return std.fmt.allocPrint(allocator, "{s}/.ml", .{home});
}
fn historyPath(allocator: std.mem.Allocator) ![]const u8 {
const dir = try historyDir(allocator);
defer allocator.free(dir);
return std.fmt.allocPrint(allocator, "{s}/history.log", .{dir});
}
pub fn record(allocator: std.mem.Allocator, job_name: []const u8, commit_id: []const u8) !void {
const dir = try historyDir(allocator);
defer allocator.free(dir);
std.fs.makeDirAbsolute(dir) catch |err| {
if (err != error.PathAlreadyExists) return err;
};
const path = try historyPath(allocator);
defer allocator.free(path);
var file = std.fs.openFileAbsolute(path, .{ .mode = .read_write }) catch |err| switch (err) {
error.FileNotFound => try std.fs.createFileAbsolute(path, .{}),
else => return err,
};
defer file.close();
// Append at end of file
try file.seekFromEnd(0);
const ts = std.time.timestamp();
// Format one line into a temporary buffer
const line = try std.fmt.allocPrint(
allocator,
"{d}\t{s}\t{s}\n",
.{ ts, job_name, commit_id },
);
defer allocator.free(line);
try file.writeAll(line);
}
pub fn loadEntries(allocator: std.mem.Allocator) ![]Entry {
const path = historyPath(allocator) catch |err| switch (err) {
error.NoHomeDir => return error.NoHomeDir,
else => return err,
};
defer allocator.free(path);
const file = std.fs.openFileAbsolute(path, .{}) catch |err| switch (err) {
error.FileNotFound => return &.{},
else => return err,
};
defer file.close();
const contents = try file.readToEndAlloc(allocator, 1024 * 1024);
defer allocator.free(contents);
var entries = std.ArrayListUnmanaged(Entry){};
defer entries.deinit(allocator);
var it = std.mem.splitScalar(u8, contents, '\n');
while (it.next()) |line_full| {
const line = std.mem.trim(u8, line_full, " \t\r");
if (line.len == 0) continue;
var parts = std.mem.splitScalar(u8, line, '\t');
const ts_str = parts.next() orelse continue;
const job = parts.next() orelse continue;
const commit = parts.next() orelse continue;
const ts = std.fmt.parseInt(i64, ts_str, 10) catch continue;
const job_dup = try allocator.dupe(u8, job);
const commit_dup = try allocator.dupe(u8, commit);
try entries.append(allocator, Entry{
.job_name = job_dup,
.commit_id = commit_dup,
.queued_at = ts,
});
}
return try entries.toOwnedSlice(allocator);
}
pub fn freeEntries(allocator: std.mem.Allocator, entries: []Entry) void {
for (entries) |entry| {
allocator.free(entry.job_name);
allocator.free(entry.commit_id);
}
allocator.free(entries);
}