Added basic web interface for API keys
Web interfaces for manging API keys have been added however translation keys are still required. Signed-off-by: Matthew Nickson <mnickson@sidingsmedia.com>
This commit is contained in:
		
							parent
							
								
									0d6a8b2101
								
							
						
					
					
						commit
						ee2eb5109b
					
				
					 12 changed files with 759 additions and 3 deletions
				
			
		|  | @ -10,4 +10,4 @@ CREATE TABLE [api_key] ( | |||
|     [expires] DATETIME DEFAULT NULL, | ||||
|     CONSTRAINT FK_user FOREIGN KEY ([user_id]) REFERENCES [user]([id]) ON DELETE CASCADE ON UPDATE CASCADE | ||||
| ); | ||||
| COMMIT; | ||||
| COMMIT; | ||||
|  |  | |||
|  | @ -113,6 +113,31 @@ async function sendProxyList(socket) { | |||
|     return list; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Emit API key list to client | ||||
|  * @param {Socket} socket Socket.io socket instance | ||||
|  * @returns {Promise<void>} | ||||
|  */ | ||||
| async function sendAPIKeyList(socket) { | ||||
|     const timeLogger = new TimeLogger(); | ||||
| 
 | ||||
|     let result = []; | ||||
|     const list = await R.find( | ||||
|         "api_key", | ||||
|         "user_id=?", | ||||
|         [ socket.userID ], | ||||
|     ); | ||||
| 
 | ||||
|     for (let bean of list) { | ||||
|         result.push(bean.toPublicJSON()); | ||||
|     } | ||||
| 
 | ||||
|     io.to(socket.userID).emit("apiKeyList", result); | ||||
|     timeLogger.print("Sent API Key List"); | ||||
| 
 | ||||
|     return list; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Emits the version information to the client. | ||||
|  * @param {Socket} socket Socket.io socket instance | ||||
|  | @ -157,6 +182,7 @@ module.exports = { | |||
|     sendImportantHeartbeatList, | ||||
|     sendHeartbeatList, | ||||
|     sendProxyList, | ||||
|     sendAPIKeyList, | ||||
|     sendInfo, | ||||
|     sendDockerHostList | ||||
| }; | ||||
|  |  | |||
							
								
								
									
										75
									
								
								server/model/api_key.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								server/model/api_key.js
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,75 @@ | |||
| const { BeanModel } = require("redbean-node/dist/bean-model"); | ||||
| const { R } = require("redbean-node"); | ||||
| 
 | ||||
| class APIKey extends BeanModel { | ||||
|     /** | ||||
|      * Get the current status of this API key | ||||
|      */ | ||||
|     getStatus() { | ||||
|         let expired = false; | ||||
|         if (expired) { | ||||
|             return "expired"; | ||||
|         } else if (this.active) { | ||||
|             return "active"; | ||||
|         } else if (!this.active) { | ||||
|             return "inactive"; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns an object that ready to parse to JSON | ||||
|      * @returns {Object} | ||||
|      */ | ||||
|     toJSON() { | ||||
|         return { | ||||
|             id: this.id, | ||||
|             key: this.key, | ||||
|             name: this.name, | ||||
|             userID: this.user_id, | ||||
|             createdDate: this.created_date, | ||||
|             active: this.active, | ||||
|             expires: this.expires, | ||||
|             status: this.getStatus(), | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns an object that ready to parse to JSON with sensitive fields | ||||
|      * removed | ||||
|      * @returns {Object} | ||||
|      */ | ||||
|     toPublicJSON() { | ||||
|         return { | ||||
|             id: this.id, | ||||
|             name: this.name, | ||||
|             userID: this.user_id, | ||||
|             createdDate: this.created_date, | ||||
|             active: this.active, | ||||
|             expires: this.expires, | ||||
|             status: this.getStatus(), | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Create a new API Key and store it in the database | ||||
|      * @param {Object} key Object sent by client | ||||
|      * @param {int} userID ID of socket user | ||||
|      * @returns {Promise<bean>} | ||||
|      */ | ||||
|     static async save(key, userID) { | ||||
|         let bean; | ||||
|         bean = R.dispense("api_key"); | ||||
| 
 | ||||
|         bean.key = key.key; | ||||
|         bean.name = key.name; | ||||
|         bean.user_id = userID; | ||||
|         bean.active = key.active; | ||||
|         bean.expires = key.expires; | ||||
| 
 | ||||
|         await R.store(bean); | ||||
| 
 | ||||
|         return bean; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| module.exports = APIKey; | ||||
|  | @ -126,7 +126,7 @@ if (config.demoMode) { | |||
| } | ||||
| 
 | ||||
| // Must be after io instantiation
 | ||||
| const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList, sendInfo, sendProxyList, sendDockerHostList } = require("./client"); | ||||
| const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList, sendInfo, sendProxyList, sendDockerHostList, sendAPIKeyList } = require("./client"); | ||||
| const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler"); | ||||
| const databaseSocketHandler = require("./socket-handlers/database-socket-handler"); | ||||
| const TwoFA = require("./2fa"); | ||||
|  | @ -135,6 +135,7 @@ const { cloudflaredSocketHandler, autoStart: cloudflaredAutoStart, stop: cloudfl | |||
| const { proxySocketHandler } = require("./socket-handlers/proxy-socket-handler"); | ||||
| const { dockerSocketHandler } = require("./socket-handlers/docker-socket-handler"); | ||||
| const { maintenanceSocketHandler } = require("./socket-handlers/maintenance-socket-handler"); | ||||
| const { apiKeySocketHandler } = require("./socket-handlers/api-key-socket-handler"); | ||||
| const { generalSocketHandler } = require("./socket-handlers/general-socket-handler"); | ||||
| const { Settings } = require("./settings"); | ||||
| const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent"); | ||||
|  | @ -1490,6 +1491,7 @@ let needSetup = false; | |||
|         proxySocketHandler(socket); | ||||
|         dockerSocketHandler(socket); | ||||
|         maintenanceSocketHandler(socket); | ||||
|         apiKeySocketHandler(socket); | ||||
|         generalSocketHandler(socket, server); | ||||
| 
 | ||||
|         log.debug("server", "added all socket handlers"); | ||||
|  | @ -1597,6 +1599,7 @@ async function afterLogin(socket, user) { | |||
|     sendNotificationList(socket); | ||||
|     sendProxyList(socket); | ||||
|     sendDockerHostList(socket); | ||||
|     sendAPIKeyList(socket); | ||||
| 
 | ||||
|     await sleep(500); | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										144
									
								
								server/socket-handlers/api-key-socket-handler.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								server/socket-handlers/api-key-socket-handler.js
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,144 @@ | |||
| const { checkLogin } = require("../util-server"); | ||||
| const { log } = require("../../src/util"); | ||||
| const { R } = require("redbean-node"); | ||||
| const crypto = require("crypto"); | ||||
| const passwordHash = require("../password-hash"); | ||||
| const apicache = require("../modules/apicache"); | ||||
| const APIKey = require("../model/api_key"); | ||||
| const { sendAPIKeyList } = require("../client"); | ||||
| 
 | ||||
| /** | ||||
|  * Handlers for Maintenance | ||||
|  * @param {Socket} socket Socket.io instance | ||||
|  */ | ||||
| module.exports.apiKeySocketHandler = (socket) => { | ||||
|     // Add a new api key
 | ||||
|     socket.on("addAPIKey", async (key, callback) => { | ||||
|         try { | ||||
|             checkLogin(socket); | ||||
|             let clearKey = crypto.randomUUID(); | ||||
|             let hashedKey = passwordHash.generate(clearKey); | ||||
|             key["key"] = hashedKey; | ||||
|             let bean = await APIKey.save(key, socket.userID); | ||||
| 
 | ||||
|             log.debug("apikeys", "Added API Key"); | ||||
|             log.debug("apikeys", key); | ||||
| 
 | ||||
|             // Append key ID to start of key seperated by -, used to get
 | ||||
|             // correct hash when validating key.
 | ||||
|             let formattedKey = bean.id + "-" + clearKey; | ||||
|             await sendAPIKeyList(socket); | ||||
| 
 | ||||
|             callback({ | ||||
|                 ok: true, | ||||
|                 msg: "Added Successfully.", | ||||
|                 key: formattedKey, | ||||
|                 keyID: bean.id, | ||||
|             }); | ||||
| 
 | ||||
|         } catch (e) { | ||||
|             callback({ | ||||
|                 ok: false, | ||||
|                 msg: e.message, | ||||
|             }); | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
|     socket.on("getAPIKeyList", async (callback) => { | ||||
|         try { | ||||
|             checkLogin(socket); | ||||
|             await sendAPIKeyList(socket); | ||||
|             callback({ | ||||
|                 ok: true, | ||||
|             }); | ||||
|         } catch (e) { | ||||
|             console.error(e); | ||||
|             callback({ | ||||
|                 ok: false, | ||||
|                 msg: e.message, | ||||
|             }); | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
|     socket.on("deleteAPIKey", async (keyID, callback) => { | ||||
|         try { | ||||
|             checkLogin(socket); | ||||
| 
 | ||||
|             log.debug("apikeys", `Deleted API Key: ${keyID} User ID: ${socket.userID}`); | ||||
| 
 | ||||
|             await R.exec("DELETE FROM api_key WHERE id = ? AND user_id = ? ", [ | ||||
|                 keyID, | ||||
|                 socket.userID, | ||||
|             ]); | ||||
| 
 | ||||
|             apicache.clear(); | ||||
| 
 | ||||
|             callback({ | ||||
|                 ok: true, | ||||
|                 msg: "Deleted Successfully.", | ||||
|             }); | ||||
| 
 | ||||
|             await sendAPIKeyList(socket); | ||||
| 
 | ||||
|         } catch (e) { | ||||
|             callback({ | ||||
|                 ok: false, | ||||
|                 msg: e.message, | ||||
|             }); | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
|     socket.on("disableAPIKey", async (keyID, callback) => { | ||||
|         try { | ||||
|             checkLogin(socket); | ||||
| 
 | ||||
|             log.debug("apikeys", `Disabled Key: ${keyID} User ID: ${socket.userID}`); | ||||
| 
 | ||||
|             await R.exec("UPDATE api_key SET active = 0 WHERE id = ? ", [ | ||||
|                 keyID, | ||||
|             ]); | ||||
| 
 | ||||
|             apicache.clear(); | ||||
| 
 | ||||
|             callback({ | ||||
|                 ok: true, | ||||
|                 msg: "Disabled Successfully.", | ||||
|             }); | ||||
| 
 | ||||
|             await sendAPIKeyList(socket); | ||||
| 
 | ||||
|         } catch (e) { | ||||
|             callback({ | ||||
|                 ok: false, | ||||
|                 msg: e.message, | ||||
|             }); | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
|     socket.on("enableAPIKey", async (keyID, callback) => { | ||||
|         try { | ||||
|             checkLogin(socket); | ||||
| 
 | ||||
|             log.debug("apikeys", `Enabled Key: ${keyID} User ID: ${socket.userID}`); | ||||
| 
 | ||||
|             await R.exec("UPDATE api_key SET active = 1 WHERE id = ? ", [ | ||||
|                 keyID, | ||||
|             ]); | ||||
| 
 | ||||
|             apicache.clear(); | ||||
| 
 | ||||
|             callback({ | ||||
|                 ok: true, | ||||
|                 msg: "Enabled Successfully", | ||||
|             }); | ||||
| 
 | ||||
|             await sendAPIKeyList(socket); | ||||
| 
 | ||||
|         } catch (e) { | ||||
|             callback({ | ||||
|                 ok: false, | ||||
|                 msg: e.message, | ||||
|             }); | ||||
|         } | ||||
|     }); | ||||
| }; | ||||
|  | @ -44,6 +44,7 @@ import { | |||
|     faWrench, | ||||
|     faHeartbeat, | ||||
|     faFilter, | ||||
|     faKey, | ||||
| } from "@fortawesome/free-solid-svg-icons"; | ||||
| 
 | ||||
| library.add( | ||||
|  | @ -88,6 +89,7 @@ library.add( | |||
|     faWrench, | ||||
|     faHeartbeat, | ||||
|     faFilter, | ||||
|     faKey, | ||||
| ); | ||||
| 
 | ||||
| export { FontAwesomeIcon }; | ||||
|  |  | |||
|  | @ -57,6 +57,12 @@ | |||
|                                 </router-link> | ||||
|                             </li> | ||||
| 
 | ||||
|                             <li> | ||||
|                                 <router-link to="/apikeys" class="dropdown-item" :class="{ active: $route.path.includes('manage-apikeys') }"> | ||||
|                                     <font-awesome-icon icon="key" /> {{ $t("API Keys") }} | ||||
|                                 </router-link> | ||||
|                             </li> | ||||
| 
 | ||||
|                             <li> | ||||
|                                 <router-link to="/settings/general" class="dropdown-item" :class="{ active: $route.path.includes('settings') }"> | ||||
|                                     <font-awesome-icon icon="cog" /> {{ $t("Settings") }} | ||||
|  |  | |||
|  | @ -34,7 +34,8 @@ export default { | |||
|             allowLoginDialog: false,        // Allowed to show login dialog, but "loggedIn" have to be true too. This exists because prevent the login dialog show 0.1s in first before the socket server auth-ed.
 | ||||
|             loggedIn: false, | ||||
|             monitorList: { }, | ||||
|             maintenanceList: { }, | ||||
|             maintenanceList: {}, | ||||
|             apiKeyList: {}, | ||||
|             heartbeatList: { }, | ||||
|             importantHeartbeatList: { }, | ||||
|             avgPingList: { }, | ||||
|  | @ -134,6 +135,10 @@ export default { | |||
|                 this.maintenanceList = data; | ||||
|             }); | ||||
| 
 | ||||
|             socket.on("apiKeyList", (data) => { | ||||
|                 this.apiKeyList = data; | ||||
|             }); | ||||
| 
 | ||||
|             socket.on("notificationList", (data) => { | ||||
|                 this.notificationList = data; | ||||
|             }); | ||||
|  | @ -461,6 +466,17 @@ export default { | |||
|             socket.emit("getMaintenanceList", callback); | ||||
|         }, | ||||
| 
 | ||||
|         /** | ||||
|          * Send list of API keys | ||||
|          * @param {socketCB} callback | ||||
|          */ | ||||
|         getAPIKeyList(callback) { | ||||
|             if (!callback) { | ||||
|                 callback = () => { }; | ||||
|             } | ||||
|             socket.emit("getAPIKeyList", callback); | ||||
|         }, | ||||
| 
 | ||||
|         /** | ||||
|          * Add a monitor | ||||
|          * @param {Object} monitor Object representing monitor to add | ||||
|  | @ -503,6 +519,24 @@ export default { | |||
|             socket.emit("deleteMaintenance", maintenanceID, callback); | ||||
|         }, | ||||
| 
 | ||||
|         /** | ||||
|          * Add an API key | ||||
|          * @param {Object} key API key to add | ||||
|          * @param {socketCB} callback | ||||
|          */ | ||||
|         addAPIKey(key, callback) { | ||||
|             socket.emit("addAPIKey", key, callback); | ||||
|         }, | ||||
| 
 | ||||
|         /** | ||||
|          * Delete specified API key | ||||
|          * @param {int} keyID ID of key to delete | ||||
|          * @param {socketCB} callback | ||||
|          */ | ||||
|         deleteAPIKey(keyID, callback) { | ||||
|             socket.emit("deleteAPIKey", keyID, callback); | ||||
|         }, | ||||
| 
 | ||||
|         /** Clear the hearbeat list */ | ||||
|         clearData() { | ||||
|             console.log("reset heartbeat list"); | ||||
|  |  | |||
							
								
								
									
										198
									
								
								src/pages/AddAPIKey.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										198
									
								
								src/pages/AddAPIKey.vue
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,198 @@ | |||
| <template> | ||||
|     <transition name="slide-fade" appear> | ||||
|         <div> | ||||
|             <h1 class="mb-3">{{ $t("Add API Key") }}</h1> | ||||
|             <form @submit.prevent="submit"> | ||||
|                 <div class="shadow-box"> | ||||
|                     <div class="row"> | ||||
|                         <div class="col-xl-10"> | ||||
|                             <!-- Title --> | ||||
|                             <div class="mb-3"> | ||||
|                                 <label for="name" class="form-label">{{ $t("Title") }}</label> | ||||
|                                 <input | ||||
|                                     id="name" v-model="key.name" type="text" class="form-control" | ||||
|                                     required | ||||
|                                 > | ||||
|                             </div> | ||||
| 
 | ||||
|                             <h2 class="mt-5">{{ $t("Expiry") }}</h2> | ||||
| 
 | ||||
|                             <!-- Expiry --> | ||||
|                             <div class="my-3"> | ||||
|                                 <label class="form-label">{{ $t("Expiry date") }}</label> | ||||
|                                 <Datepicker | ||||
|                                     v-model="key.expires" | ||||
|                                     :dark="$root.isDark" | ||||
|                                     :monthChangeOnScroll="false" | ||||
|                                     :minDate="minDate" | ||||
|                                     format="yyyy-MM-dd HH:mm" | ||||
|                                     modelType="yyyy-MM-dd HH:mm:ss" | ||||
|                                     :required="!noExpire" | ||||
|                                 /> | ||||
| 
 | ||||
|                                 <div class="form-check mb-2"> | ||||
|                                     <input | ||||
|                                         id="no-expire" v-model="noExpire" class="form-check-input" | ||||
|                                         type="checkbox" | ||||
|                                     > | ||||
|                                     <label class="form-check-label" for="no-expire">{{ | ||||
|                                         $t("Don't expire") | ||||
|                                     }}</label> | ||||
|                                 </div> | ||||
|                             </div> | ||||
| 
 | ||||
|                             <div class="mt-4 mb-1"> | ||||
|                                 <button | ||||
|                                     id="monitor-submit-btn" class="btn btn-primary" type="submit" | ||||
|                                     :disabled="processing" | ||||
|                                 > | ||||
|                                     {{ $t("Save") }} | ||||
|                                 </button> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </form> | ||||
|             <Confirm | ||||
|                 ref="keyAdded" | ||||
|                 :yes-text="$t('Continue')" | ||||
|                 :no-text="$t('Add Another')" | ||||
|                 :title="$t('Key Added')" | ||||
|                 @yes="postAdd" | ||||
|                 @no="clearForm" | ||||
|             > | ||||
|                 <p>{{ $t("apiKeyAddedMsg") }}</p> | ||||
|                 <p>{{ clearKey }}</p> | ||||
|             </Confirm> | ||||
|         </div> | ||||
|     </transition> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| 
 | ||||
| import { useToast } from "vue-toastification"; | ||||
| import dayjs from "dayjs"; | ||||
| import Datepicker from "@vuepic/vue-datepicker"; | ||||
| import Confirm from "../components/Confirm.vue"; | ||||
| 
 | ||||
| const toast = useToast(); | ||||
| 
 | ||||
| export default { | ||||
|     components: { | ||||
|         Confirm, | ||||
|         Datepicker | ||||
|     }, | ||||
| 
 | ||||
|     data() { | ||||
|         return { | ||||
|             processing: false, | ||||
|             key: {}, | ||||
|             dark: (this.$root.theme === "dark"), | ||||
|             minDate: this.$root.date(dayjs()) + " 00:00", | ||||
|             clearKey: null, | ||||
|             noExpire: false, | ||||
|         }; | ||||
|     }, | ||||
| 
 | ||||
|     watch: { | ||||
|         "$route.fullPath"() { | ||||
|             this.init(); | ||||
|         }, | ||||
|     }, | ||||
| 
 | ||||
|     mounted() { | ||||
|         this.init(); | ||||
|     }, | ||||
| 
 | ||||
|     methods: { | ||||
|         /** Initialise page */ | ||||
|         init() { | ||||
|             this.clearForm(); | ||||
|         }, | ||||
| 
 | ||||
|         /** Redirect user to apikey list */ | ||||
|         postAdd() { | ||||
|             this.$router.push("/apikeys"); | ||||
|         }, | ||||
| 
 | ||||
|         /** Clear the form */ | ||||
|         clearForm() { | ||||
|             this.key = { | ||||
|                 name: "", | ||||
|                 expires: this.minDate, | ||||
|                 active: 1, | ||||
|             }; | ||||
|             this.noExpire = false; | ||||
|         }, | ||||
| 
 | ||||
|         /** Submit data to server */ | ||||
|         async submit() { | ||||
|             this.processing = true; | ||||
| 
 | ||||
|             if (this.noExpire) { | ||||
|                 this.key.expires = null; | ||||
|             } | ||||
| 
 | ||||
|             this.$root.addAPIKey(this.key, async (res) => { | ||||
|                 if (res.ok) { | ||||
|                     this.clearKey = res.key; | ||||
|                     this.$refs.keyAdded.show(); | ||||
|                     this.clearForm(); | ||||
| 
 | ||||
|                 } else { | ||||
|                     toast.error(res.msg); | ||||
|                 } | ||||
|                 this.processing = false; | ||||
|             }); | ||||
|         }, | ||||
|     }, | ||||
| }; | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .shadow-box { | ||||
|     padding: 20px; | ||||
| } | ||||
| 
 | ||||
| textarea { | ||||
|     min-height: 150px; | ||||
| } | ||||
| 
 | ||||
| .dark-calendar::-webkit-calendar-picker-indicator { | ||||
|     filter: invert(1); | ||||
| } | ||||
| 
 | ||||
| .weekday-picker { | ||||
|     display: flex; | ||||
|     gap: 10px; | ||||
| 
 | ||||
|     & > div { | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|         align-items: center; | ||||
|         width: 40px; | ||||
| 
 | ||||
|         .form-check-inline { | ||||
|             margin-right: 0; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| .day-picker { | ||||
|     display: flex; | ||||
|     gap: 10px; | ||||
|     flex-wrap: wrap; | ||||
| 
 | ||||
|     & > div { | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|         align-items: center; | ||||
|         width: 40px; | ||||
| 
 | ||||
|         .form-check-inline { | ||||
|             margin-right: 0; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| </style> | ||||
							
								
								
									
										255
									
								
								src/pages/ManageAPIKeys.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										255
									
								
								src/pages/ManageAPIKeys.vue
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,255 @@ | |||
| <template> | ||||
|     <transition name="slide-fade" appear> | ||||
|         <div> | ||||
|             <h1 class="mb-3"> | ||||
|                 {{ $t("API Keys") }} | ||||
|             </h1> | ||||
| 
 | ||||
|             <div> | ||||
|                 <router-link to="/apikeys/add" class="btn btn-primary mb-3"> | ||||
|                     <font-awesome-icon icon="plus" /> {{ $t("Add API Key") }} | ||||
|                 </router-link> | ||||
|             </div> | ||||
| 
 | ||||
|             <div class="shadow-box"> | ||||
|                 <span v-if="Object.keys(keyList).length === 0" class="d-flex align-items-center justify-content-center my-3"> | ||||
|                     {{ $t("No API Keys") }} | ||||
|                 </span> | ||||
| 
 | ||||
|                 <div | ||||
|                     v-for="(item, index) in keyList" | ||||
|                     :key="index" | ||||
|                     class="item" | ||||
|                     :class="item.status" | ||||
|                 > | ||||
|                     <div class="left-part"> | ||||
|                         <div | ||||
|                             class="circle" | ||||
|                         ></div> | ||||
|                         <div class="info"> | ||||
|                             <div class="title">{{ item.name }}</div> | ||||
|                             <div class="status"> | ||||
|                                 {{ $t("apiKey-" + item.status) }} | ||||
|                             </div> | ||||
|                             <div class="date"> | ||||
|                                 {{ $t("Created") }}: {{ item.createdDate }} | ||||
|                             </div> | ||||
|                             <div class="date"> | ||||
|                                 {{ $t("Expires") }}: {{ item.expires || $t("Never") }} | ||||
|                             </div> | ||||
|                         </div> | ||||
|                     </div> | ||||
| 
 | ||||
|                     <div class="buttons"> | ||||
|                         <div class="btn-group" role="group"> | ||||
|                             <button v-if="item.active" class="btn btn-normal" @click="disableDialog(item.id)"> | ||||
|                                 <font-awesome-icon icon="pause" /> {{ $t("Disable") }} | ||||
|                             </button> | ||||
| 
 | ||||
|                             <button v-if="!item.active" class="btn btn-primary" @click="enableKey(item.id)"> | ||||
|                                 <font-awesome-icon icon="play" /> {{ $t("Enable") }} | ||||
|                             </button> | ||||
| 
 | ||||
|                             <button class="btn btn-danger" @click="deleteDialog(item.id)"> | ||||
|                                 <font-awesome-icon icon="trash" /> {{ $t("Delete") }} | ||||
|                             </button> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
| 
 | ||||
|             <div class="text-center mt-3" style="font-size: 13px;"> | ||||
|                 <a href="https://github.com/louislam/uptime-kuma/wiki/Maintenance" target="_blank">Learn More</a> | ||||
|             </div> | ||||
| 
 | ||||
|             <Confirm ref="confirmPause" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="disableKey"> | ||||
|                 {{ $t("disableAPIKeyMsg") }} | ||||
|             </Confirm> | ||||
| 
 | ||||
|             <Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteKey"> | ||||
|                 {{ $t("deleteAPIKeyMsg") }} | ||||
|             </Confirm> | ||||
|         </div> | ||||
|     </transition> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| import Confirm from "../components/Confirm.vue"; | ||||
| import { useToast } from "vue-toastification"; | ||||
| const toast = useToast(); | ||||
| 
 | ||||
| export default { | ||||
|     components: { | ||||
|         Confirm, | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             selectedKeyID: null, | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         keyList() { | ||||
|             let result = Object.values(this.$root.apiKeyList); | ||||
|             return result; | ||||
|         }, | ||||
|     }, | ||||
| 
 | ||||
|     methods: { | ||||
|         /** | ||||
|          * Show dialog to confirm deletion | ||||
|          * @param {number} keyID ID of monitor that is being deleted | ||||
|          */ | ||||
|         deleteDialog(keyID) { | ||||
|             this.selectedKeyID = keyID; | ||||
|             this.$refs.confirmDelete.show(); | ||||
|         }, | ||||
| 
 | ||||
|         /** | ||||
|          * Delete a key | ||||
|          */ | ||||
|         deleteKey() { | ||||
|             this.$root.deleteAPIKey(this.selectedKeyID, (res) => { | ||||
|                 if (res.ok) { | ||||
|                     toast.success(res.msg); | ||||
|                     this.$router.push("/apikeys"); | ||||
|                 } else { | ||||
|                     toast.error(res.msg); | ||||
|                 } | ||||
|             }); | ||||
|         }, | ||||
| 
 | ||||
|         /** | ||||
|          * Show dialog to confirm pause | ||||
|          */ | ||||
|         disableDialog(keyID) { | ||||
|             this.selectedKeyID = keyID; | ||||
|             this.$refs.confirmPause.show(); | ||||
|         }, | ||||
| 
 | ||||
|         /** | ||||
|          * Pause maintenance | ||||
|          */ | ||||
|         disableKey() { | ||||
|             this.$root.getSocket().emit("disableAPIKey", this.selectedKeyID, (res) => { | ||||
|                 this.$root.toastRes(res); | ||||
|             }); | ||||
|         }, | ||||
| 
 | ||||
|         /** | ||||
|          * Resume maintenance | ||||
|          */ | ||||
|         enableKey(id) { | ||||
|             this.$root.getSocket().emit("enableAPIKey", id, (res) => { | ||||
|                 this.$root.toastRes(res); | ||||
|             }); | ||||
|         }, | ||||
|     }, | ||||
| }; | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
|     @import "../assets/vars.scss"; | ||||
| 
 | ||||
|     .mobile { | ||||
|         .item { | ||||
|             flex-direction: column; | ||||
|             align-items: flex-start; | ||||
|             margin-bottom: 20px; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     .item { | ||||
|         display: flex; | ||||
|         align-items: center; | ||||
|         gap: 10px; | ||||
|         text-decoration: none; | ||||
|         border-radius: 10px; | ||||
|         transition: all ease-in-out 0.15s; | ||||
|         justify-content: space-between; | ||||
|         padding: 10px; | ||||
|         min-height: 90px; | ||||
|         margin-bottom: 5px; | ||||
| 
 | ||||
|         &:hover { | ||||
|             background-color: $highlight-white; | ||||
|         } | ||||
| 
 | ||||
|         &.active { | ||||
|             .circle { | ||||
|                 background-color: $primary; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         &.inactive { | ||||
|             .circle { | ||||
|                 background-color: $danger; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         &.expired { | ||||
|             .left-part { | ||||
|                 opacity: 0.3; | ||||
|             } | ||||
| 
 | ||||
|             .circle { | ||||
|                 background-color: $dark-font-color; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         .left-part { | ||||
|             display: flex; | ||||
|             gap: 12px; | ||||
|             align-items: center; | ||||
| 
 | ||||
|             .circle { | ||||
|                 width: 25px; | ||||
|                 height: 25px; | ||||
|                 border-radius: 50rem; | ||||
|             } | ||||
| 
 | ||||
|             .info { | ||||
|                 .title { | ||||
|                     font-weight: bold; | ||||
|                     font-size: 20px; | ||||
|                 } | ||||
| 
 | ||||
|                 .status { | ||||
|                     font-size: 14px; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         .buttons { | ||||
|             display: flex; | ||||
|             gap: 8px; | ||||
|             flex-direction: row-reverse; | ||||
| 
 | ||||
|             .btn-group { | ||||
|                 width: 310px; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     .date { | ||||
|         margin-top: 5px; | ||||
|         display: block; | ||||
|         font-size: 14px; | ||||
|         background-color: rgba(255, 255, 255, 0.5); | ||||
|         border-radius: 20px; | ||||
|         padding: 0 10px; | ||||
|         width: fit-content; | ||||
| 
 | ||||
|         .dark & { | ||||
|             color: white; | ||||
|             background-color: rgba(255, 255, 255, 0.1); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     .dark { | ||||
|         .item { | ||||
|             &:hover { | ||||
|                 background-color: $dark-bg2; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| </style> | ||||
|  | @ -7,6 +7,9 @@ | |||
|             <router-link to="/maintenance" class="nav-link"> | ||||
|                 <font-awesome-icon icon="wrench" /> {{ $t("Maintenance") }} | ||||
|             </router-link> | ||||
|             <router-link to="/apikeys" class="nav-link" :class="{ active: $route.path.includes('manage-apikeys') }"> | ||||
|                 <font-awesome-icon icon="key" /> {{ $t("API Keys") }} | ||||
|             </router-link> | ||||
|         </div> | ||||
| 
 | ||||
|         <h1 v-show="show" class="mb-3"> | ||||
|  |  | |||
|  | @ -18,6 +18,8 @@ import NotFound from "./pages/NotFound.vue"; | |||
| import DockerHosts from "./components/settings/Docker.vue"; | ||||
| import MaintenanceDetails from "./pages/MaintenanceDetails.vue"; | ||||
| import ManageMaintenance from "./pages/ManageMaintenance.vue"; | ||||
| import ManageAPIKeys from "./pages/ManageAPIKeys.vue"; | ||||
| import AddAPIKey from "./pages/AddAPIKey.vue"; | ||||
| 
 | ||||
| // Settings - Sub Pages
 | ||||
| import Appearance from "./components/settings/Appearance.vue"; | ||||
|  | @ -145,6 +147,14 @@ const routes = [ | |||
|                         path: "/maintenance/edit/:id", | ||||
|                         component: EditMaintenance, | ||||
|                     }, | ||||
|                     { | ||||
|                         path: "/apikeys", | ||||
|                         component: ManageAPIKeys | ||||
|                     }, | ||||
|                     { | ||||
|                         path: "/apikeys/add", | ||||
|                         component: AddAPIKey | ||||
|                     }, | ||||
|                 ], | ||||
|             }, | ||||
|         ], | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue