;(function() { const api = { post(url, body) { return m.request({method: "POST", url, body}) }, users: { list(page) { return api.post("/Users.List", {Page: page || {Limit: 100}}) }, }, tenants: { list(page) { return api.post("/Tenants.List", {Page: page || {Limit: 100}}) }, get(id) { return api.post("/Tenants.Get", {ID: id}) }, }, groups: { listByTenant(tenant, page) { return api.post("/Groups.ListByTenant", {Tenant: tenant, Page: page || {Limit: 100}}) }, get(tenant, path) { return api.post("/Groups.Get", {Tenant: tenant, Path: path}) }, save(group) { return api.post("/Groups.Save", {Group: group}) }, delete(tenant, path) { return api.post("/Groups.Delete", {Tenant: tenant, Path: path}) }, }, roles: { listByTenant(tenant, page) { return api.post("/Roles.ListByTenant", {Tenant: tenant, Page: page || {Limit: 100}}) }, }, grants: { listByTenant(tenant, page) { return api.post("/Grants.ListByTenant", {Tenant: tenant, Page: page || {Limit: 100}}) }, }, tenantMembers: { listByTenant(tenant, page) { return api.post("/TenantMembers.ListByTenant", {Tenant: tenant, Page: page || {Limit: 100}}) }, remove(tenant, userID) { return api.post("/TenantMembers.Remove", {Tenant: tenant, UserID: userID}) }, }, tenantInvitations: { create(tenant, email) { return api.post("/TenantInvitations.Create", {Tenant: tenant, Email: email}) }, list(tenant, page) { return api.post("/TenantInvitations.List", {Tenant: tenant, Page: page || {Limit: 100}}) }, delete(tenant, email) { return api.post("/TenantInvitations.Delete", {Tenant: tenant, Email: email}) }, }, } function navLink(path, label) { const cur = m.route.get() const active = cur === "/" + path || cur.startsWith("/" + path + "/") return m(m.route.Link, {href: "/" + path, class: active ? "active" : ""}, label) } const tenantNames = {} function getTenantName(id) { if (tenantNames[id]) return tenantNames[id] tenantNames[id] = id api.tenants.get(id).then(res => { tenantNames[id] = res.Tenant.Name; m.redraw() }) return id } const Layout = { view(vnode) { const cur = m.route.get() const match = cur.match(/^\/tenants\/([^/]+)\//) const ns = match ? match[1] : null return m(".layout", [ m("nav", [ m(".logo", "IAM Admin"), navLink("users", "Users"), navLink("tenants", "Tenants"), ns ? [ m(".nav-section", getTenantName(ns)), navLink("tenants/" + ns + "/members", "Members"), navLink("tenants/" + ns + "/invitations", "Invitations"), navLink("tenants/" + ns + "/groups", "Groups"), navLink("tenants/" + ns + "/roles", "Roles"), navLink("tenants/" + ns + "/grants", "Grants"), ] : null, ]), m("main", vnode.children), ]) } } function table(cols, rows, empty) { if (!rows || rows.length === 0) return m(".empty", empty || "No items found.") return m("table", [ m("thead", m("tr", cols.map(c => m("th", c.label)))), m("tbody", rows.map(row => m("tr", cols.map(c => m("td", {class: c.cls || ""}, c.value(row)))) )), ]) } // Users function UsersPage() { let items = [] let loading = true api.users.list().then(res => { items = res.Page.Items || []; loading = false; m.redraw() }) return { view() { return m(Layout, [ m("h1", "Users"), loading ? m(".empty", "Loading...") : table([ {label: "ID", cls: "mono", value: u => u.ID}, {label: "Email", value: u => u.Email}, {label: "Verified", value: u => u.Verified ? m("span.badge.badge-green", "Verified") : m("span.badge.badge-gray", "Unverified")}, ], items, "No users found."), ]) }, } } // Tenants list function TenantsPage() { let items = [] let loading = true api.tenants.list().then(res => { items = res.Page.Items || []; loading = false; m.redraw() }) return { view() { return m(Layout, [ m("h1", "Tenants"), loading ? m(".empty", "Loading...") : table([ {label: "ID", cls: "mono", value: t => t.ID}, {label: "Name", value: t => t.Name}, {label: "", value: t => m(m.route.Link, {href: "/tenants/" + t.ID + "/groups", class: "cap-link"}, "View")}, ], items, "No tenants found."), ]) }, } } // Tenant groups page function TenantGroupsPage(vnode) { const tenantID = vnode.attrs.ns let items = [] let loading = true api.groups.listByTenant(tenantID).then(res => { items = res.Page.Items || []; loading = false; m.redraw() }) return { view() { return m(Layout, [ m(".page-header", [ m("h1", "Groups"), m(m.route.Link, {href: "/tenants/" + tenantID + "/groups/new", class: "btn"}, "New Group"), ]), loading ? m(".empty", "Loading...") : table([ {label: "Path", cls: "mono", value: g => g.Path}, {label: "Name", value: g => g.Name}, {label: "", value: g => m(m.route.Link, {href: "/tenants/" + tenantID + "/groups/" + encodeURIComponent(g.Path) + "/edit", class: "cap-link"}, "Edit")}, ], items, "No groups found."), ]) }, } } // Group form (shared for create + edit, tenant-scoped) function TenantGroupFormPage(vnode) { const tenantID = vnode.attrs.ns let form = {Path: "", Name: ""} let saving = false let loading = false let error = null let isEdit = false const pathParam = vnode.attrs.path if (pathParam) { isEdit = true loading = true const path = decodeURIComponent(pathParam) api.groups.get(tenantID, path).then(g => { form = {Path: g.Path || "", Name: g.Name || ""} loading = false m.redraw() }).catch(() => { error = "Failed to load group"; loading = false; m.redraw() }) } const bind = key => ({value: form[key], oninput: e => form[key] = e.target.value}) const submit = e => { e.preventDefault() saving = true error = null api.groups.save({Tenant: tenantID, Path: form.Path, Name: form.Name}) .then(() => m.route.set("/tenants/" + tenantID + "/groups")) .catch(err => { error = (err.response && err.response.Message) || "An error occurred"; saving = false; m.redraw() }) } return { view() { return m(Layout, [ m("h1", isEdit ? "Edit Group" : "New Group"), loading ? m(".empty", "Loading...") : [ error && m(".error", error), m("form.form", {onsubmit: submit}, [ m(".form-field", [m("label", "Path"), m("input[type=text].mono", {required: true, placeholder: "engineering", disabled: isEdit, ...bind("Path")})]), m(".form-field", [m("label", "Name"), m("input[type=text]", {required: true, ...bind("Name")})]), m(".form-caps", [ m("button[type=submit].btn", {disabled: saving}, saving ? "Saving..." : (isEdit ? "Save Changes" : "Create Group")), m(m.route.Link, {href: "/tenants/" + tenantID + "/groups", class: "cancel-link"}, "Cancel"), ]), ]), ], ]) }, } } // Tenant roles page function TenantRolesPage(vnode) { const tenantID = vnode.attrs.ns let items = [] let loading = true api.roles.listByTenant(tenantID).then(res => { items = res.Page.Items || []; loading = false; m.redraw() }) return { view() { return m(Layout, [ m("h1", "Roles"), loading ? m(".empty", "Loading...") : table([ {label: "Slug", cls: "mono", value: r => r.Slug}, {label: "Name", value: r => r.Name}, {label: "Caps", cls: "mono", value: r => (r.Caps || []).join(", ")}, ], items, "No roles found."), ]) }, } } // Tenant grants page function TenantGrantsPage(vnode) { const tenantID = vnode.attrs.ns let items = [] let loading = true api.grants.listByTenant(tenantID).then(res => { items = res.Page.Items || []; loading = false; m.redraw() }) return { view() { return m(Layout, [ m("h1", "Grants"), loading ? m(".empty", "Loading...") : table([ {label: "Type", value: g => g.Type}, {label: "Principal", cls: "mono", value: g => g.Principal}, {label: "Role", cls: "mono", value: g => g.Role}, {label: "Path", cls: "mono", value: g => g.Path}, ], items, "No grants found."), ]) }, } } // Tenant invitations page (admin view) function TenantInvitationsPage(vnode) { const tenantID = vnode.attrs.ns let items = [] let loading = true let email = "" let inviting = false let error = null function load() { loading = true api.tenantInvitations.list(tenantID).then(res => { items = res.Page.Items || [] loading = false m.redraw() }).catch(() => { loading = false; m.redraw() }) } load() const invite = e => { e.preventDefault() inviting = true error = null api.tenantInvitations.create(tenantID, email) .then(() => { email = ""; inviting = false; load() }) .catch(err => { error = (err.response && err.response.Message) || "An error occurred"; inviting = false; m.redraw() }) } const revoke = inv => { error = null api.tenantInvitations.delete(tenantID, inv.Email) .then(() => load()) .catch(err => { error = (err.response && err.response.Message) || "An error occurred"; m.redraw() }) } return { view() { return m(Layout, [ m("h1", "Invitations"), error && m(".error", error), m("form.form.invite-form", {onsubmit: invite}, [ m("input[type=email]", {required: true, placeholder: "Email address", value: email, oninput: e => email = e.target.value}), m("button[type=submit].btn", {disabled: inviting}, inviting ? "Inviting..." : "Send Invitation"), ]), loading ? m(".empty", "Loading...") : table([ {label: "Email", value: inv => inv.Email}, {label: "Expires", value: inv => new Date(inv.ExpiresAt).toLocaleDateString()}, {label: "", value: inv => m("a.cap-link", {href: "#", onclick: e => { e.preventDefault(); revoke(inv) }}, "Revoke")}, ], items, "No pending invitations."), ]) }, } } // Tenant members page function TenantMembersPage(vnode) { const tenantID = vnode.attrs.ns let items = [] let loading = true let userID = "" let adding = false let error = null function load() { api.tenantMembers.listByTenant(tenantID).then(res => { items = res.Page.Items || []; loading = false; m.redraw() }) } load() const add = e => { e.preventDefault() adding = true error = null api.tenantMembers.save(tenantID, userID, false) .then(() => { userID = ""; adding = false; loading = true; load() }) .catch(err => { error = (err.response && err.response.Message) || "An error occurred"; adding = false; m.redraw() }) } const remove = uid => { api.tenantMembers.remove(tenantID, uid) .then(() => { loading = true; load() }) .catch(err => { error = (err.response && err.response.Message) || "An error occurred"; m.redraw() }) } return { view() { return m(Layout, [ m("h1", "Members"), error && m(".error", error), m("form.form.invite-form", {onsubmit: add}, [ m("input[type=text]", {required: true, placeholder: "User ID", value: userID, oninput: e => userID = e.target.value}), m("button[type=submit].btn", {disabled: adding}, adding ? "Adding..." : "Add Member"), ]), loading ? m(".empty", "Loading...") : table([ {label: "User ID", cls: "mono", value: mb => mb.UserID}, {label: "Owner", value: mb => mb.Owner ? m("span.badge.badge-green", "Owner") : m("span.badge.badge-gray", "Member")}, {label: "", value: mb => m("a.cap-link", {href: "#", onclick: e => { e.preventDefault(); remove(mb.UserID) }}, "Remove")}, ], items, "No members found."), ]) }, } } m.route.prefix = "/admin" m.route(document.getElementById("app"), "/tenants", { "/users": UsersPage, "/tenants": TenantsPage, "/tenants/:ns/members": TenantMembersPage, "/tenants/:ns/invitations": TenantInvitationsPage, "/tenants/:ns/groups": TenantGroupsPage, "/tenants/:ns/groups/new": TenantGroupFormPage, "/tenants/:ns/groups/:path/edit": TenantGroupFormPage, "/tenants/:ns/roles": TenantRolesPage, "/tenants/:ns/grants": TenantGrantsPage, }) })()