@cryptotaxi247 / CoPilot / commits / b9b77692

Connectors page (#81)

* connector to nav bar, route, and base connector.vue page * connectors_available * list_connectors_available only verify connection once rather than twice * precommit fixes

taylor_socfortress committed Jul 27, 2023 at 12:10 UTC b9b776929b620fc90c9041056a43354ec5b23c57
5 files changed +759 -13
backend/app/routes/connectors.py
+14 -9
@@ -33,15 +33,20 @@ def list_connectors_available():
33 logger.info("Received request to get all available connectors")
34 connectors_service = ConnectorService(db)
35 connectors = ConnectorsAvailable.query.all()
36 - result = connectors_available_schema.dump(connectors)
37 -
38 - instantiated_connectors = [
39 - connectors_service.process_connector(connector["connector_name"])
40 - for connector in result
41 - if connectors_service.process_connector(connector["connector_name"])
42 - ]
43 -
44 - return {"message": "All available connectors", "connectors": instantiated_connectors, "success": True}
36 + connectors_available = connectors_available_schema.dump(connectors)
37 +
38 + instantiated_connectors = []
39 + for connector in connectors_available:
40 + processed_connector = connectors_service.process_connector(connector["connector_name"])
41 + if processed_connector:
42 + instantiated_connectors.append(processed_connector)
43 +
44 + return {
45 + "message": "All available connectors",
46 + "connectors": instantiated_connectors,
47 + "connectors_available": connectors_available,
48 + "success": True,
49 + }
50 except Exception as e:
51 logger.error(f"Error while getting all available connectors: {e}")
52 return {"message": "Error while getting all available connectors", "success": False}, 500
src/core/nav.vue
+1 -1
@@ -22,7 +22,7 @@
22 <span slot="title">Crypto</span>
23 </el-menu-item>
24 </el-sub-menu>
25 - <el-menu-item index="/calendar"> <i class="mdi mdi-calendar"></i><span slot="title">Calendar</span> </el-menu-item>
25 + <el-menu-item index="/connectors"> <i class="mdi mdi-antenna"></i><span slot="title">Connectors</span> </el-menu-item>
26 <el-menu-item index="/contacts"> <i class="mdi mdi-account-multiple-outline"></i><span slot="title">Contacts</span> </el-menu-item>
27 <el-menu-item index="/gallery"> <i class="mdi mdi-image-multiple-outline"></i><span slot="title">Gallery</span> </el-menu-item>
28 <el-menu-item index="/cards"> <i class="mdi mdi-view-dashboard-outline"></i><span slot="title">Cards</span> </el-menu-item>
src/router/connectors.ts new
+26
@@ -0,0 +1,26 @@
1 +import Connectors from "../views/apps/Connectors.vue"
2 +
3 +import layouts from "../layout"
4 +
5 +export default {
6 + path: "/connectors",
7 + name: "connectors",
8 + component: Connectors,
9 + meta: {
10 + auth: true,
11 + layout: layouts.navLeft
12 + },
13 + children: [
14 + {
15 + path: "/connectors",
16 + name: "connectors",
17 + component: Connectors,
18 + meta: {
19 + auth: true,
20 + layout: layouts.navLeft,
21 + searchable: true,
22 + tags: ["simple"]
23 + }
24 + }
25 + ]
26 +}
src/router/index.ts
+4 -3
@@ -10,6 +10,7 @@ import Gallery from "../views/apps/Gallery.vue"
10 import Cards from "../views/apps/Cards.vue"
11 import Mail from "../views/apps/Mail.vue"
12 import Ecommerce from "./ecommerce"
13 +import Connectors from "../views/apps/Connectors.vue"
14 /*
15
16 //pages
@@ -81,9 +82,9 @@ const router = createRouter({
82 }
83 },
84 {
84 - path: "/calendar",
85 - name: "calendar",
86 - component: Calendar,
85 + path: "/connectors",
86 + name: "connectors",
87 + component: Connectors,
88 meta: {
89 auth: true,
90 layout: layouts.navLeft,
src/views/apps/Connectors.vue new
+714
@@ -0,0 +1,714 @@
1 +<template>
2 + <div class="page-table scrollable only-y" id="affix-container">
3 + <div class="page-header">
4 + <h1>Connectors</h1>
5 + <h4>Configure connections to your toolset.</h4>
6 + <el-breadcrumb separator="/">
7 + <el-breadcrumb-item :to="{ path: '/' }"><i class="mdi mdi-home-outline"></i></el-breadcrumb-item>
8 + <el-breadcrumb-item>Components</el-breadcrumb-item>
9 + <el-breadcrumb-item>Tables</el-breadcrumb-item>
10 + <el-breadcrumb-item>Table</el-breadcrumb-item>
11 + </el-breadcrumb>
12 + </div>
13 +
14 + <div class="table-box card-base card-shadow--medium scrollable only-x">
15 + <table class="styled striped">
16 + <thead>
17 + <tr>
18 + <th scope="col">Connector Name</th>
19 + <th scope="col">Connector Description</th>
20 + <th scope="col">Connector Supports</th>
21 + <th scope="col">Connector Configured</th>
22 + <th scope="col">Connector Verified</th>
23 + <th scope="col">Connector Options</th>
24 + </tr>
25 + </thead>
26 + <tbody>
27 + <tr v-for="connector in connectors" :key="connector.id">
28 + <!-- Display the connector details in the table -->
29 + <td>{{ connector.connector_name }}</td>
30 + <td>{{ connector.connector_description }}</td>
31 + <td>{{ connector.connector_supports }}</td>
32 + <td>
33 + <el-button type="primary" v-if="connector.connector_configured == true">True</el-button>
34 + <el-button type="info" v-else>False</el-button>
35 + </td>
36 + <!-- Show the connector verified which is in the `connector` table -->
37 + <td>
38 + <el-button type="success" v-if="connector.connector_verified == true">True</el-button>
39 + <el-button type="danger" v-else>False</el-button>
40 + </td>
41 + <td>
42 + <div class="btn-group" role="group">
43 + <!--If the connector is not already configured then display the configure button -->
44 + <el-button
45 + type="primary"
46 + round
47 + v-if="
48 + (connector.connector_configured == false || connector.connector_configured == null) &&
49 + connector.connector_name.toLowerCase() !== 'velociraptor'
50 + "
51 + @click="openConfigureModal(connector)"
52 + >
53 + Configure
54 + </el-button>
55 +
56 + <!--If the connector is not already configured and the connector_name IS `velociraptor`, then display the configure button -->
57 + <el-button
58 + type="primary"
59 + round
60 + v-if="
61 + (connector.connector_configured == false || connector.connector_configured == null) &&
62 + connector.connector_name.toLowerCase() === 'velociraptor'
63 + "
64 + @click="openConfigureModalFile(connector)"
65 + >
66 + Configure
67 + </el-button>
68 + <!-- If the connector is already configured and the connector_name is not `shuffle` or `dfir-iris` to lower, then display the update button -->
69 + <el-button
70 + type="warning"
71 + round
72 + v-if="
73 + connector.connector_configured == true &&
74 + // connector.connector_name.toLowerCase() !==
75 + // 'shuffle' &&
76 + // connector.connector_name.toLowerCase() !==
77 + // 'dfir-iris' &&
78 + connector.connector_name.toLowerCase() !== 'velociraptor'
79 + "
80 + @click="openUpdateModal(connector)"
81 + >
82 + Update
83 + </el-button>
84 + <!-- Update Velociraptor -->
85 + <el-button
86 + type="warning"
87 + round
88 + v-if="
89 + connector.connector_configured == true &&
90 + // connector.connector_name.toLowerCase() !==
91 + // 'shuffle' &&
92 + // connector.connector_name.toLowerCase() !==
93 + // 'dfir-iris' &&
94 + connector.connector_name.toLowerCase() === 'velociraptor'
95 + "
96 + @click="openConfigureModalFile(connector)"
97 + >
98 + Update
99 + </el-button>
100 + <!-- If the connector is already configured and the connector_name IS `shuffle` or `dfir-iris` to lower, then display the update button -->
101 + <!-- <el-button type="warning" round
102 + v-if="
103 + connector.connector_configured == true &&
104 + (connector.connector_name.toLowerCase() ===
105 + 'shuffle' ||
106 + connector.connector_name.toLowerCase() ===
107 + 'dfir-iris')
108 + "
109 + @click="openUpdateModal(connector)"
110 + >
111 + Update
112 + </el-button> -->
113 +
114 + <!--<button type="button" class="btn btn-info btn-sm" @click="updateConnector(connector)">Update</button>-->
115 + <!--<button type="button" class="btn btn-info btn-sm" @click="deleteConnector(connector)">Delete</button>-->
116 + </div>
117 + </td>
118 + </tr>
119 + </tbody>
120 + </table>
121 + </div>
122 +
123 + <!-- Configure Modal Username and Password API Key -->
124 + <div v-bind:class="{ 'modal-window': true, 'is-active': isConfigureModalActive }">
125 + <div class="modal-background" @click="openConfigureModal = false"></div>
126 + <div>
127 + <el-form :model="connectorForm" status-icon :rules="rules2" ref="connectorForm" label-width="120px" class="demo-ruleForm">
128 + <div class="modal-card">
129 + <header class="modal-card-head">
130 + <h2 class="modal-card-title">Configure {{ currentConnector ? currentConnector.connector_name : "" }}</h2>
131 + <img
132 + :src="`/src/assets/images/${
133 + currentConnector ? currentConnector.connector_name.toLowerCase() + '.svg' : 'default-logo.svg'
134 + }`"
135 + alt="Logo"
136 + class="modal-logo"
137 + />
138 + </header>
139 + <section class="modal-card-body">
140 + <div class="field">
141 + <label class="label">Connector URL</label>
142 + <div class="control">
143 + <input class="input" type="text" v-model="connectorForm.connector_url" required />
144 + </div>
145 + </div>
146 +
147 + <!-- API Key input for dfir-iris and shuffle -->
148 + <div
149 + class="field"
150 + v-if="
151 + ['dfir-iris', 'shuffle'].includes(currentConnector ? currentConnector.connector_name.toLowerCase() : '')
152 + "
153 + >
154 + <label class="label">API Key</label>
155 + <div class="control">
156 + <input class="input" type="text" v-model="connectorForm.connector_api_key" required />
157 + </div>
158 + </div>
159 +
160 + <!-- Username and Password inputs for other connectors -->
161 + <template v-else>
162 + <div class="field">
163 + <label class="label">Username</label>
164 + <div class="control">
165 + <input class="input" type="text" v-model="connectorForm.username" required />
166 + </div>
167 + </div>
168 + <div class="field">
169 + <label class="label">Password</label>
170 + <div class="control">
171 + <input class="input" type="password" v-model="connectorForm.password" required />
172 + </div>
173 + </div>
174 + </template>
175 +
176 + <div class="field">
177 + <div class="control">
178 + <button class="button is-primary" type="submit" @click.prevent="configureConnector">Save</button>
179 + <button class="button" type="button" @click="closeDialogUserandPass">Cancel</button>
180 + </div>
181 + </div>
182 + </section>
183 + </div>
184 + </el-form>
185 + </div>
186 + </div>
187 +
188 + <!-- Configure Modal File -->
189 + <div v-bind:class="{ 'modal-window': true, 'is-active': isConfigureModalFileActive }">
190 + <div class="modal-background" @click="openConfigureModalFile = false"></div>
191 + <div>
192 + <el-form :model="connectorForm" status-icon :rules="rules2" ref="connectorForm" label-width="120px" class="demo-ruleForm">
193 + <div class="modal-card">
194 + <header class="modal-card-head">
195 + <h2 class="modal-card-title">Configure File {{ currentConnector ? currentConnector.connector_name : "" }}</h2>
196 + <img
197 + :src="`/src/assets/images/${
198 + currentConnector ? currentConnector.connector_name.toLowerCase() + '.svg' : 'default-logo.svg'
199 + }`"
200 + alt="Logo"
201 + class="modal-logo"
202 + />
203 + </header>
204 + <section class="modal-card-body">
205 + <!-- Add the el-upload component here -->
206 + <el-upload
207 + class="upload-demo"
208 + action="http://localhost:5000/connectors/upload"
209 + :on-preview="handlePreview"
210 + :on-remove="handleRemove"
211 + :before-remove="beforeRemove"
212 + :on-success="handleSuccess"
213 + multiple
214 + drag
215 + :limit="3"
216 + :on-exceed="handleExceed"
217 + :file-list="fileList"
218 + >
219 + <i class="el-icon-upload"></i>
220 + <div class="el-upload__text">Drop file here or <em>click to upload</em></div>
221 + <div class="el-upload__tip" slot="tip">jpg/png files with a size less than 500kb</div>
222 + </el-upload>
223 + <!-- Add the cancel and submit buttons -->
224 + <div class="field">
225 + <div class="control">
226 + <button class="button" type="button" @click="closeConfigureModuleFile">Cancel</button>
227 + </div>
228 + </div>
229 + <!-- Rest of your existing code... -->
230 + </section>
231 + </div>
232 + </el-form>
233 + </div>
234 + </div>
235 +
236 + <!-- Update Modal Username and Password-->
237 + <div v-bind:class="{ 'modal-window': true, 'is-active': isUpdateModalActive }">
238 + <div class="modal-background" @click="openUpdateModal = false"></div>
239 + <div>
240 + <el-form :model="connectorForm" status-icon :rules="rules2" ref="connectorForm" label-width="120px" class="demo-ruleForm">
241 + <div class="modal-card">
242 + <header class="modal-card-head">
243 + <h2 class="modal-card-title">Update {{ currentConnector ? currentConnector.connector_name : "" }}</h2>
244 + <img
245 + :src="`/src/assets/images/${
246 + currentConnector ? currentConnector.connector_name.toLowerCase() + '.svg' : 'default-logo.svg'
247 + }`"
248 + alt="Logo"
249 + class="modal-logo"
250 + />
251 + </header>
252 + <section class="modal-card-body">
253 + <div class="field">
254 + <label class="label">Connector URL</label>
255 + <div class="control">
256 + <input class="input" type="text" v-model="connectorForm.connector_url" required />
257 + </div>
258 + </div>
259 +
260 + <!-- API Key input for dfir-iris and shuffle -->
261 + <div
262 + class="field"
263 + v-if="
264 + ['dfir-iris', 'shuffle'].includes(currentConnector ? currentConnector.connector_name.toLowerCase() : '')
265 + "
266 + >
267 + <label class="label">API Key</label>
268 + <div class="control">
269 + <input class="input" type="text" v-model="connectorForm.connector_api_key" required />
270 + </div>
271 + </div>
272 +
273 + <!-- Username and Password inputs for other connectors -->
274 + <template v-else>
275 + <div class="field">
276 + <label class="label">Username</label>
277 + <div class="control">
278 + <input class="input" type="text" v-model="connectorForm.username" required />
279 + </div>
280 + </div>
281 + <div class="field">
282 + <label class="label">Password</label>
283 + <div class="control">
284 + <input class="input" type="password" v-model="connectorForm.password" required />
285 + </div>
286 + </div>
287 + </template>
288 +
289 + <div class="field">
290 + <div class="control">
291 + <button class="button is-primary" type="submit" @click.prevent="updateConnector">Save</button>
292 + <button class="button" type="button" @click="closeUpdateModal">Cancel</button>
293 + </div>
294 + </div>
295 + </section>
296 + </div>
297 + </el-form>
298 + </div>
299 + </div>
300 + </div>
301 +</template>
302 +
303 +<script>
304 +import Affix from "@/components/Affix.vue"
305 +import axios from "axios"
306 +
307 +import { defineComponent } from "@vue/runtime-core"
308 +import { component } from "v-viewer"
309 +
310 +export default defineComponent({
311 + data() {
312 + return {
313 + connectors: [],
314 + currentConnector: null,
315 +
316 + // Configure Modal
317 + isConfigureModalActive: false,
318 + isConfigureModalFileActive: false,
319 +
320 + // Update Modal
321 + isUpdateModalActive: false,
322 +
323 + loading: false,
324 + showForm: false,
325 + successMessage: "",
326 + errorMessage: "",
327 + connectorForm: {
328 + connector_url: "",
329 + username: "",
330 + password: "",
331 + connector_api_key: ""
332 + }
333 + }
334 + },
335 + methods: {
336 + showDialogUserandPass(connector) {
337 + this.currentConnector = connector
338 + this.showForm = true
339 + // this.$refs.userAndPassDialog.showModal();
340 + },
341 +
342 + openConfigureModal(connector) {
343 + // Open the configure modal and set the initial form values
344 + this.isConfigureModalActive = true
345 + this.currentConnector = connector // Store the current connector
346 + this.connectorForm.connector_url = connector.connector_url
347 + this.connectorForm.username = connector.connector_username
348 + this.connectorForm.password = connector.connector_password
349 + this.connectorForm.connector_api_key = connector.connector_api_key
350 + },
351 +
352 + openConfigureModalFile(connector) {
353 + // Open the configure modal and set the initial form values
354 + this.isConfigureModalFileActive = true
355 + this.currentConnector = connector // Store the current connector
356 + this.connectorForm.connector_url = connector.connector_url
357 + this.connectorForm.username = connector.connector_username
358 + this.connectorForm.password = connector.connector_password
359 + this.connectorForm.connector_api_key = connector.connector_api_key
360 + },
361 +
362 + closeDialogUserandPass() {
363 + // this.$refs.userAndPassDialog.close();
364 + this.showForm = false
365 + this.isConfigureModalActive = false
366 + },
367 +
368 + closeConfigureModuleFile() {
369 + // this.$refs.userAndPassDialog.close();
370 + this.showForm = false
371 + this.isConfigureModalFileActive = false
372 + },
373 +
374 + openUpdateModal(connector) {
375 + // Open the update modal and set the initial form values
376 + this.isUpdateModalActive = true
377 + this.currentConnector = connector // Store the current connector
378 + this.connectorForm.connector_url = connector.connector_url
379 + this.connectorForm.username = connector.connector_username
380 + this.connectorForm.password = connector.connector_password
381 + this.connectorForm.connector_api_key = connector.connector_api_key
382 + },
383 +
384 + closeUpdateModal() {
385 + // this.$refs.userAndPassDialog.close();
386 + this.showForm = false
387 + this.isUpdateModalActive = false
388 + },
389 +
390 + configureConnector(event) {
391 + event.preventDefault()
392 + const { connector_url, username, password, connector_api_key } = this.connectorForm
393 + const path = `http://localhost:5000/connectors/${this.currentConnector.id}`
394 + this.loading = true
395 + this.closeDialogUserandPass()
396 +
397 + if (connector_api_key) {
398 + console.log("POST request to: ", path)
399 + console.log("Data: ", {
400 + connector_url: connector_url,
401 + connector_api_key: connector_api_key
402 + })
403 + axios
404 + .post(path, {
405 + connector_url: connector_url,
406 + connector_api_key: connector_api_key
407 + })
408 + .then(() => {
409 + this.successMessage = "Connector has been successfully configured." // Set success message
410 + setTimeout(() => {
411 + this.successMessage = "" // Clear success message after 5 seconds
412 + }, 5000)
413 + this.getConnectors() // Refresh the connectors
414 + })
415 + .catch(err => {
416 + if (err.response.status === 400) {
417 + this.errorMessage =
418 + "This connector is already configured. If you would like to reconfigure this connector select `Edit`." // Set the error message
419 + } else if (err.response.status === 401) {
420 + this.errorMessage = "Unauthorized. Please check your endpoint URL, username and password." // Set the error message
421 + } else {
422 + this.errorMessage =
423 + "Error updating the connector. Your settings were not inserted into the keystore. Please try again." // Set the error message
424 + }
425 + setTimeout(() => {
426 + this.errorMessage = "" // Clear error message after 5 seconds
427 + }, 5000)
428 + this.getConnectors() // Refresh the connectors
429 + console.error(err) // Also log the error for debugging
430 + })
431 + .finally(() => {
432 + this.loading = false // Set loading to false
433 + })
434 + } else {
435 + axios
436 + .post(path, {
437 + connector_url: connector_url,
438 + connector_username: username,
439 + connector_password: password
440 + })
441 + .then(() => {
442 + this.successMessage = "Connector has been successfully configured." // Set success message
443 + setTimeout(() => {
444 + this.successMessage = "" // Clear success message after 5 seconds
445 + }, 5000)
446 + this.getConnectors() // Refresh the connectors
447 + })
448 + .catch(err => {
449 + if (err.response.status === 400) {
450 + this.errorMessage =
451 + "This connector is already configured. If you would like to reconfigure this connector select `Edit`." // Set the error message
452 + } else if (err.response.status === 401) {
453 + this.errorMessage = "Unauthorized. Please check your endpoint URL, username and password." // Set the error message
454 + } else {
455 + this.errorMessage =
456 + "Error updating the connector. Your settings were not inserted into the keystore. Please try again." // Set the error message
457 + }
458 + setTimeout(() => {
459 + this.errorMessage = "" // Clear error message after 5 seconds
460 + }, 5000)
461 + this.getConnectors() // Refresh the connectors
462 + console.error(err) // Also log the error for debugging
463 + })
464 + .finally(() => {
465 + this.loading = false // Set loading to false
466 + })
467 + }
468 + },
469 +
470 + updateConnector(event) {
471 + event.preventDefault()
472 + const { connector_url, username, password, connector_api_key } = this.connectorForm
473 + const path = `http://localhost:5000/connectors/${this.currentConnector.id}`
474 + this.loading = true
475 + this.closeUpdateModal()
476 +
477 + if (connector_api_key) {
478 + console.log("POST request to: ", path)
479 + console.log("Data: ", {
480 + connector_url: connector_url,
481 + connector_api_key: connector_api_key
482 + })
483 +
484 + axios
485 + .put(path, {
486 + connector_url: connector_url,
487 + connector_username: username,
488 + connector_password: password,
489 + connector_api_key: connector_api_key
490 + })
491 + .then(() => {
492 + this.successMessage = "Connector has been successfully updated." // Set success message
493 + setTimeout(() => {
494 + this.successMessage = "" // Clear success message after 5 seconds
495 + }, 5000)
496 + this.getConnectors() // Refresh the connectors
497 + })
498 + .catch(err => {
499 + if (err.response.status === 400) {
500 + this.errorMessage =
501 + "This connector is not configured. If you would like to configure this connector select `Configure`." // Set the error message
502 + } else if (err.response.status === 401) {
503 + this.errorMessage = "Unauthorized. Please check your endpoint URL, username and password." // Set the error message
504 + } else {
505 + this.errorMessage = "Error updating the connector. Please try again." // Set the error message
506 + }
507 + setTimeout(() => {
508 + this.errorMessage = "" // Clear error message after 5 seconds
509 + }, 5000)
510 + this.getConnectors() // Refresh the connectors
511 + console.error(err) // Also log the error for debugging
512 + })
513 + .finally(() => {
514 + this.loading = false // Set loading to false
515 + })
516 + } else {
517 + axios
518 + .put(path, {
519 + connector_url: connector_url,
520 + connector_username: username,
521 + connector_password: password
522 + })
523 + .then(() => {
524 + this.successMessage = "Connector has been successfully updated." // Set success message
525 + setTimeout(() => {
526 + this.successMessage = "" // Clear success message after 5 seconds
527 + }, 5000)
528 + this.getConnectors() // Refresh the connectors
529 + })
530 + .catch(err => {
531 + if (err.response.status === 400) {
532 + this.errorMessage =
533 + "This connector is not configured. If you would like to configure this connector select `Configure`." // Set the error message
534 + } else if (err.response.status === 401) {
535 + this.errorMessage = "Unauthorized. Please check your endpoint URL, username and password." // Set the error message
536 + } else {
537 + this.errorMessage = "Error updating the connector. Please try again." // Set the error message
538 + }
539 + setTimeout(() => {
540 + this.errorMessage = "" // Clear error message after 5 seconds
541 + }, 5000)
542 + this.getConnectors() // Refresh the connectors
543 + console.error(err) // Also log the error for debugging
544 + })
545 + .finally(() => {
546 + this.loading = false // Set loading to false
547 + })
548 + }
549 + },
550 +
551 + getConnectors() {
552 + const path = "http://localhost:5000/connectors"
553 + this.loading = true
554 + axios
555 + .get(path)
556 + .then(res => {
557 + this.connectors = res.data.connectors
558 + })
559 + .catch(err => {
560 + console.error(err)
561 + })
562 + .finally(() => {
563 + this.loading = false
564 + })
565 + },
566 +
567 + resetForm(formName) {
568 + this.$refs[formName].resetFields()
569 + }
570 + },
571 + created() {
572 + this.getConnectors()
573 + }
574 +})
575 +</script>
576 +
577 +<style lang="scss" scoped>
578 +@import "../../assets/scss/_variables";
579 +
580 +.page-table {
581 + padding-left: 20px;
582 + padding-right: 15px;
583 + padding-bottom: 20px;
584 +}
585 +.table-box {
586 + overflow: auto;
587 +}
588 +
589 +.modal-window {
590 + position: fixed;
591 + background-color: rgba(255, 255, 255, 0.25);
592 + top: 0;
593 + right: 0;
594 + bottom: 0;
595 + left: 0;
596 + z-index: 999;
597 + visibility: hidden;
598 + opacity: 0;
599 + pointer-events: none;
600 + transition: all 0.3s;
601 + &.is-active {
602 + visibility: visible;
603 + opacity: 1;
604 + pointer-events: auto;
605 + }
606 + &:target {
607 + visibility: visible;
608 + opacity: 1;
609 + pointer-events: auto;
610 + }
611 + & > div {
612 + width: 400px;
613 + position: absolute;
614 + top: 50%;
615 + left: 50%;
616 + transform: translate(-50%, -50%);
617 + padding: 2em;
618 + background: white;
619 + }
620 + header {
621 + font-weight: bold;
622 + }
623 + h1 {
624 + font-size: 150%;
625 + margin: 0 0 15px;
626 + }
627 +}
628 +
629 +.modal-close {
630 + color: #aaa;
631 + line-height: 50px;
632 + font-size: 80%;
633 + position: absolute;
634 + right: 0;
635 + text-align: center;
636 + top: 0;
637 + width: 70px;
638 + text-decoration: none;
639 + &:hover {
640 + color: black;
641 + }
642 +}
643 +
644 +/* Demo Styles */
645 +
646 +html,
647 +body {
648 + height: 100%;
649 +}
650 +
651 +html {
652 + font-size: 18px;
653 + line-height: 1.4;
654 +}
655 +
656 +body {
657 + font-family: apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
658 + font-weight: 600;
659 + background-image: linear-gradient(to right, #7f53ac 0, #657ced 100%);
660 + color: black;
661 +}
662 +
663 +a {
664 + color: inherit;
665 + text-decoration: none;
666 +}
667 +
668 +.container {
669 + display: grid;
670 + justify-content: center;
671 + align-items: center;
672 + height: 100vh;
673 +}
674 +
675 +.modal-window {
676 + & > div {
677 + border-radius: 1rem;
678 + }
679 +}
680 +
681 +.modal-window div:not(:last-of-type) {
682 + margin-bottom: 15px;
683 +}
684 +
685 +.logo {
686 + max-width: 150px;
687 + display: block;
688 +}
689 +
690 +small {
691 + color: lightgray;
692 +}
693 +
694 +.btn {
695 + background-color: white;
696 + padding: 1em 1.5em;
697 + border-radius: 0.5rem;
698 + text-decoration: none;
699 + i {
700 + padding-right: 0.3em;
701 + }
702 +}
703 +
704 +.modal-card-head {
705 + display: flex;
706 + justify-content: space-between;
707 + align-items: center;
708 +}
709 +
710 +.modal-logo {
711 + height: auto; /* adjust as needed */
712 + width: auto; /* adjust as needed */
713 +}
714 +</style>