╭ changed ────╮ │src/event.zig│ ╰─────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │2123 return id_bytes; │ │2124 } │ │2125 │ │2126 +// change a user's email, and their password when the password fields are │ │2127 +// filled in │ │2128 +pub fn updateUser( │ │2129 + io: std.Io, │ │2130 + allocator: std.mem.Allocator, │ │2131 + users_dir: []const u8, │ │2132 + admin_repo: *rp.Repo(.xit, admin_repo_opts), │ │2133 + user_id: *const [event_id_size]u8, │ │2134 + email: []const u8, │ │2135 + current_password: []const u8, │ │2136 + new_password: []const u8, │ │2137 + new_password_again: []const u8, │ │2138 +) !void { │ │2139 + if (email.len == 0) return error.EmailEmpty; │ │2140 + var arena = std.heap.ArenaAllocator.init(allocator); │ │2141 + defer arena.deinit(); │ │2142 + const moment = try currentMoment(admin_repo_opts, admin_repo); │ │2143 + const user = (try User.readById(AdminDB, admin_repo_opts.hash, moment, &arena, user_id)) orelse return error.NotFound; │ │2144 + if (user.removed) return error.NotFound; │ │2145 + if (!std.mem.eql(u8, email, user.event.email) and try User.readByEmail(AdminDB, admin_repo_opts.hash, moment, &arena, email) != null) return error.EmailTaken;│ │2146 + │ │2147 + var event = user.event; │ │2148 + event.email = email; │ │2149 + var password_hash_buf: [User.password_hash_max_len]u8 = undefined; │ │2150 + if (current_password.len + new_password.len + new_password_again.len > 0) { │ │2151 + if (!User.verifyPassword(user.event.password_hash, current_password)) return error.WrongPassword; │ │2152 + if (new_password.len == 0) return error.PasswordEmpty; │ │2153 + if (!std.mem.eql(u8, new_password, new_password_again)) return error.PasswordMismatch; │ │2154 + event.password_hash = try User.hashPassword(new_password, &password_hash_buf, io); │ │2155 + } │ │2156 + │ │2157 + try consume(.{ .server = .{ .users_dir = users_dir } }, .admin, .xit, admin_repo_opts, io, allocator, admin_repo, events_ref, &[_]EventWithId{.{ │ │2158 + .id = std.fmt.bytesToHex(user_id.*, .lower), │ │2159 + .timestamp = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()), │ │2160 + .author = .{ .name = event.name, .email = email }, │ │2161 + .event = .{ .user = event }, │ │2162 + }}); │ │2163 +} │ │2164 + │ │2165 // consume a new repo's event into its owner's user repo and mark it active │ │2166 fn writeRepoEvent( │ │2167 io: std.Io, │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─────────╮ │src/event/User.zig│ ╰──────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │159 const user_map = try DB.HashMap(.read_only).init(user_cursor); │ │160 const user_event = try evt.read(Record, DB, hash_kind, arena, user_map); │ │161 │ │162 - bcrypt.strVerify(user_event.event.password_hash, password, .{ .silently_truncate_password = false }) catch {│ │163 - return .wrong_password; │ │164 - }; │ │162 + if (!verifyPassword(user_event.event.password_hash, password)) return .wrong_password; │ │163 │ │164 return .{ .success = user_id }; │ │165 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │166 │ │167 +pub fn verifyPassword(password_hash: []const u8, password: []const u8) bool { │ │168 + bcrypt.strVerify(password_hash, password, .{ .silently_truncate_password = false }) catch return false;│ │169 + return true; │ │170 +} │ │171 + │ │172 // read a user by event id via the event-id->user index, or null if the id │ │173 // isn't a known user. field byte slices are allocated in `arena`. │ │174 pub fn readById( │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭──────────────────────────────────────────────────────────────╮ │31 pub const NotFound = @import("./ui/NotFound.zig"); │ │32 pub const NewRepo = @import("./ui/NewRepo.zig"); │ │33 pub const NewUser = @import("./ui/NewUser.zig"); │ │34 -pub const UserLogout = @import("./ui/UserLogout.zig"); │ │34 +pub const UserSettings = @import("./ui/UserSettings.zig");│ │35 pub const UserLogin = @import("./ui/UserLogin.zig"); │ │36 pub const widget = @import("./ui/widget.zig"); │ │37 pub const Widget = widget.Widget; │ ╰──────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────╮ │2219 email_taken, │ │2220 required_password, │ │2221 password_mismatch, │ │2222 + wrong_password, │ │2223 │ │2224 pub fn fromError(err: anyerror) ?UserFailure {│ │2225 return switch (err) { │ ╰────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────╮ │2230 error.EmailTaken => .email_taken, │ │2231 error.PasswordEmpty => .required_password, │ │2232 error.PasswordMismatch => .password_mismatch,│ │2233 + error.WrongPassword => .wrong_password, │ │2234 else => null, │ │2235 }; │ │2236 } │ ╰───────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────╮ │2247 name: []const u8, │ │2248 email: []const u8, │ │2249 }, │ │2250 + }, │ │2251 + user_settings: struct { │ │2252 + failure: UserFailure, │ │2253 + // passwords are never retained │ │2254 + fields: struct { email: []const u8 },│ │2255 }, │ │2256 repo: struct { │ │2257 failure: RepoFailure, │ ╰───────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────╮ │2307 // serializable data sent down to web client │ │2308 pub const Data = struct { │ │2309 user_id: ?[]const u8 = null, │ │2303 - // the logged-in user's name, for the views that show it │ │2310 + // the logged-in user's name and email, for the views that show them│ │2311 user_name: ?[]const u8 = null, │ │2312 + user_email: ?[]const u8 = null, │ │2313 // a failed form retained long enough to render it for correction │ │2314 form_feedback: ?FormFeedback = null, │ │2315 // a transient local event-sync error │ ╰──────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────╮ │2357 const moment = self.haxy_moment orelse return; │ │2358 const user = (try activeUser(moment, self.arena, user_id)) orelse return self.logOut();│ │2359 self.data.user_name = user.event.name; │ │2360 + self.data.user_email = user.event.email; │ │2361 } │ │2362 │ │2363 // forget the logged-in user, on a logout or once their account is removed │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────╮ │2364 pub fn logOut(self: *Self) void { │ │2365 self.data.user_id = null; │ │2366 self.data.user_name = null; │ │2367 + self.data.user_email = null; │ │2368 } │ │2369 │ │2370 pub fn userId(self: *const Self) ?[evt.event_id_size]u8 {│ ╰───────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯