| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150 |
- using Invercargill;
- using Invercargill.DataStructures;
- using Spry;
- using Spry.Authentication;
- using Statum;
- using ClankerSaddle.Services;
- namespace ClankerSaddle {
- /** The logged-in user's own profile card on /profile. */
- public class ProfileView : Object {
- public string username { get; set; default = ""; }
- public string email { get; set; default = ""; }
- public string name_display { get; set; default = ""; }
- public bool is_admin { get; set; }
- public string created_display { get; set; default = ""; }
- public Statum.Model.ActionDto change_password { get; set; }
- /** The stored theme slug ("" = the Spry core default). */
- public string theme { get; set; default = ""; }
- /** The stored colour mode (auto|light|dark). */
- public string mode { get; set; default = MODE_DEFAULT; }
- public Statum.Model.ActionDto set_theme { get; set; }
- /**
- * The VAPID public key the browser subscribes with ("" when
- * push is unavailable) — goal 41 task 3: the bell preference
- * UI moved from the dashboard's opener card to this page's
- * Notifications card, riding the SAME dashboard push actions
- * (PushService untouched).
- */
- public string push_vapid_key { get; set; default = ""; }
- public Statum.Model.ActionDto? push_subscribe { get; set; }
- public Statum.Model.ActionDto? push_unsubscribe { get; set; }
- /** Queues a REAL test push to this user (the bell's probe). */
- public Statum.Model.ActionDto? push_test { get; set; }
- }
- /**
- * Entrypoint for /profile: the logged-in user's own account (name,
- * email, username, role), the change-own-password form and the
- * appearance card (goal 41 task 2 — the Spry theme picker). Open to
- * every logged-in user; the store row (not the signed login
- * snapshot) is read so a renamed account shows its current profile,
- * and the theme/mode fields mirror the stored preferences so the
- * picker opens on the choice this device will paint.
- */
- public class ProfileEntrypoint : StatumEntrypoint {
- private UserService users_service = Inversion.inject<UserService>();
- private UserPreferenceService preferences = Inversion.inject<UserPreferenceService>();
- private PushService push_service = Inversion.inject<PushService>();
- private InvercargillSql.Orm.OrmSession db = Inversion.inject<InvercargillSql.Orm.OrmSession>();
- public override async DirectiveBuilder handle() throws GLib.Error {
- var guard = yield SpryAuth.require_login(directives(), request, users_service);
- if (guard != null) {
- return (!)guard;
- }
- var me = SpryAuth.current(request);
- var user = yield users_service.get_user(me.user_id);
- if (user == null) {
- // The session slot outlives a deleted account; the guard
- // re-validation above already handled disabled ones, so
- // this only trips a mid-request delete. Bounce to login.
- return directives().notify("error", "Your account no longer exists.").navigate("/login");
- }
- var slot = yield build_profile_slot(state_service, action_registry,
- users_service, preferences, me, (!)user, push_service, db);
- return directives().set(slot);
- }
- /**
- * The page's PAGE-scope profile slot. Shared with
- * {@link SetThemeAction} so the save answers with the REFRESHED
- * card (the same trick as the dashboard actions): the client
- * re-renders every binding after a directive chain, and without
- * this the theme radios would snap back against the stale
- * pre-save values.
- */
- public static async Statum.Slot build_profile_slot(StateService state_service,
- ActionRegistry action_registry, UserService users_service,
- UserPreferenceService preferences, CurrentUser me, UserProjection user,
- PushService? push = null, InvercargillSql.Orm.OrmSession? db = null)
- throws GLib.Error {
- // The appearance card's current values: the stored
- // preferences (missing rows read as the defaults — the
- // core theme, auto mode).
- string theme = "";
- string mode = MODE_DEFAULT;
- try {
- theme = yield preferences.get_theme(me.user_id);
- mode = yield preferences.get_mode(me.user_id);
- } catch (Error e) {
- warning(@"[ClankerSaddle] could not read the appearance preferences: $(e.message)");
- }
- var view = new ProfileView() {
- username = user.username,
- email = user.email,
- name_display = @"$(user.forename) $(user.surname)".strip(),
- is_admin = SpryAuth.permission_matches(user.permissions.to_array(),
- SpryAuth.ADMIN_PERMISSION),
- created_display = format_stamp(user.created),
- change_password = action_registry.author<ChangeMyPasswordAction>(),
- theme = theme,
- mode = mode,
- set_theme = action_registry.author<SetThemeAction>()
- };
- // The Notifications card (goal 41 task 3): the same facts
- // the dashboard's bell strip derives its per-browser verdict
- // from, so /profile owns the preference UI while the
- // dashboard keeps only the compact strip. Reuses the
- // EXISTING dashboard push actions. The push service and db
- // session arrive as parameters (SetThemeAction's scope may
- // carry them; a static method cannot Inversion.inject — the
- // context stack is per-request). The VAPID key comes from
- // Spry's sender via the service ("" when unkeyed).
- view.push_vapid_key = push != null
- ? ((!)push).vapid_public_key() : "";
- view.push_subscribe = action_registry.author<PushSubscribeAction>();
- view.push_unsubscribe = action_registry.author<PushUnsubscribeAction>();
- view.push_test = action_registry.author<PushTestAction>();
- string[] fingerprints = new string[0];
- if (push != null && db != null) {
- try {
- fingerprints = yield ((!)push)
- .user_endpoint_fingerprints((!)db, me.user_id);
- } catch (Error e) {
- warning(@"[ClankerSaddle] could not read push endpoints: $(e.message)");
- }
- }
- var props = (PropertyDictionary) GObjectMapping.to_properties(view);
- // The fingerprint list is array-shaped — GObjectMapping
- // cannot serialise arrays as properties (the same
- // constraint as the dashboard's lists), so it rides the
- // props dictionary under the SAME key the bell engine
- // reads on the dashboard slot.
- props["push_endpoints"] = StateJson.strings(fingerprints);
- return state_service.new_slot(Scope.PAGE, new State() {
- type_name = "profile",
- public_data = props,
- private_data = new PropertyDictionary()
- });
- }
- }
- }
|