diff --git a/app/training_api.py b/app/training_api.py
index 2512852..d2fdd79 100644
--- a/app/training_api.py
+++ b/app/training_api.py
@@ -81,6 +81,11 @@ async def retrieve_flags(self, request):
logging.error(e)
return web.json_response(dict(badges=[b.display for b in badges]))
+ async def retrieve_certs(self, request):
+ access = dict(access=tuple(await self.auth_svc.get_permissions(request)))
+ certifications = await self.data_svc.locate('certifications', match=access)
+ return web.json_response(dict(certificates=[cert.display for cert in certifications]))
+
async def reset_flag(self, request):
"""
Allows cert takers to reset the latest flag if something went wrong with running the automatic operation.
diff --git a/gui/views/training.vue b/gui/views/training.vue
new file mode 100644
index 0000000..83013ad
--- /dev/null
+++ b/gui/views/training.vue
@@ -0,0 +1,531 @@
+
+
+
+#trainingPage.section-profile
+ template(v-if="completedCertificate")
+ canvas#confettiCanvas
+
+ .z-index-1
+ div
+ div
+ h2 Training
+ hr
+
+ form
+ #select-certificate.field.has-addons
+ label.label Select a certificate
+ .control.is-expanded
+ .select.is-small.is-fullwidth
+ select.has-text-centered(v-model="selectedCert")
+ option(disabled selected value="") Select a certificate
+ option(v-for="cert in certificates" :value="cert.name" :key="cert.name") {{ cert.name }}
+
+ template(v-if="completedCertificate")
+ .content.is-flex.is-align-items-center.is-flex-direction-column.mt-4
+ h3 🎉 Certificate complete! 🎉
+ .field.has-addons
+ .control
+ input#certificatecode.input.is-small(type="text" readonly v-model="certificateCode" aria-label="Certificate code")
+ .control
+ a.button.is-small(@click="copyCode()")
+ span.icon
+ font-awesome-icon(icon="far fa-copy")
+ span Copy
+ p Congrats! Fill out the form to validate your code and get a certificate of completion.
+ a.button.fancy-button(href="https://forms.office.com/g/sYRNDuxCjC" target="_blank" rel="noopener") Get your certificate 🎓
+
+ template(v-if="badgeList")
+ .is-flex.is-justify-content-space-evenly.mt-3
+ template(v-for="(badge, index) in badgeList" :key="index")
+ button.badge-container-button(@click="(selectedBadge === badge) ? selectedBadge = '' : selectedBadge = badge" :class="{ 'selected-badge': selectedBadge.name === badge.name }")
+ span.is-flex.is-flex-direction-column.is-justify-content-center.is-align-items-center.p-2
+ span.badge-icon-container(:class="badge.completed ? 'badge-completed' : ''")
+ svg(xmlns="http://www.w3.org/2000/svg" fill="current" viewBox="0 0 24 24" stroke="currentColor")
+ path(stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z")
+ img(:alt="badge.name" class="badge-icon-img" onerror="this.src='/plugin/training/assets/img/badges/defaultlock.png'" :src="badge.icon_src")
+ span.badge-text.hover.bg-caldera-primary.rounded(:class="{ 'badge-completed-text': badge.completed }") {{badge.name}}
+
+ .has-text-centered.mt-4.mb-4
+ template(v-for="(flag, index) in visibleFlagList" :key="index")
+ span.icon
+ font-awesome-icon(:icon="flag.completed ? 'fas fa-flag' : 'far fa-flag'")
+
+ .flag-card-container
+ template(v-for="(flag, index) in visibleFlagList" :key="flag.name")
+ .flag-card.is-flex.is-flex-direction-column.rounded
+ .flag-card-content.is-flex.is-flex-direction-column.overflow-hidden(:class="{ 'flag-card-content-active': isCardActive(index), 'flag-show-more': flag.showMore }")
+ .flag-card-title.is-flex.is-justify-content-space-evenly.is-align-items-center(:class="{ 'flag-card-title-active': flag.completed || isCardActive(index) }")
+ .is-flex.is-justify-content-start.is-align-items-center.flag-card-title-name
+ span.icon(:class="flag.completed ? 'flag-icon-completed' : ''")
+ font-awesome-icon(:icon="flag.completed ? 'fas fa-flag' : 'far fa-flag'")
+ p {{ flag.name }}
+ a.icon.has-tooltip-left.solution-guide-link(:class="flag.has_solution_guide ? '' : 'hidden'" :href="`/plugin/training/solution-guides/certificates/${flag.cert_name}/badges/${flag.badge_name}/flags/${flag.name}`" target="_blank" data-tooltip="Solution Guide")
+ font-awesome-icon(icon="far fa-circle-question")
+ .is-flex.is-justify-content-center.is-align-items-center.flag-card-title-badge
+ span.flag-badge-icon-container
+ svg(xmlns="http://www.w3.org/2000/svg" fill="current" viewBox="0 0 24 24" stroke="currentColor")
+ path(stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z")
+ img(:alt="flag.badge_name" class="badge-icon-img" onerror="this.src='/plugin/training/assets/img/badges/defaultlock.png'" :src="flag.badge_icon")
+ .flag-card-text
+ div
+ .is-flex.is-flex-direction-column.is-justify-content-center.has-text-left
+ p.has-text-weight-bold() {{ flag.challenge }}
+ p {{ flag.extra_info }}
+ template(v-if="flag.code.includes('text-entry')")
+ span
+ label(:for="flag.code") Write text here:
+ input(:disabled="flag.completed" class="text-colors-black pl-1 pr-2" :id="flag.code" placeholder="type here" @input="onTextInput")
+ .flag-show-more-button.is-flex.is-justify-content-center(@click="flag.showMore = !flag.showMore" :class="{ 'flag-show-more-active': isCardActive(index) }")
+ span.icon.is-small
+ font-awesome-icon(:icon="flag.showMore ? 'fas fa-chevron-up' : 'fas fa-chevron-down'")
+
+
+
diff --git a/hook.py b/hook.py
index d809b33..e87c11d 100644
--- a/hook.py
+++ b/hook.py
@@ -21,8 +21,9 @@ async def enable(services):
training_api = TrainingApi(services)
app = services.get('app_svc').application
- app.router.add_static('/training', 'plugins/training/static/', append_version=True)
+ app.router.add_static('/plugin/training/assets', 'plugins/training/static/', append_version=True)
app.router.add_route('GET', '/plugin/training/gui', training_api.splash)
+ app.router.add_route('GET', '/plugin/training/certs', training_api.retrieve_certs)
app.router.add_route('POST', '/plugin/training/flags', training_api.retrieve_flags)
app.router.add_route('POST', '/plugin/training/reset_flag', training_api.reset_flag)