diff options
author | Mike Crute <mike@crute.us> | 2021-11-24 12:12:03 -0800 |
---|---|---|
committer | Mike Crute <mike@crute.us> | 2021-11-24 12:15:09 -0800 |
commit | ebce860860eb2163bcd0614fc7e82c3f3a259cf3 (patch) | |
tree | e07ed8b290c37abf0134ed5ec8f5b6d5fce65d8f | |
parent | 09fa11a1dad5301e8e0aeb069fc1d11312b4a1c5 (diff) | |
download | cloud-identity-broker-admin-pages.tar.bz2 cloud-identity-broker-admin-pages.tar.xz cloud-identity-broker-admin-pages.zip |
WIPadmin-pages
-rw-r--r-- | app/controllers/basic.go | 8 | ||||
-rw-r--r-- | cmd/web/server.go | 4 | ||||
-rw-r--r-- | templates/account.tpl | 58 | ||||
-rw-r--r-- | templates/assets/site.css | 33 | ||||
-rw-r--r-- | templates/assets/site.js | 186 | ||||
-rw-r--r-- | templates/index.tpl | 18 | ||||
-rw-r--r-- | templates/user.tpl | 38 |
7 files changed, 345 insertions, 0 deletions
diff --git a/app/controllers/basic.go b/app/controllers/basic.go index eff97e1..5363640 100644 --- a/app/controllers/basic.go +++ b/app/controllers/basic.go | |||
@@ -11,6 +11,14 @@ func IndexHandler(c echo.Context) error { | |||
11 | return c.Render(http.StatusOK, "index.tpl", nil) | 11 | return c.Render(http.StatusOK, "index.tpl", nil) |
12 | } | 12 | } |
13 | 13 | ||
14 | func AccountFormHandler(c echo.Context) error { | ||
15 | return c.Render(http.StatusOK, "account.tpl", nil) | ||
16 | } | ||
17 | |||
18 | func UserFormHandler(c echo.Context) error { | ||
19 | return c.Render(http.StatusOK, "user.tpl", nil) | ||
20 | } | ||
21 | |||
14 | func LogoutHandler(c echo.Context) error { | 22 | func LogoutHandler(c echo.Context) error { |
15 | glecho.DeleteAllCookies(c) | 23 | glecho.DeleteAllCookies(c) |
16 | return c.Redirect(http.StatusFound, "/") | 24 | return c.Redirect(http.StatusFound, "/") |
diff --git a/cmd/web/server.go b/cmd/web/server.go index 9f750bc..282bf39 100644 --- a/cmd/web/server.go +++ b/cmd/web/server.go | |||
@@ -174,6 +174,10 @@ func webMain(cfg app.Config, embeddedTemplates fs.FS, version string) { | |||
174 | s.GET("/favicon.ico", echo.NotFoundHandler) | 174 | s.GET("/favicon.ico", echo.NotFoundHandler) |
175 | s.GET("/logout", controllers.LogoutHandler) | 175 | s.GET("/logout", controllers.LogoutHandler) |
176 | s.CachedStaticRoute("/assets", "assets") | 176 | s.CachedStaticRoute("/assets", "assets") |
177 | s.GET("/account", controllers.AccountFormHandler, am.Middleware) | ||
178 | s.GET("/account/*", controllers.AccountFormHandler, am.Middleware) | ||
179 | s.GET("/user", controllers.UserFormHandler, am.Middleware) | ||
180 | s.GET("/user/*", controllers.UserFormHandler, am.Middleware) | ||
177 | s.GET("/", controllers.IndexHandler, am.Middleware) | 181 | s.GET("/", controllers.IndexHandler, am.Middleware) |
178 | 182 | ||
179 | runner := service.NewAppRunner(ctx, s.Logger) | 183 | runner := service.NewAppRunner(ctx, s.Logger) |
diff --git a/templates/account.tpl b/templates/account.tpl new file mode 100644 index 0000000..1f3bbfa --- /dev/null +++ b/templates/account.tpl | |||
@@ -0,0 +1,58 @@ | |||
1 | <!DOCTYPE html> | ||
2 | <html lang="en"> | ||
3 | <head> | ||
4 | <title>Manage Account</title> | ||
5 | <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> | ||
6 | <link rel="stylesheet" type="text/css" href="/assets/site.css" /> | ||
7 | <script type="text/javascript" src="/assets/site.js"></script> | ||
8 | </head> | ||
9 | <body> | ||
10 | <div id="api-error"><p>Error communicating with the API.</p><p class="sub-message"></p></div> | ||
11 | <div id="api-success">Saved!</div> | ||
12 | |||
13 | <h1>Manage Account</h1> | ||
14 | <form method="PUT" action="/api/account" id="account-form"> | ||
15 | <ol> | ||
16 | <li> | ||
17 | <label for="short_name">Short Name:</label> | ||
18 | <input type="text" id="short_name" name="short_name" /> | ||
19 | </li> | ||
20 | <li> | ||
21 | <label for="account_type">Account Type:</label> | ||
22 | <input type="text" id="account_type" name="account_type" /> | ||
23 | </li> | ||
24 | <li> | ||
25 | <label>Account Number:</label> | ||
26 | <input type="number" id="account_number" name="account_number" /> | ||
27 | </li> | ||
28 | <li> | ||
29 | <label for="name">Name:</label> | ||
30 | <input type="text" id="name" name="name" /> | ||
31 | </li> | ||
32 | <li> | ||
33 | <label for="console_session_duration">Console Session Duration (ms):</label> | ||
34 | <input type="number" id="console_session_duration" name="console_session_duration" value="21600000000000" /> | ||
35 | </li> | ||
36 | <li> | ||
37 | <label for="vault_material">Vault Material:</label> | ||
38 | <input type="text" id="vault_material" name="vault_material" /> | ||
39 | </li> | ||
40 | <li> | ||
41 | <label for="default_region">Default Region:</label> | ||
42 | <input type="text" id="default_region" name="default_region" /> | ||
43 | </li> | ||
44 | <li> | ||
45 | <label for="users">Users:</label> | ||
46 | <select id="users" name="users" multiple></select> | ||
47 | </li> | ||
48 | <li class="submit"> | ||
49 | <button id="back">Back</button> | ||
50 | <button id="submit-form">Save</button> | ||
51 | </li> | ||
52 | </ol> | ||
53 | </form> | ||
54 | <script type="text/javascript"> | ||
55 | window.addEventListener('load', setupManageAccount); | ||
56 | </script> | ||
57 | </body> | ||
58 | </html> | ||
diff --git a/templates/assets/site.css b/templates/assets/site.css index df1bab8..93503ab 100644 --- a/templates/assets/site.css +++ b/templates/assets/site.css | |||
@@ -82,6 +82,39 @@ tt { | |||
82 | body.isAdmin .admin { | 82 | body.isAdmin .admin { |
83 | display: initial; | 83 | display: initial; |
84 | } | 84 | } |
85 | form ol { | ||
86 | list-style: none; | ||
87 | padding: 0; | ||
88 | margin: 0; | ||
89 | } | ||
90 | form ol li { | ||
91 | display: flex; | ||
92 | margin: 0.5em 0; | ||
93 | } | ||
94 | form ol li * { | ||
95 | flex: 2; | ||
96 | } | ||
97 | form ol li label { | ||
98 | flex: 0.5; | ||
99 | font-weight: bolder; | ||
100 | } | ||
101 | input[type="number"] { | ||
102 | appearance: textfield; | ||
103 | } | ||
104 | form ol li button { | ||
105 | flex: 0; | ||
106 | padding: 0.5em 2em; | ||
107 | margin: 0 0.5em; | ||
108 | } | ||
109 | form ol li.submit { | ||
110 | justify-content: right; | ||
111 | } | ||
112 | .user-is-admin { | ||
113 | background-color: red; | ||
114 | } | ||
115 | .user-is-service { | ||
116 | background-color: green; | ||
117 | } | ||
85 | .tag { | 118 | .tag { |
86 | display: block; | 119 | display: block; |
87 | float: right; | 120 | float: right; |
diff --git a/templates/assets/site.js b/templates/assets/site.js index 7ba4b15..66fd43f 100644 --- a/templates/assets/site.js +++ b/templates/assets/site.js | |||
@@ -59,6 +59,25 @@ function accountTableLinkClick(event) { | |||
59 | return false; | 59 | return false; |
60 | } | 60 | } |
61 | 61 | ||
62 | function populateUserRow(row) { | ||
63 | var [key, value] = row; | ||
64 | var tokens = ""; | ||
65 | |||
66 | if (value.is_admin) { | ||
67 | tokens += '<span class="tag user-is-admin">Admin</span>'; | ||
68 | } | ||
69 | |||
70 | if (value.is_service) { | ||
71 | tokens += '<span class="tag user-is-service">Service</span>'; | ||
72 | } | ||
73 | |||
74 | var out = fillTemplate("user_row_template", { | ||
75 | "username": key, | ||
76 | "tokens": tokens, | ||
77 | }); | ||
78 | document.querySelector("#user-table tr").insertAdjacentHTML("afterend", out); | ||
79 | } | ||
80 | |||
62 | function populateAccountRow(row) { | 81 | function populateAccountRow(row) { |
63 | document.querySelector("#account-table tr").insertAdjacentHTML("afterend", | 82 | document.querySelector("#account-table tr").insertAdjacentHTML("afterend", |
64 | fillTemplate(row["vendor"] + "_account_row_template", row)); | 83 | fillTemplate(row["vendor"] + "_account_row_template", row)); |
@@ -84,13 +103,180 @@ function isAdmin() { | |||
84 | return parseJWT()["admin"]; | 103 | return parseJWT()["admin"]; |
85 | } | 104 | } |
86 | 105 | ||
106 | function getApiUrl(formId) { | ||
107 | var urlParts = window.location.pathname.split("/"); | ||
108 | if (urlParts.length != 3) { | ||
109 | return null; | ||
110 | } | ||
111 | var shortName = urlParts[urlParts.length-1]; | ||
112 | return document.getElementById(formId).action + "/" + shortName; | ||
113 | } | ||
114 | |||
115 | function submitForm(target, url, method) { | ||
116 | var result = {}; | ||
117 | |||
118 | target.querySelectorAll("form input, form select").forEach(e => { | ||
119 | if (e.selectedOptions !== undefined) { | ||
120 | var items = []; | ||
121 | Array.from(e.selectedOptions).forEach(i => items.push(i.value)); | ||
122 | result[e.id] = items; | ||
123 | } else if (e.type == "number") { | ||
124 | result[e.id] = parseInt(e.value); | ||
125 | } else { | ||
126 | result[e.id] = e.value; | ||
127 | } | ||
128 | }); | ||
129 | |||
130 | fetch(url, { | ||
131 | "method": method, | ||
132 | "headers": { "Content-Type": "application/vnd.broker.v2+json" }, | ||
133 | "body": JSON.stringify(result) | ||
134 | }) | ||
135 | .then(r => { | ||
136 | if (r.status < 299) { | ||
137 | return { "request": r }; | ||
138 | } else { | ||
139 | return r.json().then(j => ({ "json": j, "request": r })) | ||
140 | } | ||
141 | }) | ||
142 | .then(r => { | ||
143 | if (r.request.status > 299) { | ||
144 | document.getElementById("api-error").style.display = "block"; | ||
145 | document.querySelector("#api-error .sub-message").innerText = r.json.message; | ||
146 | } else { | ||
147 | document.getElementById("api-success").style.display = "block"; | ||
148 | } | ||
149 | }) | ||
150 | .catch(_ => document.getElementById("api-error").style.display = "block"); | ||
151 | } | ||
152 | |||
153 | function populateUsers(selected) { | ||
154 | fetch("/api/user") | ||
155 | .then(r => { | ||
156 | if (r.status == 404) { | ||
157 | return {}; | ||
158 | } | ||
159 | // If they can't load the user endpoint they will not be able to submt updates | ||
160 | document.getElementById("submit-form").disabled = false; | ||
161 | return r.json() | ||
162 | }) | ||
163 | .then(r => { | ||
164 | var e = document.getElementById("users"); | ||
165 | |||
166 | Object.entries(r).forEach(([key, value]) => { | ||
167 | if (!selected[key]) { | ||
168 | var opt = document.createElement("option"); | ||
169 | opt.value = key; | ||
170 | opt.innerText = key; | ||
171 | e.appendChild(opt); | ||
172 | } | ||
173 | }); | ||
174 | }) | ||
175 | .catch(_ => document.getElementById("api-error").style.display = "block"); | ||
176 | } | ||
177 | |||
178 | function setupUserPage() { | ||
179 | document.getElementById("back").addEventListener('click', _ => window.location.pathname = "/"); | ||
180 | document.getElementById("user-form").addEventListener('submit', e => e.preventDefault()); | ||
181 | |||
182 | var apiUrl = getApiUrl("user-form"); | ||
183 | if (apiUrl == null) { | ||
184 | document.getElementById("submit-form").addEventListener('click', e => { | ||
185 | var target = document.getElementById("user-form"); | ||
186 | submitForm(target, target.action, "POST"); | ||
187 | e.preventDefault(); | ||
188 | return false; | ||
189 | }); | ||
190 | } else { | ||
191 | document.getElementById("submit-form").addEventListener('click', e => { | ||
192 | var target = document.getElementById("user-form"); | ||
193 | submitForm(target, getApiUrl(target.id), "PUT"); | ||
194 | e.preventDefault(); | ||
195 | return false; | ||
196 | }); | ||
197 | |||
198 | fetch(apiUrl) | ||
199 | .then(r => r.json()) | ||
200 | .then(r => { | ||
201 | Object.entries(r).forEach(([key, value]) => { | ||
202 | var e = document.getElementById(key); | ||
203 | if (e === null) { | ||
204 | return; | ||
205 | } else if (e.type == "text") { | ||
206 | e.value = value; | ||
207 | } else if (e.type == "checkbox") { | ||
208 | e.checked = value; | ||
209 | } | ||
210 | }); | ||
211 | }) | ||
212 | .catch(_ => document.getElementById("api-error").style.display = "block"); | ||
213 | } | ||
214 | } | ||
215 | |||
216 | function setupManageAccount() { | ||
217 | document.getElementById("submit-form").disabled = true; | ||
218 | document.getElementById("back").addEventListener('click', _ => window.location.pathname = "/"); | ||
219 | document.getElementById("account-form").addEventListener('submit', e => e.preventDefault()); | ||
220 | |||
221 | var apiUrl = getApiUrl("account-form"); | ||
222 | if (apiUrl == null) { | ||
223 | populateUsers({}); | ||
224 | document.getElementById("submit-form").addEventListener('click', e => { | ||
225 | var target = document.getElementById("account-form"); | ||
226 | submitForm(target, target.action, "POST"); | ||
227 | e.preventDefault(); | ||
228 | return false; | ||
229 | }); | ||
230 | } else { | ||
231 | document.getElementById("submit-form").addEventListener('click', e => { | ||
232 | var target = document.getElementById("account-form"); | ||
233 | submitForm(target, getApiUrl(target.id), "PUT"); | ||
234 | e.preventDefault(); | ||
235 | return false; | ||
236 | }); | ||
237 | |||
238 | fetch(apiUrl) | ||
239 | .then(r => r.json()) | ||
240 | .then(r => { | ||
241 | var selectedUsers = {}; | ||
242 | |||
243 | Object.entries(r).forEach(([key, value]) => { | ||
244 | var e = document.getElementById(key); | ||
245 | if (e.type !== "select") { | ||
246 | e.value = value; | ||
247 | } | ||
248 | |||
249 | if (key == "users") { | ||
250 | value.forEach(v => { | ||
251 | var opt = document.createElement("option"); | ||
252 | opt.value = v; | ||
253 | opt.selected = true; | ||
254 | opt.innerText = v; | ||
255 | e.appendChild(opt); | ||
256 | selectedUsers[v] = true; | ||
257 | }); | ||
258 | } | ||
259 | }); | ||
260 | |||
261 | return selectedUsers; | ||
262 | }) | ||
263 | .then(populateUsers) | ||
264 | .catch(_ => document.getElementById("api-error").style.display = "block"); | ||
265 | } | ||
266 | } | ||
267 | |||
87 | function setupHomePage() { | 268 | function setupHomePage() { |
269 | document.getElementById("add-account").addEventListener('click', _ => window.location.pathname = "/account"); | ||
270 | document.getElementById("add-user").addEventListener('click', _ => window.location.pathname = "/user"); | ||
271 | |||
88 | fetch("/api/account").then(getJSON).then(r => r.forEach(populateAccountRow)); | 272 | fetch("/api/account").then(getJSON).then(r => r.forEach(populateAccountRow)); |
89 | 273 | ||
90 | document.getElementById("username").innerText = parseJWT()["sub"]; | 274 | document.getElementById("username").innerText = parseJWT()["sub"]; |
91 | 275 | ||
92 | if (isAdmin()) { | 276 | if (isAdmin()) { |
93 | document.body.classList.add("isAdmin"); | 277 | document.body.classList.add("isAdmin"); |
278 | |||
279 | fetch("/api/user").then(getJSON).then(r => Object.entries(r).forEach(populateUserRow)); | ||
94 | } | 280 | } |
95 | 281 | ||
96 | document.getElementById("show-api-key").addEventListener("click", _ => { | 282 | document.getElementById("show-api-key").addEventListener("click", _ => { |
diff --git a/templates/index.tpl b/templates/index.tpl index 19d533e..2d7d22b 100644 --- a/templates/index.tpl +++ b/templates/index.tpl | |||
@@ -20,6 +20,15 @@ | |||
20 | <a data-content-type="application/vnd.broker.v2.credential.aws.ini" href="#/cli/[[ .short_name ]]">AWS CLI</a> | | 20 | <a data-content-type="application/vnd.broker.v2.credential.aws.ini" href="#/cli/[[ .short_name ]]">AWS CLI</a> | |
21 | <a data-content-type="application/vnd.broker.v2.credential.aws.sh" href="#/sh/[[ .short_name ]]">Bash</a> | | 21 | <a data-content-type="application/vnd.broker.v2.credential.aws.sh" href="#/sh/[[ .short_name ]]">Bash</a> | |
22 | <a data-content-type="application/vnd.broker.v2.credential.aws.psl" href="#/ps/[[ .short_name ]]">Powershell</a> | 22 | <a data-content-type="application/vnd.broker.v2.credential.aws.psl" href="#/ps/[[ .short_name ]]">Powershell</a> |
23 | <span class="admin">| <a href="/account/[[ .short_name ]]">Edit</a></span> | ||
24 | </td> | ||
25 | </tr> | ||
26 | </script> | ||
27 | <script id="user_row_template" type="text/template"> | ||
28 | <tr> | ||
29 | <td> | ||
30 | <a href="/user/[[ .username ]]">[[ .username ]]</a> | ||
31 | [[ .tokens ]] | ||
23 | </td> | 32 | </td> |
24 | </tr> | 33 | </tr> |
25 | </script> | 34 | </script> |
@@ -42,6 +51,15 @@ | |||
42 | <th>Credentials</th> | 51 | <th>Credentials</th> |
43 | </tr> | 52 | </tr> |
44 | </table> | 53 | </table> |
54 | <p><button class="admin" id="add-account">Add Account</button></p> | ||
55 | |||
56 | <h1>User Accounts</h1> | ||
57 | <table id="user-table"> | ||
58 | <tr> | ||
59 | <th>Username</th> | ||
60 | </tr> | ||
61 | </table> | ||
62 | <p><button class="admin" id="add-user">Add User</button></p> | ||
45 | 63 | ||
46 | <div id="api-key-block"> | 64 | <div id="api-key-block"> |
47 | <h1>API Key</h1> | 65 | <h1>API Key</h1> |
diff --git a/templates/user.tpl b/templates/user.tpl new file mode 100644 index 0000000..cae7947 --- /dev/null +++ b/templates/user.tpl | |||
@@ -0,0 +1,38 @@ | |||
1 | <!DOCTYPE html> | ||
2 | <html lang="en"> | ||
3 | <head> | ||
4 | <title>Manage User</title> | ||
5 | <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> | ||
6 | <link rel="stylesheet" type="text/css" href="/assets/site.css" /> | ||
7 | <script type="text/javascript" src="/assets/site.js"></script> | ||
8 | </head> | ||
9 | <body> | ||
10 | <div id="api-error"><p>Error communicating with the API.</p><p class="sub-message"></p></div> | ||
11 | <div id="api-success">Saved!</div> | ||
12 | |||
13 | <h1>Manage User</h1> | ||
14 | <form method="PUT" action="/api/user" id="user-form"> | ||
15 | <ol> | ||
16 | <li> | ||
17 | <label for="username">Username:</label> | ||
18 | <input type="text" id="username" name="username" /> | ||
19 | </li> | ||
20 | <li> | ||
21 | <label for="is_admin">Admin:</label> | ||
22 | <input type="checkbox" id="is_admin" name="is_admin" /> | ||
23 | </li> | ||
24 | <li> | ||
25 | <label for="is_service">Service User:</label> | ||
26 | <input type="checkbox" id="is_service" name="is_service" /> | ||
27 | </li> | ||
28 | <li class="submit"> | ||
29 | <button id="back">Back</button> | ||
30 | <button id="submit-form">Save</button> | ||
31 | </li> | ||
32 | </ol> | ||
33 | </form> | ||
34 | <script type="text/javascript"> | ||
35 | window.addEventListener('load', setupUserPage); | ||
36 | </script> | ||
37 | </body> | ||
38 | </html> | ||