Compare commits

...
This repository has been archived on 2025-06-10. You can view files and clone it, but you cannot make any changes to it's state, such as pushing and creating new issues, pull requests or comments.

2176 commits

Author SHA1 Message Date
ac5982f905 fileexporter: Close opened fd
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-07 14:01:12 +02:00
961542283d fileexporter: Include standalone exercices
All checks were successful
continuous-integration/drone/push Build is passing
2025-04-07 11:23:37 +02:00
7f38911bbb Introducing fileexporter to create archive from git or other importer 2025-04-07 11:07:14 +02:00
c2996b9f0a repochecker: Use SetWriteFileFunc to avoid writing any file on disk 2025-04-07 10:29:06 +02:00
8723f500cc sync: Markdown imports files using generic functions 2025-04-07 10:16:30 +02:00
b55151623c sync: resizePicture uses image from importer instead of local file 2025-04-07 10:16:03 +02:00
c7d1d7ce4c sync: Refactor importFile to use a parametrable writer 2025-04-07 10:15:26 +02:00
c5d0616896 sync: Split SyncFiles function into import and files sync 2025-04-07 10:14:13 +02:00
e6f6686a39 admin: Fix team stats
All checks were successful
continuous-integration/drone/push Build is passing
2025-04-01 12:47:58 +02:00
56efb4ae94 sync: Fix non-trimed git links
Some checks failed
continuous-integration/drone/tag Build is failing
2025-03-31 15:43:56 +02:00
7d775fe26d admin: New page to list forge link per theme and exercice 2025-03-31 15:42:07 +02:00
b713eba2a5 chore(deps): lock file maintenance
All checks were successful
continuous-integration/drone/push Build is passing
2025-03-31 02:31:11 +00:00
f3641a7c8f List some features in README 2025-03-30 16:26:41 +02:00
21752d1ca2 admin: Import from cyberrange handles UUID 2025-03-30 15:44:48 +02:00
f6713c768b admin: Better identify tries on exercice page
All checks were successful
continuous-integration/drone/push Build is passing
2025-03-30 15:20:11 +02:00
38e3a4efdf admin: Obtain full current gains from a solved exercice 2025-03-30 13:46:10 +02:00
9f25bc54d3 Round score instead of floor + display score100 to player 2025-03-30 13:31:52 +02:00
a0cb395c79 frontend: Fix standalone exercices not showing before challenge start
All checks were successful
continuous-integration/drone/push Build is passing
2025-03-30 11:43:57 +02:00
532b3eccdc frontend: Round numbers in rules
Some checks reported errors
continuous-integration/drone/push Build was killed
2025-03-29 22:37:08 +01:00
08afde34a8 admin: Fix OAuth settings display
All checks were successful
continuous-integration/drone/push Build is passing
2025-03-29 19:41:42 +01:00
3c31a9d4b4 frontend: Fix flag saving to local storage 2025-03-29 19:33:18 +01:00
7930391ac0 admin: Standalone exercices are present twice 2025-03-29 19:20:03 +01:00
bc94d0c649 admin: Download file only if file is not present locally 2025-03-29 19:19:29 +01:00
fbc84f9d08 admin: Fix dex template 2025-03-29 17:50:52 +01:00
4973f7ac4a frontend: Highlight files not downloaded
All checks were successful
continuous-integration/drone/push Build is passing
2025-03-29 14:45:06 +01:00
0f2dafa3b1 frontend: Save submissions to display in interface later 2025-03-29 14:34:58 +01:00
404f29e6ea frontend: Visual improvements on themes details page
Some checks are pending
continuous-integration/drone/push Build is running
2025-03-29 13:04:25 +01:00
7da127ecb0 frontend: Improve team registration processus 2025-03-29 12:46:16 +01:00
f05664e2e3 frontend: Reset current theme when going to tag page 2025-03-29 12:17:03 +01:00
eaca60e5e0 frontend: Normalize tags 2025-03-29 12:14:41 +01:00
42b9e54ec7 frontend: Avoid relative paths 2025-03-29 12:14:16 +01:00
698e69d132 frontend: Fix indexes of tags 2025-03-29 12:02:44 +01:00
7af23ed297 frontend: Remove text indentation on cards 2025-03-29 11:54:35 +01:00
ac5772008b fix warning Docker build 2025-03-29 10:46:48 +01:00
1ec71728de go vet
All checks were successful
continuous-integration/drone/push Build is passing
2025-03-28 20:17:11 +01:00
24fa72eb8a admin: Fix bad type assertion in history 2025-03-28 19:56:34 +01:00
0edf71107a admin: Improve coeff inputs
Some checks failed
continuous-integration/drone/push Build is failing
2025-03-28 19:39:15 +01:00
f841d9c11c frontend: Mark bad submissions as invalid 2025-03-28 19:39:15 +01:00
bf2be00f15 Indicate flag order in grid-score 2025-03-28 19:39:15 +01:00
71120c1c89 frontend: Improve rules 2025-03-28 19:39:15 +01:00
5ba86d0c5f admin: Refactor rank query by extracting optional query parts 2025-03-28 19:39:15 +01:00
8e196136c3 admin: Can gain points for each question answered // partial exercice solved 2025-03-28 19:39:15 +01:00
4ca2bc106a admin: Add doc around settings 2025-03-28 19:39:15 +01:00
74f388a2b9 admin: Check all theme/exercice attribute are in sync with repo 2025-03-28 19:39:15 +01:00
5e262b75a3 admin: Can list independant exercices as theme 2025-03-28 19:39:15 +01:00
d26333c5e2 chore(deps): update module github.com/go-sql-driver/mysql to v1.9.1
Some checks are pending
continuous-integration/drone/push Build is running
2025-03-28 18:38:58 +00:00
cb0e0e2c24 chore(deps): update module golang.org/x/oauth2 to v0.28.0
Some checks reported errors
continuous-integration/drone/push Build was killed
2025-03-28 10:07:14 +00:00
6100f33e7c chore(deps): update module golang.org/x/image to v0.25.0
All checks were successful
continuous-integration/drone/push Build is passing
2025-03-28 08:26:08 +00:00
47f2004a4c chore(deps): update dependency eslint-plugin-svelte to v3
Some checks are pending
continuous-integration/drone/push Build is running
2025-03-28 08:25:55 +00:00
faf74ec808 chore(deps): update module github.com/go-git/go-git/v5 to v5.14.0
Some checks are pending
continuous-integration/drone/push Build is running
2025-03-27 22:22:56 +00:00
e42545416f chore(deps): update module golang.org/x/crypto to v0.36.0
Some checks reported errors
continuous-integration/drone/push Build was killed
2025-03-27 16:08:13 +00:00
3f5d6bb04b chore(deps): update module github.com/burntsushi/toml to v1.5.0
Some checks reported errors
continuous-integration/drone/push Build was killed
2025-03-27 12:06:30 +00:00
99c436ba9a Keep repochecker on 3.19 (needed for grammalecte)
All checks were successful
continuous-integration/drone/push Build is passing
2025-03-27 12:28:48 +01:00
a7d521fbdd admin: Can change capture_regexp along with key 2025-03-27 12:28:16 +01:00
0c53372618 admin: Implement .gz file download test 2025-03-27 12:28:16 +01:00
84be750ce6 admin: circle animation no more block click on refresh button 2025-03-27 12:28:16 +01:00
3881385c9e admin: List all existing association between users and teams 2025-03-27 12:28:16 +01:00
e44cac32ac admin: New button to refine teams colors 2025-03-27 12:28:16 +01:00
485e6b0173 admin: Able to import Cyberrange teams from interface 2025-03-27 12:28:16 +01:00
f1ada8ce99 admin: Use logo from challengeinfo in ui template 2025-03-27 12:28:16 +01:00
7e301b8ecb admin: Replace PKI page by authentication settings, refactor 2025-03-27 12:28:16 +01:00
4dcf1218d8 chore(deps): update module github.com/asticode/go-astisub to v0.34.0
Some checks are pending
continuous-integration/drone/push Build is running
2025-03-27 11:27:30 +00:00
3f5b7b9ed7 chore(deps): update alpine docker tag to v3.21
Some checks are pending
continuous-integration/drone/push Build is running
2025-03-27 11:27:16 +00:00
bc0570c2c7 chore(deps): update dependency eslint-config-prettier to v10.1.1
Some checks are pending
continuous-integration/drone/push Build is running
2025-03-27 11:27:00 +00:00
cd50a4b9d3 chore(deps): update dependency vite to v5.4.15
Some checks are pending
continuous-integration/drone/push Build is running
2025-03-27 11:26:33 +00:00
4734a8f047 chore(deps): update dependency sass-loader to v16.0.5
Some checks are pending
continuous-integration/drone/push Build is running
2025-03-27 11:26:18 +00:00
dadb84e8f9 admin: Dex config contains challenge name instead of hardcoded name
All checks were successful
continuous-integration/drone/push Build is passing
2025-03-26 12:04:43 +01:00
801042e5cf fickit: Update linuxkit images 2025-03-26 11:55:36 +01:00
fca27b07fe admin: Also import team members from CyberRange 2025-03-26 11:25:06 +01:00
3fc765ccfa admin: Export logos present in challenge.json 2025-03-26 11:13:09 +01:00
590a55c395 libfic: Create a color randomization function 2025-03-25 18:54:36 +01:00
b62369f89f admin: New route to import teams from CyberRange format 2025-03-25 18:19:22 +01:00
cb4ceecbf5 challenge-sync-airbus: Refactor and prefer calling it cyberrange 2025-03-25 18:02:11 +01:00
98d9f2daf3 Keep repochecker on 3.19 (needed for grammalecte) 2025-03-25 12:04:09 +01:00
db1e2603fc chore(deps): update alpine docker tag to v3.21 2025-03-25 12:04:09 +01:00
0730a22daa chore(deps): update dependency @sveltejs/kit to v2.20.2 2025-03-25 12:04:09 +01:00
3467ca6db5 chore(deps): update dependency sass to v1.86.0 2025-03-25 12:04:09 +01:00
910adb123a chore(deps): update dependency prettier to v3.5.3 2025-03-25 12:04:09 +01:00
1551c11a00 chore(deps): update dependency eslint to v9.23.0 2025-03-25 12:04:09 +01:00
ed3e6b66de chore(deps): update dependency @sveltestrap/sveltestrap to v7.1.0 2025-03-25 12:04:09 +01:00
c21fd098a0 Remove useless file 2025-03-25 12:04:09 +01:00
7df675346c challenge-sync-airbus: 2025 API ready
All checks were successful
continuous-integration/drone/push Build is passing
2025-03-24 19:31:59 +01:00
526d693ffd chore(deps): update module golang.org/x/crypto to v0.33.0
Some checks reported errors
continuous-integration/drone/push Build was killed
2025-02-11 16:18:38 +00:00
df8a759134 chore(deps): update dependency sass to v1.84.0
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-11 12:56:59 +00:00
03e48b749c chore(deps): update dependency @sveltejs/kit to v2.17.1
Some checks are pending
continuous-integration/drone/push Build is pending
2025-02-11 12:56:55 +00:00
97f7e3fa59 chore(deps): update dependency @sveltestrap/sveltestrap to v7
Some checks are pending
continuous-integration/drone/push Build is pending
2025-02-11 12:56:18 +00:00
e421c91ac2 chore(deps): update module github.com/go-git/go-git/v5 to v5.13.2
Some checks are pending
continuous-integration/drone/push Build is pending
2025-02-11 12:56:05 +00:00
baccc54d02 chore(deps): update module golang.org/x/oauth2 to v0.26.0
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-05 02:55:02 +00:00
e8e87c9958 chore(deps): update module golang.org/x/image to v0.24.0
Some checks reported errors
continuous-integration/drone/push Build was killed
2025-02-05 00:42:32 +00:00
08a31898df admin: New button to delete tries for a flag
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-04 19:08:44 +01:00
b409fa6806 admin: Retrieve stats on exercices 2025-02-04 19:08:44 +01:00
63b4cdc622 admin: Use non-breakable whitespaces 2025-02-04 19:08:44 +01:00
650f933993 admin: duration change impact the expected end 2025-02-04 19:08:44 +01:00
603b226955 fickit: Prepare team registration through checker 2025-02-04 19:08:44 +01:00
55e829fa64 fickit: Allow admin to remove submissions 2025-02-04 19:08:44 +01:00
45a0504c44 chore(deps): lock file maintenance
All checks were successful
continuous-integration/drone/push Build is passing
2025-01-27 02:27:27 +00:00
ad7489e558 admin: Start compute flag stats
All checks were successful
continuous-integration/drone/push Build is passing
2025-01-25 14:46:47 +01:00
57c3cd8fd6 admin: Fix mcq entry update 2025-01-24 23:49:37 +01:00
24686a6a24 admin: Fix check file on disk for compressed files 2025-01-24 23:49:37 +01:00
407b67f4c2 sync: Ensure placeholder and raw are not the same 2025-01-24 23:49:37 +01:00
c28d974105 fickit: Update images 2025-01-24 23:49:37 +01:00
ffb69663b6 fickit: Initiate sshd config with keys on first run 2025-01-24 23:49:37 +01:00
4ec4f47951 fickit: keep last metadata iso when dm-crypt key change 2025-01-24 23:49:37 +01:00
96707e3a29 configs: Detect mkisofs 2025-01-24 23:49:37 +01:00
a4001759f6 ui: Fix file disclaimer not showing 2025-01-24 23:49:37 +01:00
f15cd29f78 chore(deps): update dependency @sveltestrap/sveltestrap to v6.2.8
All checks were successful
continuous-integration/drone/push Build is passing
2025-01-23 18:41:07 +00:00
7e41ddd664 chore(deps): update dependency eslint-config-prettier to v10
Some checks are pending
continuous-integration/drone/push Build is running
2025-01-23 18:40:58 +00:00
89b7710544 chore(deps): update dependency @sveltejs/kit to v2.16.1
Some checks are pending
continuous-integration/drone/push Build is running
2025-01-23 18:40:43 +00:00
79ec20d11c chore(deps): update dependency sass to v1.83.4
Some checks reported errors
continuous-integration/drone/push Build was killed
2025-01-21 14:57:58 +00:00
09206df20a chore(deps): update module golang.org/x/crypto to v0.32.0
All checks were successful
continuous-integration/drone/push Build is passing
2025-01-18 12:40:53 +00:00
a14c151b04 admin: Implement button to delete the entire FILES dir
Some checks failed
continuous-integration/drone/push Build is failing
2025-01-14 16:03:37 +01:00
68dad00930 ui: Force download of XML files 2025-01-14 16:03:37 +01:00
c74eadc801 admin: Also fill lastSyncError in autosync 2025-01-14 16:03:37 +01:00
Leo Blanc Di Pasquale
376e112130 Update authorized_keys 2025-01-14 16:00:45 +01:00
Maxence Michot
1d2a09f612 feat: added maxence.michot to the authorized_keys 2025-01-14 16:00:45 +01:00
Alexandra Delin
6a35bd6345 Update authorized_keys 2025-01-14 16:00:45 +01:00
Hugo Rubio
a463e88a90 Update authorized_keys 2025-01-14 16:00:45 +01:00
Victor Chartraire
3d066fbdeb Update authorized_keys 2025-01-14 16:00:45 +01:00
liryc116
7dd3f64a08 chore: added personnal authorized key 2025-01-14 16:00:45 +01:00
bd5050b24a admin: Fix missing replacement 2025-01-14 10:58:17 +01:00
0ab453811c qa: Bump version
All checks were successful
continuous-integration/drone/push Build is passing
2025-01-14 10:51:11 +01:00
e71dc24a27 qa: Improve work assignation
Some checks are pending
continuous-integration/drone/push Build is running
2025-01-14 10:45:01 +01:00
fb5147fac3 repochecker: Rely on archive.org to find grammalecte 2025-01-14 10:45:01 +01:00
f073ea0fd0 fickit: Update images
Some checks failed
continuous-integration/drone/push Build is failing
2025-01-13 22:36:26 +01:00
34cf1789f3 frontend: Sync lock file 2025-01-13 22:21:55 +01:00
9e75386038 qa: New to delete assigned work
Some checks failed
continuous-integration/drone/push Build is failing
2025-01-13 20:21:03 +01:00
a1b0e6a79b qa: Add logs to gitlab export 2025-01-13 20:21:03 +01:00
092d2256f7 qa: Improve manager dashboard 2025-01-13 20:21:03 +01:00
28b4e7e529 ui: Improve dev by specifying hmr port 2025-01-13 20:21:03 +01:00
32632322d4 qa: Update the reverse proxy 2025-01-13 20:21:03 +01:00
4473166ee7 qa: Try to fix GitLab connection return on live infra 2025-01-13 20:21:03 +01:00
12feb91d48 qa: Update lock file 2025-01-13 20:21:03 +01:00
03d02669ea admin: Refactor synchronization status report + display last git error 2025-01-13 20:21:03 +01:00
c1924c0e92 admin: Can delete a repository directory if needed 2025-01-13 20:21:03 +01:00
7692f92aa4 Readd disclaimer to my.json 2025-01-13 20:21:03 +01:00
724f985770 chore(deps): update module golang.org/x/oauth2 to v0.25.0 2025-01-04 16:38:23 +00:00
fa1b21e49f chore(deps): update module github.com/go-git/go-git/v5 to v5.13.1
Some checks failed
continuous-integration/drone/push Build is failing
2025-01-04 16:13:40 +00:00
6b9283f7ca chore(deps): update dependency @sveltejs/kit to v2.15.1
Some checks are pending
continuous-integration/drone/push Build is running
2025-01-04 16:13:27 +00:00
2a0a0dc9d4 chore(deps): update dependency sass to v1.83.1
Some checks are pending
continuous-integration/drone/push Build is running
2025-01-04 11:40:04 +00:00
ad6e59d8eb chore(deps): update dependency @sveltejs/adapter-static to v3.0.8
Some checks failed
continuous-integration/drone/push Build is failing
2025-01-04 11:09:14 +00:00
6a1120898b chore(deps): update module github.com/gin-contrib/sessions to v1.0.2
Some checks are pending
continuous-integration/drone/push Build is pending
2025-01-04 11:08:58 +00:00
e178f7a80f chore(deps): update module github.com/asticode/go-astisub to v0.32.0
Some checks are pending
continuous-integration/drone/push Build is pending
2025-01-04 11:08:33 +00:00
895e34fef4 chore(deps): lock file maintenance
Some checks are pending
continuous-integration/drone/push Build is pending
2024-12-17 13:59:31 +00:00
148deb77ec chore(deps): update module github.com/studio-b12/gowebdav to v0.10.0
All checks were successful
continuous-integration/drone/push Build is passing
2024-12-17 11:52:22 +00:00
5321d499b2 chore(deps): lock file maintenance
All checks were successful
continuous-integration/drone/push Build is passing
2024-12-16 00:47:57 +00:00
f6d2794fbd chore(deps): lock file maintenance
All checks were successful
continuous-integration/drone/push Build is passing
2024-12-11 19:52:19 +00:00
ba37309ee5 chore(deps): update module golang.org/x/crypto to v0.31.0
All checks were successful
continuous-integration/drone/push Build is passing
2024-12-11 18:41:38 +00:00
6dfe4115c3 chore(deps): lock file maintenance
All checks were successful
continuous-integration/drone/push Build is passing
2024-12-09 00:49:21 +00:00
0296c9cc85 chore(deps): update module golang.org/x/image to v0.23.0
Some checks reported errors
continuous-integration/drone/push Build was killed
2024-12-06 16:57:48 +00:00
180fcbfa33 chore(deps): update module golang.org/x/crypto to v0.30.0
Some checks reported errors
continuous-integration/drone/push Build was killed
2024-12-05 21:40:59 +00:00
5168d875d5 chore(deps): update dependency prettier-plugin-svelte to v3.3.2
All checks were successful
continuous-integration/drone/push Build is passing
2024-12-05 17:48:07 +00:00
53095d40a9 chore(deps): update dependency hash-wasm to v4.12.0
Some checks are pending
continuous-integration/drone/push Build is pending
2024-12-05 17:47:55 +00:00
a15532f8b8 chore(deps): update node docker tag to v23
Some checks are pending
continuous-integration/drone/push Build is pending
2024-12-05 17:47:27 +00:00
70882779e8 chore(deps): update dependency @sveltejs/adapter-static to v3.0.6
Some checks are pending
continuous-integration/drone/push Build is pending
2024-12-05 17:47:14 +00:00
c97897f2a4 chore(deps): update dependency wordcloud to v1.2.3
Some checks are pending
continuous-integration/drone/push Build is pending
2024-12-05 17:47:04 +00:00
d955dc1b3b chore(deps): update dependency sass-loader to v16.0.4
Some checks are pending
continuous-integration/drone/push Build is pending
2024-12-05 17:46:54 +00:00
2597bf2a14 chore(deps): update module github.com/asticode/go-astisub to v0.30.0
Some checks are pending
continuous-integration/drone/push Build is pending
2024-12-05 17:46:46 +00:00
681fb44462 chore(deps): update dependency @sveltejs/kit to v2.9.0
Some checks are pending
continuous-integration/drone/push Build is pending
2024-11-29 18:44:03 +00:00
be6448971b chore(deps): update module golang.org/x/oauth2 to v0.24.0
All checks were successful
continuous-integration/drone/push Build is passing
2024-11-22 16:58:49 +00:00
24dd190299 chore(deps): update module golang.org/x/image to v0.22.0
All checks were successful
continuous-integration/drone/push Build is passing
2024-11-22 15:06:42 +00:00
d14936fd29 chore(deps): update module golang.org/x/crypto to v0.29.0
All checks were successful
continuous-integration/drone/push Build is passing
2024-11-22 02:42:08 +00:00
f5e2d91c1b chore(deps): update dependency eslint to v9.15.0
All checks were successful
continuous-integration/drone/push Build is passing
2024-11-21 09:00:30 +00:00
8a91d4b7fa chore(deps): update dependency sass to v1.81.0
Some checks are pending
continuous-integration/drone/push Build is pending
2024-11-21 09:00:19 +00:00
07043bd692 chore(deps): update dependency @sveltejs/kit to v2.8.1
Some checks are pending
continuous-integration/drone/push Build is pending
2024-11-21 09:00:11 +00:00
629b450f89 chore(deps): update dependency vite to v5.4.11
Some checks are pending
continuous-integration/drone/push Build is pending
2024-11-21 08:59:47 +00:00
696cbae7fa chore(deps): update module github.com/yuin/goldmark to v1.7.8
Some checks are pending
continuous-integration/drone/push Build is pending
2024-11-21 08:59:42 +00:00
0eb50e0cbc chore(deps): update dependency eslint-plugin-svelte to v2.46.0
Some checks are pending
continuous-integration/drone/push Build is pending
2024-11-21 08:59:19 +00:00
4265d6ab92 chore(deps): update module github.com/asticode/go-astisub to v0.29.0
Some checks are pending
continuous-integration/drone/push Build is pending
2024-11-21 08:58:44 +00:00
0a8a36d73d chore(deps): lock file maintenance
Some checks reported errors
continuous-integration/drone/push Build was killed
2024-10-14 01:57:35 +00:00
ea8ad1d6db sync: Don't warn about no flag if WIP
All checks were successful
continuous-integration/drone/push Build is passing
2024-10-11 14:55:49 +02:00
e08dd2f2e8 sync: Allow empty files 2024-10-11 14:55:28 +02:00
ac8f704062 chore(deps): lock file maintenance
All checks were successful
continuous-integration/drone/push Build is passing
2024-10-07 00:54:26 +00:00
371eb2fe68 chore(deps): update module golang.org/x/crypto to v0.28.0
Some checks reported errors
continuous-integration/drone/push Build was killed
2024-10-04 17:50:41 +00:00
b2200ad8f2 chore(deps): update module golang.org/x/image to v0.21.0
Some checks failed
continuous-integration/drone/push Build is failing
2024-10-04 16:38:30 +00:00
5b4514a254 chore(deps): lock file maintenance
All checks were successful
continuous-integration/drone/push Build is passing
2024-09-30 00:08:08 +00:00
005c13196b chore(deps): update module github.com/asticode/go-astisub to v0.27.0
Some checks reported errors
continuous-integration/drone/push Build was killed
2024-09-24 11:51:41 +00:00
60170390ca chore(deps): update dependency eslint to v9.11.1
Some checks are pending
continuous-integration/drone/push Build is running
2024-09-24 11:51:31 +00:00
626a5b0981 Revert "chore(deps): update alpine docker tag to v3.20"
All checks were successful
continuous-integration/drone/push Build is passing
This reverts commit 8802fe80d7.
2024-09-24 13:51:13 +02:00
106fddeb2e chore(deps): lock file maintenance
Some checks failed
continuous-integration/drone/push Build is failing
2024-09-23 13:54:00 +00:00
0c61fa29cd chore(deps): lock file maintenance
Some checks failed
continuous-integration/drone/push Build is failing
2024-09-23 15:15:19 +02:00
83cdee759a chore(deps): lock file maintenance 2024-09-23 15:15:19 +02:00
1adb1807b5 admin: Fix add to delegated QA manager 2024-09-23 15:14:05 +02:00
0f9d56fcbf qa: Refactor work attribution 2024-09-23 15:11:42 +02:00
8c6db30c52 ui: Fix display of sentence 2024-09-23 15:11:42 +02:00
Victor Chartraire
3316375cbb fix: Do not remove every delegated_QA on dropDelegatedQA 2024-09-18 18:55:50 +02:00
3d2606ab9c qa: Fix challenge access calculation 2024-09-18 15:18:10 +02:00
2f6c7ecd8b qa: New route to assign all exercices 2024-09-18 12:32:11 +02:00
1f295c3411 qa: Fix display for standalone exercices 2024-09-18 12:31:58 +02:00
0bf367bd3b qa: Handle single theme review 2024-09-18 11:55:13 +02:00
a82e3642a8 qa: Use GetThemesExtended to include standalones exercices 2024-09-18 11:54:52 +02:00
38fa6ec1de qa: Arrange team color 2024-09-18 11:38:41 +02:00
051d62a5fa qa: Add pointer on clickable rows 2024-09-18 11:38:25 +02:00
4c3b07db1e qa: Fix team color 2024-09-18 11:34:01 +02:00
c293b58a94 qa: Handle standalones exercices 2024-09-18 11:33:45 +02:00
a630075116 qa: Don't fail if no scenario + don't show menu item 2024-09-18 11:16:44 +02:00
7ae1517a59 qa: repositories page moved to admin 2024-09-18 11:14:25 +02:00
5a4960f9ad ui: Redraw the whole exercices when tag changes 2024-09-18 11:00:03 +02:00
0669f74395 ui: Make others menu items active when on respective page 2024-09-18 10:56:25 +02:00
5981240280 ui: Make menu tags active on tags pages 2024-09-18 10:47:59 +02:00
180ec5e29d Don't display Scenarii menu if no scenario 2024-09-18 10:47:34 +02:00
3d1b318091 dashboard: Handle standalone exercices 2024-09-18 09:08:56 +02:00
c8f70c48fa qa: Make gitlabBaseURL configurable through env 2024-09-18 08:39:41 +02:00
81958ef4b9 Use non-versionned alpine images
Some checks reported errors
continuous-integration/drone/push Build was killed
2024-09-13 12:09:15 +02:00
8bb8cb18e3 If there is no themes, display all exercices
Some checks are pending
continuous-integration/drone/push Build is running
2024-09-13 12:06:48 +02:00
caae846bc7 chore(deps): lock file maintenance
Some checks reported errors
continuous-integration/drone/push Build was killed
2024-09-09 00:49:39 +00:00
bf71a40f49 chore(deps): update module golang.org/x/crypto to v0.27.0
All checks were successful
continuous-integration/drone/push Build is passing
2024-09-05 17:43:46 +00:00
e86e50fcd0 chore(deps): update module golang.org/x/image to v0.20.0
Some checks reported errors
continuous-integration/drone/push Build was killed
2024-09-04 16:55:31 +00:00
bd901abf56 chore(deps): update module golang.org/x/oauth2 to v0.23.0
All checks were successful
continuous-integration/drone/push Build is passing
2024-09-04 15:44:02 +00:00
6cbfea1494 chore(deps): lock file maintenance
All checks were successful
continuous-integration/drone/push Build is passing
2024-09-03 14:15:47 +00:00
8802fe80d7 chore(deps): update alpine docker tag to v3.20
Some checks reported errors
continuous-integration/drone/push Build was killed
2024-09-03 14:15:37 +00:00
cf623c7a47 evdist requires DASHBOARD directory
All checks were successful
continuous-integration/drone/push Build is passing
2024-09-03 15:28:24 +02:00
ba9a0aee42 Fix nginx config with standalone exercices access and _app discovery 2024-09-03 15:28:23 +02:00
c2c138b6e3 chore(deps): lock file maintenance
Some checks reported errors
continuous-integration/drone/push Build was killed
2024-08-26 00:50:30 +00:00
46cdd304e8 chore(deps): update module golang.org/x/image to v0.19.0
All checks were successful
continuous-integration/drone/push Build is passing
2024-08-19 19:56:01 +00:00
86b0ff1669 chore(deps): update module golang.org/x/crypto to v0.26.0
All checks were successful
continuous-integration/drone/push Build is passing
2024-08-19 07:38:43 +00:00
517cfdd4d2 chore(deps): lock file maintenance
All checks were successful
continuous-integration/drone/push Build is passing
2024-08-19 00:45:31 +00:00
88cc25e8a2 chore(deps): update dependency eslint to v9.9.0
Some checks failed
continuous-integration/drone/push Build is failing
2024-08-18 22:26:48 +00:00
65435ceeb4 chore(deps): update dependency sass-loader to v16
Some checks are pending
continuous-integration/drone/push Build is running
2024-08-18 22:26:24 +00:00
84b5702da8 chore(deps): update dependency @sveltejs/kit to v2.5.22
Some checks are pending
continuous-integration/drone/push Build is running
2024-08-18 22:25:47 +00:00
8f14814335 chore(deps): update module golang.org/x/oauth2 to v0.22.0
Some checks are pending
continuous-integration/drone/push Build is running
2024-08-18 22:25:29 +00:00
aaad54d643 chore(deps): update ghcr.io/dexidp/dex docker tag to v2.41.1
Some checks are pending
continuous-integration/drone/push Build is running
2024-08-18 22:25:03 +00:00
c26617607d chore(deps): update dependency @sveltejs/adapter-static to v3.0.4
Some checks are pending
continuous-integration/drone/push Build is running
2024-08-18 22:24:41 +00:00
cc552db7cf chore(deps): update dependency vite to v5.4.1
Some checks are pending
continuous-integration/drone/push Build is running
2024-08-18 22:24:22 +00:00
b828b26162 chore(deps): update module github.com/yuin/goldmark to v1.7.4
Some checks are pending
continuous-integration/drone/push Build is pending
2024-08-18 22:23:59 +00:00
e64acba944 chore(deps): update module github.com/cenkalti/dominantcolor to v1.0.3
Some checks are pending
continuous-integration/drone/push Build is running
2024-08-18 22:23:19 +00:00
9105285235 chore(deps): lock file maintenance
Some checks reported errors
continuous-integration/drone/push Build was killed
2024-07-22 00:43:59 +00:00
4c93a94334 chore(deps): lock file maintenance
All checks were successful
continuous-integration/drone/push Build is passing
2024-07-15 00:44:28 +00:00
3d221f3ab2 chore(deps): lock file maintenance
All checks were successful
continuous-integration/drone/push Build is passing
2024-07-05 15:38:11 +00:00
c39e6a6ef4 chore(deps): update module golang.org/x/crypto to v0.25.0
Some checks reported errors
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build was killed
2024-07-05 14:34:39 +00:00
93ac43183d chore(deps): lock file maintenance
Some checks reported errors
continuous-integration/drone/push Build was killed
2024-07-01 00:48:14 +00:00
a13c055bf3 chore(deps): lock file maintenance
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-25 21:40:51 +00:00
34c11bf731 chore(deps): update module golang.org/x/image to v0.18.0
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-25 20:36:50 +00:00
afde4acc2c chore(deps): lock file maintenance
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-24 00:39:04 +00:00
914698a38c chore(deps): update dependency eslint-plugin-svelte to v2.40.0
Some checks reported errors
continuous-integration/drone/push Build was killed
2024-06-19 11:38:09 +00:00
e68fd1c0d8 chore(deps): update dependency @sveltejs/adapter-static to v3.0.2
Some checks are pending
continuous-integration/drone/push Build is running
2024-06-19 11:37:51 +00:00
462dffe9ab chore(deps): update dependency eslint to v9
Some checks are pending
continuous-integration/drone/push Build is running
2024-06-19 11:37:42 +00:00
0eb6934474 chore(deps): update ghcr.io/dexidp/dex docker tag to v2.40.0
Some checks are pending
continuous-integration/drone/push Build is running
2024-06-19 11:36:53 +00:00
efb374a573 chore(deps): update node docker tag to v22
Some checks are pending
continuous-integration/drone/push Build is running
2024-06-19 11:36:21 +00:00
3052e2af6d chore(deps): lock file maintenance
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-10 00:41:09 +00:00
7f62fd7205 Keep repochecker on alpine 3.19 as grammalecte doesn't support python 3.12
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-07 19:07:49 +02:00
1796ecc25e chore(deps): update module golang.org/x/crypto to v0.24.0
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-04 19:33:13 +00:00
f21dc21193 chore(deps): update module golang.org/x/image to v0.17.0
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-04 17:32:29 +00:00
1204b9df18 chore(deps): update module golang.org/x/oauth2 to v0.21.0
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-04 16:27:19 +00:00
db0e49a8cb chore(deps): lock file maintenance
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-27 00:25:13 +00:00
43ffc36557 chore(deps): update module golang.org/x/oauth2 to v0.20.0
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-24 15:20:59 +00:00
125662bc8c chore(deps): update module golang.org/x/image to v0.16.0
Some checks are pending
continuous-integration/drone/push Build is running
2024-05-24 14:18:16 +00:00
3393122dbc chore(deps): update module github.com/gin-gonic/gin to v1.10.0
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-24 13:35:08 +00:00
a8b8a4cc31 chore(deps): update module github.com/burntsushi/toml to v1.4.0
Some checks are pending
continuous-integration/drone/push Build is running
2024-05-24 13:18:23 +00:00
9ee910514e chore(deps): update dependency eslint-plugin-svelte to v2.39.0
Some checks are pending
continuous-integration/drone/push Build is running
2024-05-24 12:55:10 +00:00
ea7bfee5ea chore(deps): update dependency sass to v1.77.2
Some checks are pending
continuous-integration/drone/push Build is running
2024-05-24 12:54:51 +00:00
40b9c713dc chore(deps): update alpine docker tag to v3.20
Some checks are pending
continuous-integration/drone/push Build is running
2024-05-24 12:54:35 +00:00
08b27eeba9 chore(deps): update module github.com/gin-contrib/sessions to v1.0.1
Some checks are pending
continuous-integration/drone/push Build is running
2024-05-24 12:54:08 +00:00
ed478eaf67 chore(deps): update dependency sass-loader to v14.2.1
Some checks are pending
continuous-integration/drone/push Build is running
2024-05-24 12:53:45 +00:00
49239045e2 chore(deps): update dependency @sveltejs/kit to v2.5.10
Some checks are pending
continuous-integration/drone/push Build is running
2024-05-24 12:53:32 +00:00
91c7ef9785 chore(deps): update dependency svelte to v4.2.17
Some checks are pending
continuous-integration/drone/push Build is running
2024-05-24 12:53:17 +00:00
3e828f9bb7 chore(deps): update dependency vite to v5.2.11
Some checks are pending
continuous-integration/drone/push Build is running
2024-05-24 12:52:58 +00:00
8bbd0e643b chore(deps): update module github.com/go-git/go-git/v5 to v5.12.0
Some checks are pending
continuous-integration/drone/push Build is running
2024-05-24 09:21:33 +00:00
e2db847f70 ci: Remove -v
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-24 10:52:14 +02:00
0b7fa570db chore(deps): update ghcr.io/dexidp/dex docker tag to v2.39.1
Some checks are pending
continuous-integration/drone/push Build is running
2024-05-24 10:51:05 +02:00
f0a253245d chore(deps): update module github.com/yuin/goldmark to v1.7.1 2024-05-24 10:51:05 +02:00
83778129d3 chore(deps): update module github.com/go-sql-driver/mysql to v1.8.1 2024-05-24 10:51:05 +02:00
3c457015eb repochecker: Remove indication on how to circumvent forbidden-string 2024-05-24 10:51:05 +02:00
eeced21be8 sync: Allow justified as flag type
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-18 00:28:59 +02:00
651d428223 sync: Prefer challenge.toml over challenge.txt
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-16 13:09:13 +02:00
b5065df4c3 ui: Prepare publication 2024-05-16 13:09:13 +02:00
5ece912ec9 ui: Remove old content 2024-05-16 13:09:13 +02:00
77cdfdb355 chore(deps): lock file maintenance
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-19 11:49:00 +02:00
0b45855842 chore(deps): update module golang.org/x/oauth2 to v0.19.0 2024-04-19 11:49:00 +02:00
ade20a0410 chore(deps): update module golang.org/x/crypto to v0.22.0 2024-04-19 11:49:00 +02:00
74fb73a268 chore(deps): lock file maintenance 2024-04-19 11:49:00 +02:00
84e218a672 chore(deps): lock file maintenance 2024-04-19 11:49:00 +02:00
f0377f5f5d admin: Able to export an archive for static publication
Some checks failed
continuous-integration/drone/tag Build is failing
2024-04-19 11:49:00 +02:00
9172f36be7 admin: Can view team 0/public my.json 2024-04-19 11:49:00 +02:00
298d09f346 confs: Add _ as an expected theme char 2024-04-19 11:49:00 +02:00
18cab3dc6b fickit: Fix launch of challenge-sync containers 2024-04-19 11:49:00 +02:00
d98aa1c269 fickit: Fix whitelist of remote sync IP 2024-04-19 11:49:00 +02:00
c0188e6d52 fickit: Update local remote score destination 2024-04-19 11:49:00 +02:00
66ab0dfc78 ui: Add some rules about standalone exercices 2024-04-19 11:49:00 +02:00
76606b3c80 checker: Ensure a flag is found before considering a good response 2024-04-19 11:49:00 +02:00
c53140b88e challenge-sync-airbus: Avoid concurrent map write 2024-04-19 11:49:00 +02:00
59cf98ead2 challenge-sync-airbus: Use --delay-updates to avoid WRITE 2024-04-19 11:49:00 +02:00
c547c45d31 fickit: Can use MYSQL_PASSWORD_FILE in backups 2024-04-19 11:49:00 +02:00
ecb815666e Remove all remaining validator_regexp 2024-04-19 11:49:00 +02:00
373bd83640 ui: Indicates Reverse exercices 2024-04-19 11:49:00 +02:00
59344893cb fickit: Add a second remote-challenge-sync container in parallel 2024-04-19 11:49:00 +02:00
9dbf34f4d3 fickit: New script to upgrade backend without reboot 2024-04-19 11:49:00 +02:00
9d87f70bc8 CI: Also push manifest of fic-frontend-ui 2024-04-19 11:49:00 +02:00
725e015478 fickit: Allow access secrets in sshd container 2024-04-19 11:49:00 +02:00
adb18a6a7c admin: New route to altern color between teams 2024-04-19 11:49:00 +02:00
382417b9ff admin: Fix color transformer 2024-04-19 11:49:00 +02:00
b4ec736948 fickit: Allow remote-sync IP in firewall 2024-04-19 11:49:00 +02:00
3f0e0536b9 chore(deps): lock file maintenance 2024-04-19 11:49:00 +02:00
f4d0e0001c sync: Don't overwrite theme image if it exists 2024-04-19 11:49:00 +02:00
9e6a03c681 ui: Before start, display some standalone exercices 2024-04-19 11:49:00 +02:00
122e919daf admin: Don't do only standalone exercices when doing speedy sync 2024-04-19 11:49:00 +02:00
df08e1ec72 admin: Remove hardcoded strings 2024-04-19 11:49:00 +02:00
239e8ae88d admin: Sane parameters for ResetSettings 2024-04-19 11:49:00 +02:00
0092170dbd fixkit: Reuse ssh configuration between boots 2024-04-19 11:49:00 +02:00
e3e55c579a fickit: Use rootfs directory instead of lower 2024-04-19 11:49:00 +02:00
ed217b5d72 fickit: Create dummy vouch-proxy config on frontend 2024-04-19 11:49:00 +02:00
a0a62a808d fickit: On deimos, allow performing status command from sshd 2024-04-19 11:49:00 +02:00
45395e399d fickit: Don't allow to quit ash in case of metadata erase skip 2024-04-19 11:49:00 +02:00
a01380730c fickit: Use mariadb instead of mysql 2024-04-19 11:49:00 +02:00
ada16f4ce7 fickit: Fix remote-sync config path 2024-04-19 11:49:00 +02:00
db603676a8 fickit: Update linuxkit images 2024-04-19 11:49:00 +02:00
5974fe8cd4 admin: Generate Vouch-Proxy config 2024-04-19 11:49:00 +02:00
59af4103b8 chore(deps): update dependency vite to v5.2.6 2024-04-19 11:49:00 +02:00
48c7a42922 chore(deps): update module github.com/gin-contrib/sessions to v1 2024-04-19 11:49:00 +02:00
d049e0f18e chore(deps): update dependency vite to v5.2.4 2024-04-19 11:49:00 +02:00
24e825d500 admin: Generate Vouch-Proxy config 2024-04-19 11:49:00 +02:00
81d272c5b2 fickit: Extract previous ISO in a temporary directory 2024-04-19 11:49:00 +02:00
dc5350c20f fickit: Handle secrets more seriously 2024-04-19 11:49:00 +02:00
c3e6cadb70 fickit: Add a script to retrieve containers status 2024-04-19 11:49:00 +02:00
1889447b34 fickit: Handle eth1 IP assignment for QA and iDRAC 2024-04-19 11:49:00 +02:00
cf4ff0245f admin: When generating team's symlinks, remove existing ones 2024-04-19 11:49:00 +02:00
52bc7b6650 admin: Make OIDC_ISSUER a variable 2024-04-19 11:49:00 +02:00
18fb11360b fickit: Update logo path 2024-04-19 11:49:00 +02:00
052b1a5949 ui: Fix header size on 4k screens 2024-04-19 11:49:00 +02:00
d8d95027af Update to dex 2.39.0 2024-04-19 11:49:00 +02:00
f585d75a66 Move to live.fic.srs.epita.fr 2024-04-19 11:48:59 +02:00
d9483ee98c libfic: Missing one ? 2024-04-19 11:48:59 +02:00
0808af7ded fickit: Update to live.fic.srs.epita.fr 2024-04-19 11:48:59 +02:00
fa5aee89c8 fickit: Add metadata to update image 2024-04-19 11:48:59 +02:00
dbf22f668b fickit: Fix filename 2024-04-19 11:48:59 +02:00
c9449b2338 fickit: fix metadatas 2024-04-19 11:48:59 +02:00
65b5bf8c16 CI: Add remote-challenge-sync-airbus 2024-04-19 11:48:59 +02:00
Jules Lefebvre
1404aa2c0f metadata: add jules.lefebvre keys into authorized_keys file 2024-04-19 11:48:59 +02:00
f815bff8ca CI: Enable CGO for repochecker 2024-04-19 11:48:59 +02:00
21bf188691 metadata: Use authorized_keys file 2024-04-19 11:48:59 +02:00
1e24d0ea25 remote-challenge-sync-airbus: Pass arguments through metadata files 2024-04-19 11:48:59 +02:00
3c8ba3ecc2 fickit: Add IP config in metadatas 2024-04-19 11:48:59 +02:00
bed79b947b fickit: Update mdadm pkg 2024-04-19 11:48:59 +02:00
b74d49aae7 challenge-sync-airbus: Balance score after each score change 2024-04-19 11:48:59 +02:00
c0017d7cbb challenge-sync-airbus: Use a score with better precision 2024-04-19 11:48:59 +02:00
f157d9c3bd challenge-sync-airbus: Handle individual try 2024-04-19 11:48:59 +02:00
ac966f9023 challenge-sync-airbus: Ready for 2024 2024-04-19 11:48:59 +02:00
5a6d9047c2 admin: Add buttons to navigate between teams 2024-04-19 11:48:59 +02:00
e9dc522a81 fickit: Update to latest images 2024-04-19 11:48:59 +02:00
989dce2aed ui: Improve interface 2024-04-19 11:48:59 +02:00
26c282138e Extract background color to continue image 2024-04-19 11:48:59 +02:00
35d07c1aa4 sync: Peak a deterministic ID if 0 2024-04-19 11:48:59 +02:00
85ab183bed chore(deps): update module github.com/go-sql-driver/mysql to v1.8.0 2024-04-19 11:48:59 +02:00
ba388780d8 chore(deps): lock file maintenance 2024-04-19 11:48:59 +02:00
7fbe2f3f8e repochecker: Ensure hint and choice_cost are not higher than gain 2024-04-19 11:48:59 +02:00
09c1111135 repochecker: Ensure non-optional flag doesn't depend on optional one 2024-04-19 11:48:59 +02:00
b3c207d07d ui: Avoid some hard coded strings 2024-04-19 11:48:59 +02:00
6e684436d1 ui: Handle new nextchangetime settings 2024-04-19 11:48:59 +02:00
0db9e9b539 evdist: Publish next settings change 2024-04-19 11:48:59 +02:00
357944564b admin: Add title on todo badges 2024-04-19 11:48:59 +02:00
d323bf9ee9 admin: Improve file readability 2024-04-19 11:48:59 +02:00
a0cd651dae admin: Can gunzip files 2024-04-19 11:48:59 +02:00
c082ee43d0 frontend: Visual improvements 2024-04-19 11:48:59 +02:00
6e5fd70156 checker: Refactor + ensure theme is not disabled 2024-04-19 11:48:59 +02:00
b9ded53920 admin: Rework progression on home page 2024-04-19 11:48:59 +02:00
c638789b61 admin: Require to be identitied to change the history 2024-04-19 11:48:59 +02:00
977caccc1f admin: Add ability to append element to exercice history 2024-04-19 11:48:59 +02:00
ae5068f8b8 Split Unlock standalone exercices between themes and standalone ex 2024-04-19 11:48:59 +02:00
cc147a9819 ui: Fix starting settings refresh 2024-04-19 11:48:59 +02:00
f32873f307 evdist: Add interrupts to consult state 2024-04-19 11:48:59 +02:00
0ca7aa568d admin: Fix activate timer 2024-04-19 11:48:59 +02:00
84b9e352ee ui: Improve display of locked exercices 2024-04-19 11:48:59 +02:00
398de21793 Apply standalone exercices settings 2024-04-19 11:48:59 +02:00
a1ce2df131 admin: New settings to define how to unlock standalone exercices 2024-04-19 11:48:59 +02:00
516ebf9c5a ui: Display a card to other exercices on home page 2024-04-19 11:48:59 +02:00
5e48ab0928 ui: Use Masonry layout to present themes and exercices on home page 2024-04-19 11:48:59 +02:00
13a11269a8 ui: Handle standalone exercice in menus and pages 2024-04-19 11:48:59 +02:00
d234bbf272 ui: Make menu item active according to the visited scenario 2024-04-19 11:48:59 +02:00
4c29f2e53e ui: Reset current_theme on page change 2024-04-19 11:48:59 +02:00
a9635ca8ac ui: handle exercice image on CardTheme 2024-04-19 11:48:59 +02:00
5edaf2cf3d ui: Update scss to avoid color aberation on list-group 2024-04-19 11:48:59 +02:00
adb0e36dd4 Able to sync and export standalone exercices 2024-04-19 11:48:59 +02:00
76f830b332 frontend: Add a cloud word for tags 2024-04-19 11:48:59 +02:00
daae6f4f07 admin: New option to drop all solutions from the database 2024-04-19 11:48:59 +02:00
79afaa8fb2 admin: Handle dashboard later publication through evdist 2024-04-19 11:48:59 +02:00
5592fabefa evdist: Retactor + include dashboard lookup 2024-04-19 11:48:59 +02:00
d44fc4f715 admin: Use datetime-local input types in settings 2024-04-19 11:48:59 +02:00
ad72eb0b95 admin: public sid can't contains / to avoid path traversal 2024-04-19 11:48:59 +02:00
19481962d5 admin: Update old bootstrap 2024-04-19 11:48:59 +02:00
0c45b52e04 admin: Reexpose themed exercices_stats.json
Lost in 8b3fbdb64a
2024-04-19 11:48:59 +02:00
c7fc18bfb4 admin: Really expose route to update team history
Related-to: a35aa7be70
2024-04-19 11:48:59 +02:00
a0bc832910 Theme can be optional: exercices can be standalone 2024-04-19 11:48:59 +02:00
3519f7416d Remove deadcode or fix unreachable code 2024-04-19 11:48:59 +02:00
e4f404d8d6 Remove unused StripPrefix
Not used since 8b3fbdb64a
2024-04-19 11:48:58 +02:00
b0cb03bbe1 chore(deps): lock file maintenance 2024-04-19 11:48:58 +02:00
97f73cbadb chore(deps): update module golang.org/x/oauth2 to v0.18.0 2024-04-19 11:48:58 +02:00
79d78bd358 chore(deps): lock file maintenance 2024-04-19 11:48:58 +02:00
9a8cb32719 chore(deps): update module golang.org/x/crypto to v0.21.0 2024-04-19 11:48:58 +02:00
f646eb41be chore(deps): lock file maintenance 2024-04-19 11:48:58 +02:00
55e3b35198 chore(deps): update module golang.org/x/crypto to v0.20.0 2024-04-19 11:48:58 +02:00
71da41b7cd chore(deps): lock file maintenance 2024-04-19 11:48:58 +02:00
a4fe5bbedd zqsd: Update to 2024 revision 2024-04-19 11:48:58 +02:00
177c87eeb7 Fix CI: building plugins require CGO 2024-04-19 11:48:58 +02:00
c09f2b9ad7 chore(deps): lock file maintenance 2024-04-19 11:48:58 +02:00
9e3ed09c5c chore(deps): lock file maintenance 2024-04-19 11:48:58 +02:00
aa2650f247 chore(deps): update module golang.org/x/oauth2 to v0.17.0 2024-04-19 11:48:58 +02:00
9732f7c7bd chore(deps): update module golang.org/x/crypto to v0.19.0 2024-04-19 11:48:58 +02:00
b2c37b7fff chore(deps): update dependency vite to v5.1.1 2024-04-19 11:48:58 +02:00
756412bafd chore(deps): update dependency svelte to v4.2.10 2024-04-19 11:48:58 +02:00
508ee458cd chore(deps): update dependency @sveltestrap/sveltestrap to v6.2.4 2024-04-19 11:48:58 +02:00
0f28a175b5 chore(deps): update dependency prettier to v3.2.5 2024-04-19 11:48:58 +02:00
7e00529aa8 chore(deps): update dependency sass-loader to v14.1.0 2024-04-19 11:48:58 +02:00
ef76edf9e0 chore(deps): update dependency @sveltejs/vite-plugin-svelte to v3.0.2 2024-04-19 11:48:58 +02:00
e7e4ab35dd chore(deps): update dependency @sveltejs/kit to v2.5.0 2024-04-19 11:48:58 +02:00
cb1cb391d1 chore(deps): update module github.com/yuin/goldmark to v1.7.0 2024-04-19 11:48:58 +02:00
eac4d717ff chore(deps): update linuxkit/kernel docker tag to v6 2024-04-19 11:48:58 +02:00
697e55fd74 chore(deps): update ghcr.io/dexidp/dex docker tag to v2.38.0 2024-04-19 11:48:58 +02:00
632eb62f1f admin: Add a button to permit deleting strange submissions 2024-04-19 11:48:58 +02:00
0b04185933 sync: It the repository is on the wrong branch and shallow, update the config 2024-04-19 11:48:58 +02:00
904ef661ba sync: Continue sync if just submodule update fails 2024-04-19 11:48:58 +02:00
fb6a5d8063 qa: Fix load functions not awaited since SvelteKit 2 2024-04-19 11:48:58 +02:00
81cd6fe3a1 admin: Base challenge start on server time 2024-04-19 11:48:58 +02:00
7798420d56 ui: Truncate exercice title 2024-04-19 11:48:58 +02:00
37bd268470 chore(deps): lock file maintenance 2024-04-19 11:48:58 +02:00
c674e1b621 chore(deps): update dependency @sveltestrap/sveltestrap to v6.2.3 2024-04-19 11:48:58 +02:00
b93c57f704 sync: If step is undefined, use the default value 2024-04-19 11:48:58 +02:00
e3762ffb1d chore(deps): update dependency @sveltejs/kit to v2.4.1 2024-04-19 11:48:58 +02:00
7cde7c05a1 chore(deps): update dependency vite to v5.0.12 2024-04-19 11:48:58 +02:00
3bd6ecf11e sync: Place hints files in the files/ directory 2024-04-19 11:48:58 +02:00
5eb1f66ba7 chore(deps): update dependency sass to v1.70.0 2024-04-19 11:48:58 +02:00
017df69896 chore(deps): update dependency @sveltejs/kit to v2.3.5 2024-04-19 11:48:58 +02:00
b1ca990537 chore(deps): update dependency sass-loader to v14 2024-04-19 11:48:58 +02:00
afcc7f2de0 Allow more parameters to be passed in environment 2024-04-19 11:48:58 +02:00
48fe1e7711 chore(deps): lock file maintenance 2024-04-19 11:48:58 +02:00
954cf84f0f repochecker: Test number step are in phase with response precision
Closes: https://gitlab.cri.epita.fr/ing/majeures/srs/fic/server/-/issues/36
2024-04-19 11:48:58 +02:00
04e938ff73 CI: Ignore failures on fic-get-remote-files 2024-04-19 11:48:58 +02:00
636cc2b55b repochecker: Markdown: unescape path to images 2024-04-19 11:48:58 +02:00
Erwan Polès
55f7d0826e Change erwan.poles ssh key 2024-04-19 11:48:58 +02:00
0de881d23e chore(deps): update module github.com/go-git/go-git/v5 to v5.11.0 2024-04-19 11:48:58 +02:00
b049930da9 chore(deps): update alpine docker tag to v3.19 2024-04-19 11:48:58 +02:00
2feb2d47b5 chore(deps): update module github.com/asticode/go-astisub to v0.26.2 2024-04-19 11:48:58 +02:00
a043138a63 qa: migration to SvelteKit 2 + Sveltestrap 6 2024-04-19 11:48:58 +02:00
4dedcfc420 frontend: migration to SvelteKit 2 + Sveltestrap 6 2024-04-19 11:48:58 +02:00
fe786e8b93 chore(deps): update module golang.org/x/oauth2 to v0.16.0 2024-04-19 11:48:58 +02:00
bff01deaff chore(deps): update module golang.org/x/image to v0.15.0 2024-04-19 11:48:58 +02:00
e9359b9a5e chore(deps): lock file maintenance 2024-04-19 11:48:58 +02:00
a10a93f336 chore(deps): update module golang.org/x/crypto to v0.18.0 2024-04-19 11:48:58 +02:00
efd571b36d chore(deps): lock file maintenance 2024-04-19 11:48:58 +02:00
3ef2db319f Also build fic-get-remote-files for arm64 2024-04-19 11:48:58 +02:00
631b2ff990 repochecker/epita: Treat labels as []rune 2024-04-19 11:48:58 +02:00
75bf99ed7b fickit-pkg/mdadm: Don't fail if /etc/init.d/ is empty 2024-04-19 11:48:58 +02:00
82d4cb3e38 chore(deps): lock file maintenance 2024-04-19 11:48:58 +02:00
0591f81255 ui: Handle number vector flag 2024-04-19 11:48:58 +02:00
e611424ba3 chore(deps): lock file maintenance 2024-04-19 11:48:58 +02:00
a5c6cdeb7e chore(deps): update module github.com/go-git/go-git/v5 to v5.10.1 2024-04-19 11:48:58 +02:00
cc2eee22f5 chore(deps): lock file maintenance 2024-04-19 11:48:58 +02:00
add006b2c2 chore(deps): update module golang.org/x/oauth2 to v0.15.0 2024-04-19 11:48:58 +02:00
834ab0f5c4 chore(deps): update module golang.org/x/crypto to v0.16.0 2024-04-19 11:48:58 +02:00
ac45800326 chore(deps): lock file maintenance 2024-04-19 11:48:58 +02:00
1a6209e58d qa: Don't append difficulty and/or timecount if we select another state 2024-04-19 11:48:58 +02:00
d4cad767eb qa: Refactor gitlab use 2024-04-19 11:48:58 +02:00
c31f76e9c3 qa: If admin link is down, use local data instead 2024-04-19 11:48:58 +02:00
9a5347b8ef admin: Don't consider non-git directory as Fatal 2024-04-19 11:48:58 +02:00
334512ef0d qa: New field to save exported state of qa 2024-04-19 11:48:58 +02:00
563cf14adc db: Don't forget exercices_qa in reset 2024-04-19 11:48:58 +02:00
34fd07ba4e qa: Fix link in issue 2024-04-19 11:48:58 +02:00
c9e3a9ac3c Introducing get-remote-files to download remote files 2024-04-19 11:48:58 +02:00
c3353572e6 sync: Export DownloadExerciceFile function 2024-04-19 11:48:58 +02:00
f3cdf1afca qa: Make new parameters modifiable through env 2024-04-19 11:48:58 +02:00
1f833d39fc admin: Export more importer functions 2024-04-19 11:48:57 +02:00
092256d9e5 fickit: Add missing sync directory on first boot 2024-04-19 11:48:57 +02:00
60a34d3ced qa: Can export to gitlab 2024-04-19 11:48:57 +02:00
Adrien Langou
0a22d09947 feat(gen_metadata): add sshkey 2024-04-19 11:48:57 +02:00
b52622c772 repochecker: Check flag's help content through mdTextHooks 2024-04-19 11:48:57 +02:00
9896445e00 grammalecte: Force capital letter only if first char is a letter
Fixes: https://gitlab.cri.epita.fr/ing/majeures/srs/fic/server/-/issues/34
2024-04-19 11:48:57 +02:00
130bb0c092 chore(deps): update dependency bootstrap-icons to v1.11.2 2024-04-19 11:48:57 +02:00
ff83468fef chore(deps): update dependency prettier-plugin-svelte to v3.1.2 2024-04-19 11:48:57 +02:00
83ceded133 chore(deps): update module github.com/asticode/go-astisub to v0.26.1 2024-04-19 11:48:57 +02:00
ecf5cac9c9 Add tests 2024-04-19 11:48:57 +02:00
b6966d47ce sync: Replace []error by go.uber.org/multierr 2024-04-19 11:48:57 +02:00
9f49a689fd Fix (CWE-118): Implicit memory aliasing in for loop. 2024-04-19 11:48:57 +02:00
e651a7495f chore(deps): lock file maintenance 2024-04-19 11:48:57 +02:00
50f668c229 chore(deps): update dependency @sveltejs/kit to v1.27.6 2024-04-19 11:48:57 +02:00
f1a662b11f chore(deps): lock file maintenance 2024-04-19 11:48:57 +02:00
3dd5127145 chore(deps): update dependency hash-wasm to v4.11.0 2024-04-19 11:48:57 +02:00
16e331586b chore(deps): update dependency prettier to v3.1.0 2024-04-19 11:48:57 +02:00
790e5b20f5 chore(deps): update node docker tag to v21 2024-04-19 11:48:57 +02:00
4e07dba966 chore(deps): lock file maintenance 2024-04-19 11:48:57 +02:00
4cd784765a chore(deps): update module golang.org/x/oauth2 to v0.14.0 2024-04-19 11:48:57 +02:00
520c0a86c7 chore(deps): update module golang.org/x/crypto to v0.15.0 2024-04-19 11:48:57 +02:00
e9ed3a42f6 chore(deps): update module golang.org/x/image to v0.14.0 2024-04-19 11:48:57 +02:00
c82e09a1c4 chore(deps): lock file maintenance 2024-04-19 11:48:57 +02:00
1378d5bd71 fickit: Share the SYNC directory 2024-04-19 11:48:57 +02:00
df90c5c72f sync: Give the type of non-implemented flag type 2024-04-19 11:48:57 +02:00
c70eb7f582 frontend: Keep the flag card displayed if not marked as solved
+ Ensure its hide when validated in presence of labels

Fixes: https://gitlab.cri.epita.fr/ing/majeures/srs/fic/server/-/issues/31
2024-04-19 11:48:57 +02:00
84a771a4a2 frontend: Display value of validated flags 2024-04-19 11:48:57 +02:00
6fd14306e1 Don't take case in count when sorting non-ordered vector flag
Fixes: https://gitlab.cri.epita.fr/ing/majeures/srs/fic/server/-/issues/30
2024-04-19 11:48:57 +02:00
20272e7bad Display a badge to inform about case sensitivity (can be disabled by setting) 2024-04-19 11:48:57 +02:00
f6b94b33e5 New option to allow teams to self reset their progression 2024-04-19 11:48:57 +02:00
a5234e401e chore(deps): update dependency eslint to v8.53.0 2024-04-19 11:48:57 +02:00
7e5e93ed35 chore(deps): update dependency @sveltejs/kit to v1.27.3 2024-04-19 11:48:57 +02:00
4ca709a11d chore(deps): update module github.com/yuin/goldmark to v1.6.0 2024-04-19 11:48:57 +02:00
ceb3ff0b6a chore(deps): update module github.com/go-git/go-git/v5 to v5.10.0 2024-04-19 11:48:57 +02:00
190fdfe422 sync: Label flags can contain more than 255 chars in database 2024-04-19 11:48:57 +02:00
d6ff46ca7f New option to allow teams to self reset their progression 2024-04-19 11:48:57 +02:00
a0c34018cf frontend: Refactor waitDiff func 2024-04-19 11:48:57 +02:00
ac39fe2493 frontend: $team is a derivated store now, use teamStore instead 2024-04-19 11:48:57 +02:00
5a9d2226e4 frontend: Label flag doesn't have id, use index instead 2024-04-19 11:48:57 +02:00
e957fccaca frontend: Reset current_exercice when viewing theme page 2024-04-19 11:48:57 +02:00
9cca20c6f8 frontend: Don't count labels in flag count 2024-04-19 11:48:57 +02:00
f328261ea2 repochecker: Fix number of detected choices with step attribute 2024-04-19 11:48:57 +02:00
08107931ea admin: View out of sync repositories 2024-04-19 11:48:57 +02:00
9a34e393f3 CI: Drop old assets automaticaly 2024-04-19 11:48:57 +02:00
a109c6d341 fickit: Fix preparation and deploy 2024-04-19 11:48:57 +02:00
0c7f5b88df fickit: Fix PXE + prepare 2024-04-19 11:48:57 +02:00
ec61b7ed1d CI: Build fickit-deploy 2024-04-19 11:48:57 +02:00
a80dd34d1b fickit: Use dm-crypt key is not changed during updates 2024-04-19 11:48:57 +02:00
e8c5b540d1 fickit: Add a command for fic-qa 2024-04-19 11:48:57 +02:00
01281adf28 fickit: Fallback on 7zip if isoinfo doesn't work 2024-04-19 11:48:57 +02:00
e65c54ea37 fickit: Fix tftpd usage 2024-04-19 11:48:57 +02:00
a7432b070d fickit: Fix Mariadb init + pin image version 2024-04-19 11:48:57 +02:00
b08039c997 admin: New routes to expose git repositories status 2024-04-19 11:48:57 +02:00
598b34eb4f fickit: Able to update already existing metadata iso 2024-04-19 11:48:57 +02:00
Tanguy
f6bb741070 gen_metada.sh: Check commands exist 2024-04-19 11:48:57 +02:00
Tanguy
1afbab32dd gen_metadata.sh: Change to bash 2024-04-19 11:48:57 +02:00
b3e221a15b inotify: Also treat the first WRITE event 2024-04-19 11:48:57 +02:00
a2d2457811 chore(deps): lock file maintenance 2024-04-19 11:48:57 +02:00
3f8750990e chore(deps): lock file maintenance 2024-04-19 11:48:57 +02:00
d996c12452 repochecker-grammalecte: Fix headline.md spelling error 2024-04-19 11:48:57 +02:00
ba26dd6bb1 sync: Don't warn on remote compressed files 2024-04-19 11:48:57 +02:00
106f1a5e30 CI: Replace License scanning by Dependency scanning for gitlab 16 compatibility 2024-04-19 11:48:57 +02:00
e74b5b267b CI: build fickit only after docker images 2024-04-19 11:48:57 +02:00
7573717f71 admin: Fix generator return format (was base64 bytes) 2024-04-19 11:48:57 +02:00
4b03f0befd admin: Create the SYNC directory if it doesn't exist 2024-04-19 11:48:57 +02:00
0ab321bb87 fickit: Check vault binary presence 2024-04-19 11:48:57 +02:00
7c8ab2567a fickit-deploy: Ensure artifacts and metadata are presents 2024-04-19 11:48:57 +02:00
5e5114bbd3 fickit: Standardize kernel filename (given by artifact name) 2024-04-19 11:48:57 +02:00
5db06e688a fickit: Avoid sha256 image number 2024-04-19 11:48:57 +02:00
41ff4b9f50 fickit-deploy: Dynamically update config at start and set expected IP on interface 2024-04-19 11:48:57 +02:00
49aa1682d7 sync: Refactor how and when remote files are downloaded 2024-04-19 11:48:57 +02:00
46db6bbf20 chore(deps): lock file maintenance 2024-04-19 11:48:57 +02:00
27a69b321b chore(deps): update dependency eslint to v8.51.0 2024-04-19 11:48:57 +02:00
dfd8580afd chore(deps): update module golang.org/x/oauth2 to v0.13.0 2024-04-19 11:48:57 +02:00
43de6a6b71 chore(deps): update dependency vite to v4.4.11 2024-04-19 11:48:57 +02:00
2097be2e21 chore(deps): update dependency @sveltejs/kit to v1.25.2 2024-04-19 11:48:57 +02:00
54590087ab chore(deps): update dependency sass to v1.69.3 2024-04-19 11:48:57 +02:00
6163d51e5b repochecker: New option to restrict domain where remote file can come from 2024-04-19 11:48:57 +02:00
3e1c01031f admin: Fix missing return 2024-04-19 11:48:57 +02:00
8b50029f4d admin: Fix errors reporting 2024-04-19 11:48:57 +02:00
1d4b79bf90 sync: Handle remote challenge files 2024-04-19 11:48:57 +02:00
ec3f818c30 sync: Exercice headline can be in a dedicated file 2024-04-19 11:48:57 +02:00
7cc076b31e chore(deps): update module golang.org/x/image to v0.13.0 2024-04-19 11:48:57 +02:00
5a484abaa8 chore(deps): update module golang.org/x/crypto to v0.14.0 2024-04-19 11:48:56 +02:00
ba8dcc1ddb chore(deps): update dependency vite to v4.4.10 2024-04-19 11:48:56 +02:00
28edb55131 chore(deps): lock file maintenance 2024-04-19 11:48:56 +02:00
59e52f3020 chore(deps): update module github.com/go-git/go-git/v5 to v5.9.0 2024-04-19 11:48:56 +02:00
5811cc2c2f chore(deps): lock file maintenance 2024-04-19 11:48:56 +02:00
2dbe79930a chore(deps): update module golang.org/x/oauth2 to v0.12.0 2024-04-19 11:48:56 +02:00
682a5f933f chore(deps): update module golang.org/x/crypto to v0.13.0 2024-04-19 11:48:56 +02:00
289393e9c3 chore(deps): update module golang.org/x/image to v0.12.0 2024-04-19 11:48:56 +02:00
3b1fd53ba6 chore(deps): lock file maintenance 2024-04-19 11:48:56 +02:00
22bd4e68c4 chore(deps): update module github.com/yuin/goldmark to v1.5.6 2024-04-19 11:48:56 +02:00
81b9649c5d chore(deps): update dependency eslint-config-prettier to v9 2024-04-19 11:48:56 +02:00
bb7ded3619 chore(deps): update dependency @sveltejs/kit to v1.23.0 2024-04-19 11:48:56 +02:00
02edce9df7 chore(deps): update dependency sass to v1.66.1 2024-04-19 11:48:56 +02:00
04a3a64ffa chore(deps): lock file maintenance 2024-04-19 11:48:56 +02:00
f4f42b1303 chore(deps): update dependency eslint to v8.47.0 2024-04-19 11:48:56 +02:00
a905f56e42 chore(deps): update dependency @sveltejs/kit to v1.22.6 2024-04-19 11:48:56 +02:00
6578cddf02 chore(deps): update dependency vite to v4.4.9 2024-04-19 11:48:56 +02:00
d65026edd0 chore(deps): update dependency sass to v1.65.1 2024-04-19 11:48:56 +02:00
3c97f91a46 chore(deps): update module github.com/u2takey/ffmpeg-go to v0.5.0 2024-04-19 11:48:56 +02:00
60d88cd317 chore(deps): update dependency sveltestrap to v5.11.1 2024-04-19 11:48:56 +02:00
3ef5a562c9 chore(deps): update module github.com/asticode/go-astisub to v0.26.0 2024-04-19 11:48:56 +02:00
430e5f5cb8 chore(deps): update module golang.org/x/oauth2 to v0.11.0 2024-04-19 11:48:56 +02:00
55e6700c45 chore(deps): update module golang.org/x/crypto to v0.12.0 2024-04-19 11:48:56 +02:00
1aaf2105a5 chore(deps): update module golang.org/x/image to v0.11.0 2024-04-19 11:48:56 +02:00
6eab49510c chore(deps): update dependency prettier to v3.0.1 2024-04-19 11:48:56 +02:00
705047f770 chore(deps): update dependency eslint-config-prettier to v8.10.0 2024-04-19 11:48:56 +02:00
4b97d20ff6 nginx: Can add custom text before head and body (tracking links, ...) 2024-04-19 11:48:56 +02:00
0fcdcfaa7a chore(deps): lock file maintenance 2024-04-19 11:48:56 +02:00
f27acf3830 chore(deps): update module golang.org/x/image to v0.10.0 2024-04-19 11:48:56 +02:00
d12e6597bd chore(deps): update dependency sass to v1.64.2 2024-04-19 11:48:56 +02:00
23a982f083 chore(deps): update dependency @sveltejs/kit to v1.22.4 2024-04-19 11:48:56 +02:00
bf61b0184b dashboard: Wait for the themes to load before getting the events 2024-04-19 11:48:56 +02:00
4ee950cca2 dashboard: Parametrize the challenge name 2024-04-19 11:48:56 +02:00
11662d8c5e dashboard: Allow countdown to be hidden 2024-04-19 11:48:56 +02:00
17d140d7cc chore(deps): lock file maintenance 2024-04-19 11:48:56 +02:00
cb2cd7f4c0 Requires login to see themes (when using fic-nginx container) 2024-04-19 11:48:56 +02:00
51e3bfde90 README: Add diagrams 2024-04-19 11:48:56 +02:00
08002751bf chore(deps): update linuxkit/kernel docker tag to v5.15.110 2024-04-19 11:48:56 +02:00
19c447d4e4 chore(deps): update dependency prettier-plugin-svelte to v3.0.1 2024-04-19 11:48:56 +02:00
7bfab1ad64 chore(deps): update module github.com/go-git/go-git/v5 to v5.8.1 2024-04-19 11:48:56 +02:00
9aee418889 chore(deps): update dependency bootstrap to v5.3.1 2024-04-19 11:48:56 +02:00
06cb10a0c7 fickit-metadata: Add missing key 2024-04-19 11:48:56 +02:00
a058679829 qa: New overview screen for managers 2024-04-19 11:48:56 +02:00
c13da8b574 qa: Refactor layout 2024-04-19 11:48:56 +02:00
859b6a318e qa: Can export QA in JSON 2024-04-19 11:48:56 +02:00
cd64fc90bf qa: Managers can view team and manage theirs todo list 2024-04-19 11:48:56 +02:00
b94beb363b qa: New development route 2024-04-19 11:48:56 +02:00
7114ece593 qa: Don't fail if no intro 2024-04-19 11:48:56 +02:00
d4990916b5 qa: Update scripts 2024-04-19 11:48:56 +02:00
d2f409db7a New setting delegated_qa to store QA managers 2024-04-19 11:48:56 +02:00
e000778696 fickit: Add QA platform 2024-04-19 11:48:56 +02:00
bbe2072b4f fickit: Fix inverted hostname 2024-04-19 11:48:56 +02:00
57720f156e renovate: Fix regexp for linuxkit files 2024-04-19 11:48:56 +02:00
dd594a8d08 chore(deps): update dependency vite to v4.4.7 2024-04-19 11:48:56 +02:00
4fb0c11736 Introduce fickit-deploy image 2024-04-19 11:48:56 +02:00
3e5e8c9ba4 CI: Optimize builds 2024-04-19 11:48:56 +02:00
c69a335a91 chore(deps): update module github.com/yuin/goldmark to v1.5.5 2024-04-19 11:48:56 +02:00
89334ce57c admin: Fix panic as map is nil 2024-04-19 11:48:56 +02:00
1ace4394b5 fixkit: Ready for tomorrow 2024-04-19 11:48:56 +02:00
3e828ebbfc fickit: Use sane format options 2024-04-19 11:48:56 +02:00
0c9ba50fcc Include fickit-prepare in boot (to permit to reinstall) 2024-04-19 11:48:56 +02:00
25f2b5827a fickit: Fix too long interface name 2024-04-19 11:48:56 +02:00
89d687cd94 Terminate implementation of metadata in fickit 2024-04-19 11:48:56 +02:00
Adrien Langou
a5699b6cce feat(configs): create iso file instead of json 2024-04-19 11:48:56 +02:00
Adrien Langou
a431b75e69 feat(configs): create script gen_metadata 2024-04-19 11:48:55 +02:00
8717fc24fd Start playing with metadata 2024-04-19 11:48:55 +02:00
6caf8c53b9 chbase: Use same strategy for relative paths as qa 2024-04-19 11:48:55 +02:00
8132bc6b17 chore(deps): lock file maintenance 2024-04-19 11:48:55 +02:00
1d66450240 CI: Ignore non-existant SSL_FILES 2024-04-19 11:48:55 +02:00
97bdad21a2 CI: Use a dedicated docker image for LinuxKit 2024-04-19 11:48:55 +02:00
acf909ab1e chbase: Use same strategy for relative paths as qa 2024-04-19 11:48:55 +02:00
472e3a8cba chbase: Follow new sveltekit changes 2024-04-19 11:48:55 +02:00
27d2121337 chore(deps): update dependency sass to v1.64.1 2024-04-19 11:48:55 +02:00
d91d514ed6 Update linuxkit containers 2024-04-19 11:48:55 +02:00
c86349bc72 Update external IP for fic.srs.epita.fr 2024-04-19 11:48:55 +02:00
dfccde82cf Build fickit packages 2024-04-19 11:48:55 +02:00
0aff2a3151 chore(deps): update dependency vite to v4.4.6 2024-04-19 11:48:55 +02:00
55817f265b chore(deps): update dependency prettier-plugin-svelte to v3 2024-04-19 11:48:55 +02:00
3478ffca03 chore(deps): update module github.com/go-git/go-git/v5 to v5.8.0 2024-04-19 11:48:55 +02:00
bbb23e08ec chore(deps): update dependency sass to v1.64.0 2024-04-19 11:48:55 +02:00
b6bc7f5736 chore(deps): update dependency eslint to v8.45.0 2024-04-19 11:48:55 +02:00
3d2a601580 chore(deps): update dependency @sveltejs/kit to v1.22.3 2024-04-19 11:48:55 +02:00
3502e3f6b8 chore(deps): update dependency vite to v4.4.4 2024-04-19 11:48:55 +02:00
c08386b0ca chore(deps): update dependency prettier to v3 2024-04-19 11:48:55 +02:00
6acc752bd9 label flags: Increase allowed size 2024-04-19 11:48:55 +02:00
5dd92a6603 CI: Use ./... instead of listing all packages 2024-04-19 11:48:55 +02:00
6d450d3667 Use nginx:stable-alpine-slim to reduce available libs 2024-04-19 11:48:55 +02:00
f097c029f3 Security fix: Incorrect permission assignment for critical resource 2024-04-19 11:48:55 +02:00
499e251796 security fix: Uncontrolled resource consumption (Slowloris) 2024-04-19 11:48:55 +02:00
Adrien Langou
b3b102b2f4 feat(ci): add generator 2024-04-19 11:48:55 +02:00
Adrien Langou
eb67674da0 feat(ci): add sast and qa jobs 2024-04-19 11:48:55 +02:00
Adrien Langou
0200dce71b feat(ci): split gitlab-ci in mutiple files 2024-04-19 11:48:55 +02:00
Adrien Langou
4ef8589330 feat(ci): trigger image build only on master 2024-04-19 11:48:55 +02:00
Adrien Langou
9c656c92fe feat(ci): rename stages 2024-04-19 11:48:55 +02:00
Adrien Langou
7999464384 feat(ci): add latest tag 2024-04-19 11:48:55 +02:00
Adrien Langou
5cf894031c feat(ci): add image builds 2024-04-19 11:48:55 +02:00
Adrien Langou
4856a2ce2d feat(ci): add build stage 2024-04-19 11:48:55 +02:00
Adrien Langou
979f64845c feat(ci): add ci first stage 2024-04-19 11:48:55 +02:00
50adfa9536 nginx: Fix localhost redirections when not ending with / 2024-04-19 11:48:55 +02:00
934493f77a nginx: Increase allowed load time for admin api 2024-04-19 11:48:55 +02:00
1769938205 generator: Can perform synchronous generation 2024-04-19 11:48:55 +02:00
ec98e521dc Dockerfile: keep node version in sync 2024-04-19 11:48:55 +02:00
4df1948069 Update README with the new process names 2024-04-19 11:48:55 +02:00
ed091e761c Split backend service into checker and generator
Both are linked through a unix socket.
2024-04-19 11:48:55 +02:00
f755d7c998 chore(deps): lock file maintenance 2024-04-19 11:48:55 +02:00
1ca5452707 Rename frontend as receiver 2024-04-19 11:48:51 +02:00
dc83efa868 sync: Better perform exception in exercices 2024-04-19 11:46:54 +02:00
edbb867f62 sync: Allow exercice directory to do not have identifier 2024-04-19 11:46:54 +02:00
6fd1856dd9 compose: Fix usability 2024-04-19 11:46:54 +02:00
7a2b1bdede nginx: Fix initialization 2024-04-19 11:46:54 +02:00
b9a456e8f7 chore(deps): update dependency vite to v4.4.0 2024-04-19 11:46:54 +02:00
1e2db62c74 chore(deps): update module golang.org/x/oauth2 to v0.10.0 2024-04-19 11:46:54 +02:00
2116253e9d chore(deps): update dependency @sveltejs/kit to v1.22.0 2024-04-19 11:46:54 +02:00
ad57faf0c1 chore(deps): update module golang.org/x/image to v0.9.0 2024-04-19 11:46:54 +02:00
240ecd269d docker-compose: Include evdist layer 2024-04-19 11:46:54 +02:00
fd09d1bbbc chore(deps): update module github.com/studio-b12/gowebdav to v0.9.0 2024-04-19 11:46:54 +02:00
064d291c2a chore(deps): update dependency eslint to v8.44.0 2024-04-19 11:46:54 +02:00
144ae6d5a2 chore(deps): update dependency @sveltejs/kit to v1.21.0 2024-04-19 11:46:54 +02:00
073258e067 chore(deps): update dependency svelte to v3.59.2 2024-04-19 11:46:54 +02:00
e787a48f76 chore(deps): update module github.com/asticode/go-astisub to v0.25.1 2024-04-19 11:46:54 +02:00
ca3b5460ae chore(deps): update module golang.org/x/oauth2 to v0.9.0 2024-04-19 11:46:54 +02:00
2137c8c243 chore(deps): update dependency sass to v1.63.6 2024-04-19 11:46:54 +02:00
d5642d7888 chore(deps): update dependency @sveltejs/kit to v1.20.5 2024-04-19 11:46:54 +02:00
b3ec909f96 chore(deps): update dependency eslint to v8.43.0 2024-04-19 11:46:54 +02:00
d475365b43 chore(deps): update module golang.org/x/crypto to v0.10.0 2024-04-19 11:46:54 +02:00
b86a6ebc0c admin: Add an animation when modifications are in progress 2024-04-19 11:46:54 +02:00
34f175e57b admin: Use branch indication to access the repo 2024-04-19 11:46:54 +02:00
61ec7a56f5 ui: Fix size of countdown 2024-04-19 11:46:54 +02:00
60243dd486 ui: Handle exercice authors 2024-04-19 11:46:54 +02:00
28ad0fa791 fic: Can overwrite authors for each exercice 2024-04-19 11:46:54 +02:00
1a8ebcb8bf ui: Handle exercice image 2024-04-19 11:46:54 +02:00
ab23ef8f71 admin: Fix API response 2024-04-19 11:46:54 +02:00
abe5ad61d4 fic: Exercice can have heading.jpg 2024-04-19 11:46:54 +02:00
f366d6b8c1 sync: Handle repochecker-ack.txt in exercice directory 2024-04-19 11:46:54 +02:00
c06d667088 fixup! svelte-migrate: updated files 2024-04-19 11:46:54 +02:00
5cd285f6d0 chore(deps): update module golang.org/x/image to v0.8.0 2024-04-19 11:46:54 +02:00
1051291b84 chore(deps): lock file maintenance 2024-04-19 11:46:54 +02:00
3a2fce7376 chore(deps): lock file maintenance 2024-04-19 11:46:54 +02:00
232e7ae879 chore(deps): update dependency sass-loader to v13.3.2 2024-04-19 11:46:54 +02:00
033cb1fe18 chore(deps): update dependency sass to v1.63.3 2024-04-19 11:46:54 +02:00
ecd5f25ec9 chore(deps): update module github.com/burntsushi/toml to v1.3.2 2024-04-19 11:46:54 +02:00
620f744c0c chore(deps): update dependency sass to v1.63.2 2024-04-19 11:46:54 +02:00
3c5b4a7d9f chore(deps): update dependency @sveltejs/kit to v1.20.2 2024-04-19 11:46:54 +02:00
d85417b925 qa: New route to export data 2024-04-19 11:46:54 +02:00
31bccddc49 qa: Add a new field to retrieve passed time on exercice 2024-04-19 11:46:54 +02:00
3a38a75e25 admin: Readd missing route to add exercice from ui 2024-04-19 11:46:54 +02:00
3cf3a03ab8 qa: Fix new sveltekit release 2024-04-19 11:46:54 +02:00
d9abf90e84 chore(deps): update module github.com/burntsushi/toml to v1.3.0 2024-04-19 11:46:54 +02:00
5e712dc4a4 chore(deps): update module github.com/gin-gonic/gin to v1.9.1 2024-04-19 11:46:54 +02:00
290a445e41 chore(deps): lock file maintenance 2024-04-19 11:46:54 +02:00
4a19893480 chore(deps): lock file maintenance 2024-04-19 11:46:54 +02:00
81b7dea92d chore(deps): update dependency sass-loader to v13.3.1 2024-04-19 11:46:54 +02:00
9a9dff84ee chore(deps): update module github.com/go-git/go-git/v5 to v5.7.0 2024-04-19 11:46:54 +02:00
1ece65ff9b chore(deps): update dependency @popperjs/core to v2.11.8 2024-04-19 11:46:54 +02:00
1c87b9ce55 chore(deps): update dependency prettier-plugin-svelte to v2.10.1 2024-04-19 11:46:54 +02:00
6943c66c25 chore(deps): update dependency vite to v4.3.9 2024-04-19 11:46:54 +02:00
7712419a9f chore(deps): update dependency @sveltejs/kit to v1.20.0 2024-04-19 11:46:54 +02:00
edc5c25a29 sync: Try to handle new submodules on pull 2024-04-19 11:46:54 +02:00
5b102ad8ea chore(deps): lock file maintenance 2024-04-19 11:46:54 +02:00
0a627b9c6d chore(deps): lock file maintenance 2024-04-19 11:46:54 +02:00
1c78cefb09 chore(deps): update dependency eslint to v8.41.0 2024-04-19 11:46:54 +02:00
d4c6a1ccbb chore(deps): update module github.com/asticode/go-astisub to v0.24.0 2024-04-19 11:46:54 +02:00
6fa262eb7f chore(deps): update dependency vite to v4.3.8 2024-04-19 11:46:54 +02:00
bd0b1d28a1 chore(deps): update dependency @sveltejs/kit to v1.18.0 2024-04-19 11:46:54 +02:00
Antoine Thouvenin
0a7fc6fa47 flake: update for airbus-sync 2024-04-19 11:46:54 +02:00
d8458e5b49 repochecker: Update documentation URL 2024-04-19 11:46:54 +02:00
78189aab37 Rename ValidatorRegexp to CaptureRegexp 2024-04-19 11:46:54 +02:00
e472b482d6 chore(deps): update alpine docker tag to v3.18 2024-04-19 11:46:54 +02:00
2d6fd8eb45 chore(deps): update dependency vite to v4.3.6 2024-04-19 11:46:54 +02:00
4a603dcaf7 chore(deps): lock file maintenance 2024-04-19 11:46:54 +02:00
890a532d01 Minimize needed images 2024-04-19 11:46:54 +02:00
acabe41e07 nginx: When no base url, remove the unneed rewrite 2024-04-19 11:46:54 +02:00
b7344f2b73 qa: Keep base hack in sync with sveltekit 2024-04-19 11:46:54 +02:00
20dc1f65dc dashboard: can customize main image background 2024-04-19 11:46:54 +02:00
37dde01444 New settings hide_header to hide the top banner with partners and countdown 2024-04-19 11:46:53 +02:00
aad95f1e53 settings: Challenge can never ends 2024-04-19 11:46:53 +02:00
d4f69059bf docker: Fix build on arm64 2024-04-19 11:46:53 +02:00
34889a949f chore(deps): update dependency @sveltejs/kit to v1.16.3 2024-04-19 11:46:53 +02:00
532cb6e489 chore(deps): update dependency svelte to v3.59.1 2024-04-19 11:46:53 +02:00
5f31b85556 chore(deps): update module golang.org/x/crypto to v0.9.0 2024-04-19 11:46:53 +02:00
fa57099f45 chore(deps): update module golang.org/x/oauth2 to v0.8.0 2024-04-19 11:46:53 +02:00
541741760b chore(deps): lock file maintenance 2024-04-19 11:46:53 +02:00
5fb85c22dc sync: Don't pull repo when doing synchronization. Do it only on auto-sync 2024-04-19 11:46:53 +02:00
ac64db277a admin: Don't consider .locked file as problematic 2024-04-19 11:46:53 +02:00
6407970dfa CI: Add git info into admin binary 2024-04-19 11:46:53 +02:00
2ca3d1471f chore(deps): update dependency vite to v4.3.5 2024-04-19 11:46:53 +02:00
902d9195ac chore(deps): update dependency @sveltejs/kit to v1.16.1 2024-04-19 11:46:53 +02:00
79a5d251aa chore(deps): update dependency svelte to v3.59.0 2024-04-19 11:46:53 +02:00
f6a251e2ec sync: Don't start SpeedySyncDeep by pull 2024-04-19 11:46:53 +02:00
2140939364 sync: Allow using challenge.toml instead of challenge.txt 2024-04-19 11:46:53 +02:00
20c41ec573 admin: Handle exercice path given to auto-sync 2024-04-19 11:46:53 +02:00
75eae43f60 admin: auto-sync tries to sync themes if it doesn't exists yet 2024-04-19 11:46:53 +02:00
63cf665f2d admin: Refactor sync/auto 2024-04-19 11:46:53 +02:00
abd91012f8 chore(deps): update dependency @sveltejs/kit to v1.16.0 2024-04-19 11:46:53 +02:00
3103dc1029 repochecker: Use challenge.txt as a more representative file for detecting exercices 2024-04-19 11:46:53 +02:00
e261c77c79 sync: Include in file presence checks splitted and compressed files 2024-04-19 11:46:53 +02:00
f5529ff72d admin: New option to pass branch to use 2024-04-19 11:46:53 +02:00
f623699f56 repochecker: If a statement file is present, treat as exercice 2024-04-19 11:46:53 +02:00
c5a059bd3b sync: Expose sync.Exists function 2024-04-19 11:46:53 +02:00
5cf4565573 Keep chbase in sync with latest sveltekit version 2024-04-19 11:46:53 +02:00
f1d96089ce chore(deps): update dependency vite to v4.3.4 2024-04-19 11:46:53 +02:00
55de68f428 chore(deps): update node docker tag to v20 2024-04-19 11:46:53 +02:00
8f18dbbf81 chore(deps): lock file maintenance 2024-04-19 11:46:53 +02:00
c8f59ce706 chore(deps): update module github.com/go-sql-driver/mysql to v1.7.1 2024-04-19 11:46:53 +02:00
dd8781ea24 chore(deps): lock file maintenance 2024-04-19 11:46:53 +02:00
4fd8d3880a chore(deps): update dependency @sveltejs/kit to v1.15.6 2024-04-19 11:46:53 +02:00
1f16cabe19 chore(deps): update dependency @sveltejs/adapter-static to v2.0.2 2024-04-19 11:46:53 +02:00
5db62e45d4 chore(deps): update dependency @sveltejs/kit to v1.15.5 2024-04-19 11:46:53 +02:00
6faae3e48d chore(deps): update dependency sass to v1.62.0 2024-04-19 11:46:53 +02:00
3b84537430 Update README 2024-04-19 11:46:53 +02:00
fb64472f2f fickit: Update J2
Some checks failed
continuous-integration/drone/tag Build is failing
2024-04-19 11:46:53 +02:00
375f1da071 Display exercices when theme is not locked, but not flags 2024-04-19 11:46:53 +02:00
d8462cf58e backend: Display a message when the exercice is disabled 2024-04-19 11:46:53 +02:00
089e604679 ui: Move behind in the menu disabled themes 2024-04-19 11:46:53 +02:00
0d5b87b3f7 challenge-sync-airbus: Done 2024-04-19 11:46:53 +02:00
268925db0d backend: Can lock submission for a given exercice 2024-04-19 11:46:53 +02:00
3344e05e0d challenge-sync-airbus: Do job 2024-04-19 11:46:53 +02:00
18b8f0f722 ui: Don't count label in flag count 2024-04-19 11:46:53 +02:00
b79fe47f00 ui: Ask confirmation before open hint 2024-04-19 11:46:53 +02:00
bf758e7fe5 dashboard: Programmize awards date 2024-04-19 11:46:53 +02:00
0d596ccb8c dashboard: Fix last challenge due to hashtable change 2024-04-19 11:46:53 +02:00
cc0e26ef1f dashboard: Fix image size 2024-04-19 11:46:53 +02:00
a9ba784e58 dashboard: In prod, we don't use ../ 2024-04-19 11:46:53 +02:00
094deeab51 fickit: Fic backend IP range 2024-04-19 11:46:53 +02:00
37bfd8f2b9 ui: Change exercices' theme alignment 2024-04-19 11:46:53 +02:00
9ac09278f6 ui: Add a link to change password through issue report 2024-04-19 11:46:53 +02:00
00f7399170 ui: Update rules to include discounted factor 2024-04-19 11:46:53 +02:00
217c85aed5 fickit: Update keys 2024-04-19 11:46:53 +02:00
848eb913e4 fickit: Add paul.leroux 2024-04-19 11:46:53 +02:00
32a3414ff1 chore(deps): lock file maintenance 2024-04-19 11:46:53 +02:00
d84040bda9 chore(deps): update dependency bootstrap-icons to v1.10.4 2024-04-19 11:46:53 +02:00
7faec773d6 chore(deps): update dependency @sveltejs/kit to v1.15.2 2024-04-19 11:46:53 +02:00
0fef44a542 chore(deps): update dependency sass to v1.61.0 2024-04-19 11:46:53 +02:00
6ba309e5e5 chore(deps): update module golang.org/x/crypto to v0.8.0 2024-04-19 11:46:53 +02:00
3f9e0d851d chore(deps): update module golang.org/x/oauth2 to v0.7.0 2024-04-19 11:46:53 +02:00
8f8f22b5bc chore(deps): update module golang.org/x/image to v0.7.0 2024-04-19 11:46:53 +02:00
802e7b4f53 chore(deps): update dependency @sveltejs/kit to v1.15.1 2024-04-19 11:46:53 +02:00
6bd23ee9f0 admin: Fix move between exercices 2024-04-19 11:46:53 +02:00
a585a6338e admin: Fix typo 2024-04-19 11:46:53 +02:00
47aea1de66 ui: Title was not readable 2024-04-19 11:46:53 +02:00
5163ce4c8d Also consider compressed file check 2024-04-19 11:46:53 +02:00
c84d39ca27 fickit: Pull 2023 repository 2024-04-19 11:46:53 +02:00
6a0b0545d7 ui: Display a message when dealing with compressed downloads 2024-04-19 11:46:53 +02:00
1a21115503 ui: Ensure a message is shown when theme is disabled 2024-04-19 11:46:53 +02:00
0a7b40abd7 ui: Move locked themes at the end of the list 2024-04-19 11:46:53 +02:00
4d7161281d ui: Next button is back! 2024-04-19 11:46:53 +02:00
0c7eecf315 ui: Fill correctly the field containing a link to the exercice 2024-04-19 11:46:53 +02:00
af1fe1e6d8 fickit: Update IPs 2024-04-19 11:46:53 +02:00
c2104a642d fickit: Update images 2024-04-19 11:46:53 +02:00
71d7ac3cbf Update ssh-keys 2024-04-19 11:46:53 +02:00
1bd30632d3 challenge-sync-airbus: Add an option to not search for exercice validation 2024-04-19 11:46:53 +02:00
bd9d9e9402 frontend-ui: Don't need python2 anymore 2024-04-19 11:46:53 +02:00
d98d846cd6 fickit: Update local pkgs 2024-04-19 11:46:53 +02:00
da6f8acbbd fickit: LinuxKit 1.0 standards 2024-04-19 11:46:52 +02:00
938ee1d172 fickit: Don't forget this variable for OIDC loggin 2024-04-19 11:46:52 +02:00
7de41b78a7 fickit: Update linuxkit images 2024-04-19 11:46:52 +02:00
d17e0d17a9 chore(deps): update dependency @sveltejs/kit to v1.15.0 2024-04-19 11:46:52 +02:00
8ffa7a2cba chore(deps): update dependency svelte to v3.58.0 2024-04-19 11:46:52 +02:00
769024517f chore(deps): update dependency eslint to v8.37.0 2024-04-19 11:46:52 +02:00
159b8bda50 chore(deps): update dependency sass-loader to v13.2.2 2024-04-19 11:46:52 +02:00
54a96ab501 chore(deps): lock file maintenance 2024-04-19 11:46:52 +02:00
f263712185 Fix non-standard SQL statement in score calculation 2024-04-19 11:46:52 +02:00
5fac0d4b30 Fix gain estimation error due to variable overwrite 2024-04-19 11:46:52 +02:00
3772af4965 Fix EstimateGain function 2024-04-19 11:46:52 +02:00
4451e41285 New setting: introduce a decote/discount to exercice's gain 2024-04-19 11:46:52 +02:00
838ce2fb3f ui: Mark locked theme as « Confidential » 2024-04-19 11:46:52 +02:00
d4ce0dd474 Can lock theme 2024-04-19 11:46:52 +02:00
b8c5ec6725 ui: Hide tags with less than 2 exercices 2024-04-19 11:46:52 +02:00
fd7f6d0c63 Update package-lock.json 2024-04-19 11:46:52 +02:00
e636763597 Fix access to exercice page directly 2024-04-19 11:46:52 +02:00
38c0a5ceee Make stop_refresh an object that can be modified from another module 2024-04-19 11:46:52 +02:00
ef1eafb789 themes.json: Use a exercice list instead of hash 2024-04-19 11:46:52 +02:00
3fcf705dcf admin: Show what properties will be overwritted 2024-04-19 11:46:52 +02:00
eb85b28f5b Add a disabled state to exercices 2024-04-19 11:46:52 +02:00
0f41e44e13 Increase external_id capacity in db 2024-04-19 11:46:52 +02:00
0e9d0f4933 chore(deps): update module github.com/go-git/go-git/v5 to v5.6.1 2024-04-19 11:46:52 +02:00
f24d3ea5e8 chore(deps): update module golang.org/x/image to v0.6.0 2024-04-19 11:46:52 +02:00
3d89159d0c ui: Use programatic base 2024-04-19 11:46:52 +02:00
ade363cab1 Ignore failure on binaries deployment 2024-04-19 11:46:52 +02:00
d2aa336bf2 ui: New button to expand resolution text in a large modal 2024-04-19 11:46:52 +02:00
5427d3cbf1 Don't count label flags in NbFlags 2024-04-19 11:46:52 +02:00
6855318e42 chore(deps): update module golang.org/x/oauth2 to v0.6.0 2024-04-19 11:46:52 +02:00
66ddca30fb chore(deps): update module golang.org/x/crypto to v0.7.0 2024-04-19 11:46:52 +02:00
a882a4a302 chore(deps): update module github.com/go-git/go-git/v5 to v5.6.0 2024-04-19 11:46:52 +02:00
f1c0ffd679 chore(deps): lock file maintenance 2024-04-19 11:46:52 +02:00
1c1f5a8485 chore(deps): update module github.com/gin-gonic/gin to v1.9.0 2024-04-19 11:46:52 +02:00
444d6c3523 chore(deps): update dependency @sveltejs/adapter-static to v2 2024-04-19 11:46:52 +02:00
f7dfc5a510 chore(deps): lock file maintenance 2024-04-19 11:46:52 +02:00
7333fbeb71 chore(deps): update module golang.org/x/image to v0.5.0 2024-04-19 11:46:52 +02:00
26911181a2 qa: Fix generation 2024-04-19 11:46:52 +02:00
3fb7c6e772 renovate: Enable lock-file maintenance 2024-04-19 11:46:52 +02:00
f7540ee5cf ui: Update node packages 2024-04-19 11:46:52 +02:00
f92116c2ba chore(deps): update module golang.org/x/image to v0.4.0 2024-04-19 11:46:52 +02:00
83f30319b3 chore(deps): update module golang.org/x/oauth2 to v0.5.0 2024-04-19 11:46:52 +02:00
394e11a742 chore(deps): update module golang.org/x/crypto to v0.6.0 2024-04-19 11:46:52 +02:00
4b7181c857 chore(deps): update github.com/studio-b12/gowebdav digest to 3282f94 2024-04-19 11:46:52 +02:00
b879bb3aca chore(deps): update github.com/studio-b12/gowebdav digest to cd21842 2024-04-19 11:46:52 +02:00
75145dc5cd chore(deps): update module github.com/yuin/goldmark to v1.5.4 2024-04-19 11:46:52 +02:00
847a42700f admin: Fix team updating 2024-04-19 11:46:52 +02:00
1d3a75ff82 sync: Don't stop parsing after the first treated image 2024-04-19 11:46:52 +02:00
0f65babdf4 admin/sync: typo 2024-04-19 11:46:52 +02:00
1720906ec8 repochecker/videos: Use subtitle track language as grammar check lang 2024-04-19 11:46:52 +02:00
a7309b6a00 repochecker/videos: Improve checks when dealing with translated exercices 2024-04-19 11:46:52 +02:00
6b74674123 repochecker/grammalecte: Reduce the avoided checks due to other lang 2024-04-19 11:46:52 +02:00
81ea38e2c1 Fix build on arm64 2024-04-19 11:46:52 +02:00
3cbb20a985 Revert "repochecker: grammalecte.net is down"
This reverts commit 9b3d9da2ac951c48b04bc8f9769b3ffa5cbc47d7.
2024-04-19 11:46:52 +02:00
aa0e7406c1 Detect theme and exercice language at runtime (not stored) 2024-04-19 11:46:52 +02:00
99cc79421f chore(deps): update module github.com/go-git/go-git/v5 to v5.5.2 2024-04-19 11:46:52 +02:00
ab2eb7f1cc chore(deps): update module golang.org/x/oauth2 to v0.4.0 2024-04-19 11:46:52 +02:00
935f2c0c24 chore(deps): update module golang.org/x/image to v0.3.0 2024-04-19 11:46:52 +02:00
87a0d1ccaa chore(deps): update module golang.org/x/crypto to v0.5.0 2024-04-19 11:46:52 +02:00
9b3087c8b1 chore(deps): update module github.com/gin-gonic/gin to v1.8.2 2024-04-19 11:46:52 +02:00
9f7b4e5498 Update dependency eslint-plugin-svelte3 to v4 2024-04-19 11:46:52 +02:00
0acafa24ac Update dependency eslint to v8 2024-04-19 11:46:52 +02:00
cad2bc09e4 ui: Update to sveltekit 1.0 + fix warnings 2024-04-19 11:46:52 +02:00
ef194d2cf3 ui: Update node packages 2024-04-19 11:46:52 +02:00
7cc8c65ab2 chore(deps): update module github.com/go-git/go-git/v5 to v5.5.1 2024-04-19 11:46:52 +02:00
dbe0984560 chore(deps): update dependency vite to v4 2024-04-19 11:46:52 +02:00
4b09efa9fd sync: Handle finished.md as alternative to finished.txt 2024-04-19 11:46:52 +02:00
2df227c25b chore(deps): update module golang.org/x/crypto to v0.4.0 2024-04-19 11:46:52 +02:00
baa410e654 sync: Import labels of Label flags as Markdown 2024-04-19 11:46:52 +02:00
7edbd0f9de repochecker: grammalecte.net is down 2024-04-19 11:46:52 +02:00
30a39fa9ef chore(deps): update module golang.org/x/image to v0.2.0 2024-04-19 11:46:52 +02:00
4b9e796341 chore(deps): update module github.com/go-sql-driver/mysql to v1.7.0 2024-04-19 11:46:52 +02:00
8db701889f chore(deps): update module golang.org/x/oauth2 to v0.3.0 2024-04-19 11:46:52 +02:00
93eed8d5e4 sync: Display import error in markdown processing 2024-04-19 11:46:52 +02:00
ecc9ae6ef1 repochecker/grammalecte: Add new words to dict 2024-04-19 11:46:52 +02:00
f079ecd9e3 repochecker/grammalecte: Allow redondances in resolution.md 2024-04-19 11:46:52 +02:00
f087213f0a repochecker/grammalecte: Allow * as all paragraphs 2024-04-19 11:46:52 +02:00
ec9a3a408d repochecker/grammalecte: Don't use HTML writer 2024-04-19 11:46:52 +02:00
84f85d631a repochecker/grammalecte: Fix CodeSpan 2024-04-19 11:46:52 +02:00
fee1ab2a26 repochecker/grammalecte: Add some new spelling exceptions 2024-04-19 11:46:52 +02:00
2381dfe4f5 repochecker/grammalecte: Refactor grammar passage extraction 2024-04-19 11:46:52 +02:00
ccc2c5d1d7 repochecker/grammalecte: Write new line only on paragraph exit 2024-04-19 11:46:52 +02:00
ea02fa4617 repochecker/grammalecte: Don't replace CodeSpan by text to avoid repetition 2024-04-19 11:46:52 +02:00
f0e6183c21 configs: Enable gzip_static module 2024-04-19 11:46:52 +02:00
3421286c9b repochecker/grammalecte: Check for forbidden strings (raw flags) in resolution.md 2024-04-19 11:46:52 +02:00
80422daffb repochecker/grammalecte: Check resolution.md 2024-04-19 11:46:51 +02:00
1f3f0fd55b admin: Transmit sync errors to interface 2024-04-19 11:46:51 +02:00
20f5656a74 repochecker/file-inspector: ZIP archive shouldn't contain Unix rootfs 2024-04-19 11:46:51 +02:00
bd19d31577 New attribute "disclaimer" on downloadable files 2024-04-19 11:46:51 +02:00
c28ad9533b repochecker/*-inspector: Refactor file opening 2024-04-19 11:46:51 +02:00
14f10c91db repochecker/*-inspector: Refactor file opening 2024-04-19 11:46:51 +02:00
257c594dbe sync: Expose GetFileSize 2024-04-19 11:46:51 +02:00
60d790f8d3 repochecker/file-inspector: Handle ZIP archives 2024-04-19 11:46:51 +02:00
abb277210c admin: Fix file download from admin interface 2024-04-19 11:46:51 +02:00
9bcf4a481e repochecker: Check gunziped file hash 2024-04-19 11:46:51 +02:00
6ca71230c1 Refactor sync file reading 2024-04-19 11:46:51 +02:00
541e32e10b sync: Return the reader from importer instead of writing to a given Writer 2024-04-19 11:46:51 +02:00
f4c3f1e15e sync: Create empty file for nginx gzip-static module 2024-04-19 11:46:51 +02:00
24a1e86c19 CD: Don't publish binaries from renovate branches 2024-04-19 11:46:51 +02:00
8cd15bc1d8 chore(deps): update alpine docker tag to v3.17 2024-04-19 11:46:51 +02:00
de245a14f4 chore(deps): update module golang.org/x/crypto to v0.3.0 2024-04-19 11:46:51 +02:00
d89f2b5b0f chore(deps): update module github.com/asticode/go-astisub to v0.23.0 2024-04-19 11:46:51 +02:00
85027166d7 repochecker: Fix dependency loop detection 2024-04-19 11:46:51 +02:00
0834bc4d13 chore(deps): update module github.com/asticode/go-astisub to v0.22.0 2024-04-19 11:46:51 +02:00
3a4003670f chore(deps): update module github.com/yuin/goldmark to v1.5.3 2024-04-19 11:46:51 +02:00
Antoine Thouvenin
643ecb1e14 nixos: backend server 2024-04-19 11:46:51 +02:00
83be5595ba repochecker/ip-inspector: gofmt + CI 2024-04-19 11:46:51 +02:00
f1a2e6c360 repochecker: New plugin ip-inspector 2024-04-19 11:46:51 +02:00
7eb56999a3 fickit used at FIC 2024-04-19 11:46:51 +02:00
95a7986e94 admin: Rephrase label 2024-04-19 11:46:51 +02:00
072637c8cd chore(deps): update github.com/studio-b12/gowebdav digest to 60ec5ad 2024-04-19 11:46:51 +02:00
26fb0c5554 chore(deps): update module golang.org/x/crypto to v0.2.0 2024-04-19 11:46:51 +02:00
22716fc51c chore(deps): update module golang.org/x/oauth2 to v0.2.0 2024-04-19 11:46:51 +02:00
a8fa58e111 repochecker: Add loop dependency detection 2024-04-19 11:46:51 +02:00
b784c448a0 Add some files to .dockerignore 2024-04-19 11:46:51 +02:00
0ffa126950 qa: Autorefresh myExercices + add button to trigger manual refresh 2024-04-19 11:46:51 +02:00
3049989ac6 qa: Include tasks in Todo list 2024-04-19 11:46:51 +02:00
cb489396ab qa: Handle errors with toaster 2024-04-19 11:46:51 +02:00
9044260a71 qa: Handle $FILES$ in paths 2024-04-19 11:46:51 +02:00
cd51246d9b qa: Use local bootstrap 2024-04-19 11:46:51 +02:00
5560a526b1 qa: Make some crappy hacks to let sveltekit work with baseurl 2024-04-19 11:46:51 +02:00
0e19b59452 qa: Improve design 2024-04-19 11:46:51 +02:00
13588fc634 qa: Add explaination on home page 2024-04-19 11:46:51 +02:00
67f129ce4c qa: Auto-solve OK requests 2024-04-19 11:46:51 +02:00
8758effc99 qa: Add spinners and rework 2024-04-19 11:46:51 +02:00
1aa82bb2ef qa: Back to the same situation 2024-04-19 11:46:51 +02:00
00f84e43ca qa: Use $lib 2024-04-19 11:46:51 +02:00
d002c2a4c5 qa: Update node modules 2024-04-19 11:46:51 +02:00
f49ff5aeda qa: svelte-migrate: renamed files 2024-04-19 11:46:51 +02:00
ee080c0666 qa: qa-svelte: auth ok 2024-04-19 11:46:51 +02:00
0fe037d7f5 qa-svelte: initial commit 2024-04-19 11:46:51 +02:00
abdf146fea qa: Use gin 2024-04-19 11:46:51 +02:00
9fd5564410 qa: Fix JSON in auth requests 2024-04-19 11:46:51 +02:00
7c2e97740f repochecker/grammalecte: Overload grammar paragraph in some situations 2024-04-19 11:46:51 +02:00
cd07bec05b admin: Use branch main instead of master 2024-04-19 11:46:51 +02:00
8e1b3bede0 ui: Cap large image to screen size 2024-04-19 11:46:51 +02:00
0e0b50d439 ui: Mark wip on theme page 2024-04-19 11:46:51 +02:00
c13fd3d0b1 ui: Redesign step attributs display 2024-04-19 11:46:51 +02:00
2ace5e1e52 ui: Center images on browsers supporting :has selector 2024-04-19 11:46:51 +02:00
ad7ad37e7f repochecker/grammalecte: add some new allowed words 2024-04-19 11:46:51 +02:00
ef999999ea repochecker/grammalecte: Fix flag label_majuscule exception and title_majuscule 2024-04-19 11:46:51 +02:00
c415e06237 libfic: Can indicate that an exercice is WIP 2024-04-19 11:46:51 +02:00
4b8e447b1b repochecker/grammalecte: Fix odd slice bounds out of range 2024-04-19 11:46:51 +02:00
ae1378780f admin/sync: Also handle uncompressed file in CheckExerciceFiles 2024-04-19 11:46:51 +02:00
5b47d1c250 repochecker/grammalecte: Don't harass on mc_mot_composé already flag as spelling exception 2024-04-19 11:46:51 +02:00
f9e9bfcb75 repochecker: fix numerous general issues with exception inheritance 2024-04-19 11:46:51 +02:00
057ce22fb9 repochecker/file-inspector: New checker 2024-04-19 11:46:51 +02:00
7a800b10de repochecker/epita: Ask to compress huge files 2024-04-19 11:46:51 +02:00
76ee40b7f1 repochecker/epita: Fix file format checking 2024-04-19 11:46:51 +02:00
b334122707 repochecker/epita: Check we have the original digest of compressed files 2024-04-19 11:46:51 +02:00
19daf69482 admin/sync: New syntax for flag dependency 2024-04-19 11:46:51 +02:00
cc37348aaa repochecker/epita: Check full numbered flag has type number 2024-04-19 11:46:51 +02:00
6a5119cb6a chore(deps): update github.com/studio-b12/gowebdav digest to 200a600 2024-04-19 11:46:51 +02:00
79c251d85f repochecker/epita: Check full numbered flag has type number 2024-04-19 11:46:51 +02:00
7b2603afb0 admin/sync: Able to filter on the second column 2024-04-19 11:46:51 +02:00
3d35cee67d ui: Solve scenario loading mess 2024-04-19 11:46:51 +02:00
ffd43ac8e1 ui: Refactor stores 2024-04-19 11:46:51 +02:00
9d171cfe89 libfic: Fix my.json generation when number flag are present 2024-04-19 11:46:51 +02:00
50b3e4c739 repochecker/grammalecte: Fix out of bound array 2024-04-19 11:46:51 +02:00
98939e9d61 repochecker/grammalecte: Check labels and titles have upper case 2024-04-19 11:46:51 +02:00
960122dfb6 Justified MCQ are back! 2024-04-19 11:46:51 +02:00
a28f108b8a db: Add a published attribute, filled by challenge.txt 2024-04-19 11:46:51 +02:00
6b7ed273b7 db: Add cksum_shown field to files in order to store second checksum in case of gziped content 2024-04-19 11:46:51 +02:00
91b2daea2e repochecker/videos: Also check video ratio 2024-04-19 11:46:51 +02:00
38a4e21e28 admin/sync: Use globbing for ack handling 2024-04-19 11:46:51 +02:00
5d716106c4 repochecker/videos: Also check grammar in subtitles 2024-04-19 11:46:51 +02:00
acdf0a6261 repochecker/grammalecte: Extract struct in a dedicated lib 2024-04-19 11:46:50 +02:00
23ac512ce6 admin/sync: Keep Exceptions from multiple files 2024-04-19 11:46:50 +02:00
ac25202024 repochecker/grammalecte: Allow including quotes without checking 2024-04-19 11:46:50 +02:00
d7ff10762e repochecker/grammalecte: Add a custom hook CheckGrammar 2024-04-19 11:46:50 +02:00
5d07634d0d admin/sync: Add custom hooks that plugins can register and call 2024-04-19 11:46:50 +02:00
d48dd2647c repochecker: Introducing new plugin videos-rules.so to check resolution.mp4 2024-04-19 11:46:50 +02:00
4647ff033c repochecker/grammalecte: Add "overflow" as allowed word 2024-04-19 11:46:50 +02:00
070684d618 repochecker/grammalecte: Add "root" as globally allowed word 2024-04-19 11:46:50 +02:00
a4701af619 repochecker/grammalecte: Remove unwanted poncfin_règle1 for labels 2024-04-19 11:46:50 +02:00
a790ced236 admin/sync: Removes windows \r in exceptions 2024-04-19 11:46:50 +02:00
56e41cad6a admin: Include grammalecte-rules.so in Dockerfile 2024-04-19 11:46:50 +02:00
adebdd180d admin: Fix sync report display 2024-04-19 11:46:50 +02:00
cea3c13369 repochecker/grammalecte: Add some words 2024-04-19 11:46:50 +02:00
fb368d79d1 sync: Introduce repochecker-ack.txt to support check exceptions 2024-04-19 11:46:50 +02:00
edde9f885d repochecker/grammalecte: New plugin to check french grammar 2024-04-19 11:46:43 +02:00
721908ee18 libfic: Refactor flag label parsing 2024-04-19 11:46:10 +02:00
2b26b2b819 repochecker/epita: Fix nil pointer exception 2024-04-19 11:46:10 +02:00
d791e74a2a ui: Fix errors after migration 2024-04-19 11:46:10 +02:00
47776eeeb4 ui: Update node modules 2024-04-19 11:46:10 +02:00
3a6daa3d04 ui: Use $lib instead of ../../../../ mess 2024-04-19 11:46:10 +02:00
3cf92b4798 svelte-migrate: updated files 2024-04-19 11:46:06 +02:00
ca12b3dde5 svelte-migrate: renamed files 2022-10-29 17:23:58 +02:00
e86a2c9be2 chore(deps): update module github.com/gin-gonic/gin to v1.8.1 2022-10-29 17:23:58 +02:00
92d5c0bd64 Switch to go 1.18 2022-10-29 17:23:54 +02:00
9a6dd6e360 chore(deps): update module github.com/burntsushi/toml to v1.2.1 2022-10-29 17:23:18 +02:00
9ed4a918d4 chore(deps): update node docker tag to v19 2022-10-29 17:23:18 +02:00
d028a2e50a chore(deps): update module golang.org/x/oauth2 to v0.1.0 2022-10-29 17:23:18 +02:00
b5193d5bac chore(deps): update module golang.org/x/crypto to v0.1.0 2022-10-29 17:23:18 +02:00
ad5d74cfe3 chore(deps): update module golang.org/x/image to v0.1.0 2022-10-29 17:23:18 +02:00
06c7067b7a chore(deps): update golang.org/x/image digest to ffcb3fe 2022-10-29 17:23:16 +02:00
be4cfb8a38 chore(deps): update github.com/studio-b12/gowebdav digest to 17255f2 2022-10-29 17:22:55 +02:00
503da6cc4f chore(deps): update golang.org/x/oauth2 digest to 6fdb5e3 2022-10-29 17:22:55 +02:00
eaa73fcb85 chore(deps): update github.com/studio-b12/gowebdav digest to 8190232 2022-10-29 17:22:55 +02:00
0f97e07649 chore(deps): update golang.org/x/crypto digest to 56aed06 2022-10-29 17:22:55 +02:00
16168df123 Follow SvelteKit 2022-10-29 17:22:55 +02:00
1297fe2a39 repochecker-epita: Special case for number flag 2022-10-29 17:22:55 +02:00
64feef8b95 libfic: New function to analyze number flag 2022-10-29 17:22:55 +02:00
6aead541d6 chore(deps): update golang.org/x/crypto digest to eccd636 2022-10-29 17:22:55 +02:00
85b47afa6a chore(deps): update module github.com/yuin/goldmark to v1.5.2 2022-10-29 17:22:55 +02:00
044e38172b chore(deps): update module github.com/yuin/goldmark to v1.5.0 2022-10-29 17:22:55 +02:00
0accd98e5c chore(deps): update golang.org/x/crypto digest to 4ba4fb4 2022-10-29 17:22:55 +02:00
e0b34c8043 chore(deps): update module github.com/yuin/goldmark to v1.4.15 2022-10-29 17:22:55 +02:00
1b3a32b65e chore(deps): update golang.org/x/crypto digest to 35f4265 2022-10-29 17:22:55 +02:00
7d31bb3312 chore(deps): update module github.com/yuin/goldmark to v1.4.14 2022-10-29 17:22:55 +02:00
7daf4e07cc chore(deps): update golang.org/x/oauth2 digest to f213421 2022-10-29 17:22:55 +02:00
c982576625 chore(deps): update golang.org/x/image digest to e7cb969 2022-10-29 17:22:55 +02:00
5678efa5c4 chore(deps): update golang.org/x/crypto digest to c86fa9a 2022-10-29 17:22:55 +02:00
60aafc5997 chore(deps): update golang.org/x/crypto digest to 5757bc0 2022-10-29 17:22:55 +02:00
ad993e4e4c chore(deps): update golang.org/x/oauth2 digest to 0ebed06 2022-10-29 17:22:55 +02:00
32582f576b chore(deps): update golang.org/x/crypto digest to bc19a97 2022-10-29 17:22:54 +02:00
198314c2f5 chore(deps): update golang.org/x/oauth2 digest to 8227340 2022-10-29 17:22:54 +02:00
26fb064a81 renovate: Update linuxkit images 2022-10-29 17:22:54 +02:00
817f43ee7a chore(deps): update module github.com/burntsushi/toml to v1.2.0 2022-10-29 17:22:54 +02:00
8e4e60d9d0 chore(deps): update golang.org/x/oauth2 digest to 128564f 2022-10-29 17:22:54 +02:00
737207d59a chore(deps): update golang.org/x/image digest to 062f8c9 2022-10-29 17:22:54 +02:00
51ff2ef3d6 chore(deps): update golang.org/x/crypto digest to 630584e 2022-10-29 17:22:54 +02:00
1dd41fcb3a chore(deps): update golang.org/x/oauth2 digest to c8730f7 2022-10-29 17:22:54 +02:00
b5fff619b8 chore(deps): update golang.org/x/oauth2 digest to 2104d58 2022-10-29 17:22:52 +02:00
a3f229d56e chore(deps): update golang.org/x/image digest to 41969df 2022-10-29 17:22:25 +02:00
211facf9d0 chore(deps): update golang.org/x/crypto digest to 0559593 2022-10-29 17:22:25 +02:00
e3e8e86b4c chore(deps): update module github.com/yuin/goldmark to v1.4.13 2022-10-29 17:22:23 +02:00
e2b47e744d ui: Update node version and node packages 2022-10-29 17:21:50 +02:00
41fb5a1cd0 backend: Don't fail if an hint is already opened 2022-10-29 17:21:50 +02:00
9bc9851b12 CI: ignore failures for docker arm64 2022-10-29 17:21:50 +02:00
95c992555c admin: Fix marshal of error in SyncReport 2022-10-29 17:21:50 +02:00
86d9a039c8 epita-rules: Requires placeholder for each flag 2022-10-29 17:21:50 +02:00
a85c2efa45 admin: Fix reset exercices 2022-10-29 17:21:50 +02:00
db9d7edf6b epita-rules: Checks that CVE- flag are UCQ, and number of choices
Fixes: #26
2022-10-29 17:21:50 +02:00
8aba067d05 settings: Convert JSON strings to the given type 2022-10-29 17:21:50 +02:00
95aadffb2e fic: Use user order to sort exercices in interface 2022-10-29 17:21:50 +02:00
c78545c18b sync: Report custom errors 2022-10-29 17:21:50 +02:00
08ea1bac0d admin: Arrange unlockedChallengeUpTo field 2022-10-29 17:21:50 +02:00
a0013947dd admin: Disable syncVideos when prod mod enabled 2022-10-29 17:21:50 +02:00
c7968fb256 admin: Add button to switch from WIP to PROD 2022-10-29 17:21:50 +02:00
c34fe51641 admin: config is not defined on sync page, use settings instead 2022-10-29 17:21:50 +02:00
e84b1d67cb Fix go vet errors 2022-10-29 17:21:50 +02:00
b0129e5239 sync: Use errors instead of string to report 2022-10-29 17:21:49 +02:00
d8943ba1f3 admin: Improve resolutions.json 2022-10-29 17:21:49 +02:00
Élie BRAMI
8afbacd654 build: reduce nix cache miss.
Each pkgs.buildGoModule create a .go-modules sub derivation that is equivalent
to the vendor directory (aka it is copied to vendor). Before we have a cache
miss because this sub derivation has another name but it is the same (comapared
with nix-diff) here we rename this sub derivation to hit the network only once.

before:
$ nix build .#all --dry-run
these 15 derivations will be built:
  /nix/store/64z5ixzsfd536jkybap6364wlkm4s8jn-admin-202208232340-go-modules.drv
  /nix/store/hjj8dv97h00y5wfvw4xk0ax4745473zl-frontend-202208232340-go-modules.drv
  /nix/store/xrx1b6hnvq13qkijqrbw7jsbr3gf5fn8-dashboard-202208232340-go-modules.drv
  /nix/store/kv0spqnh3il0lgimg7gk40cxh5gspvkp-dashboard-202208232340.drv
  /nix/store/mpa6f97mrvfq7nd0vrwgsq9z2k3hhzgw-scores-sync-zqds-202208232340-go-modules.drv
  /nix/store/nl1p2k58rdazvjhpifc76948x5bjwr0f-repochecker-202208232340-go-modules.drv
  /nix/store/pc290pk0rxhm2vrrbn78f92r3k7mmhzx-backend-202208232340-go-modules.drv
  /nix/store/p5v7ghgndp78gx7g653kdw4yasyxnlad-backend-202208232340.drv
  /nix/store/sj3vv4sxva1rkrxrb6k7p66vq1b5nlgq-qa-202208232340-go-modules.drv
  /nix/store/q3f00kq76iq453awbihgh30r8j9np7g0-qa-202208232340.drv
  /nix/store/q6zcphpyqlk5yc9r0z9c4lbayvr1rnm6-scores-sync-zqds-202208232340.drv
  /nix/store/r4bq4b8g0pjn3k0r9wh70divbm9m7m3v-frontend-202208232340.drv
  /nix/store/wffyj2nbgzi0qmv6iqx0kn0hzzm41i61-repochecker-202208232340.drv
  /nix/store/yfisx4c6zhqcqyflbj064s0p60930v82-admin-202208232340.drv
  /nix/store/vk06zp9q2hs6149n363hmk8azfhxr683-fic-all.drv
this path will be fetched (0.01 MiB download, 0.04 MiB unpacked):
  /nix/store/7cifvbmgjm5y9ds5a7c6c861g1xcm1qr-stdenv-linux

after:
$ nix build .#all --dry-run
these 9 derivations will be built:
  /nix/store/826caq9p8rphffwyrvcpxgrs6p3xla13-fic-.-.-202208232340-go-modules.drv
  /nix/store/29gr8z8zm1qxd3hg4nfj5zpz39w5239j-qa-202208232340.drv
  /nix/store/2clvljkrlpfxfrlb4cyk68n8zq5xg3m4-dashboard-202208232340.drv
  /nix/store/5nz6izdjhsklq354m1fdssbadr2fnpkk-repochecker-202208232340.drv
  /nix/store/95pzd7y6n99v4dn4xyfi677kmdq536qg-frontend-202208232340.drv
  /nix/store/i79s2fkj5g75729lg76b2i126m7q36w9-scores-sync-zqds-202208232340.drv
  /nix/store/q1v7hq02hmqn4nql5rcyiwf6grlrdc4q-admin-202208232340.drv
  /nix/store/vkid4pr7r6w3glfhfyfg5malp501jkaf-backend-202208232340.drv
  /nix/store/f94c2m5fq6il03f97lyswcwg15s3c50w-fic-all.drv
this path will be fetched (0.01 MiB download, 0.04 MiB unpacked):
  /nix/store/7cifvbmgjm5y9ds5a7c6c861g1xcm1qr-stdenv-linux
2022-08-27 00:43:15 +02:00
Élie BRAMI
af388dbfff flakes: Add .#all target. 2022-08-24 01:40:00 +02:00
Élie BRAMI
04a9e3b3b6 deps: Update vendorSha256 with script. 2022-08-24 00:51:49 +02:00
Élie BRAMI
a3144fac45 feat: Avoid question in label.
All checks were successful
continuous-integration/drone/push Build is passing
2022-06-12 23:12:23 +02:00
05f396f5c9 Remove all favicon
Some checks failed
continuous-integration/drone/push Build is failing
2022-06-12 14:48:55 +02:00
e0034a1fc1 Add .dockerignore 2022-06-12 13:18:18 +02:00
b26ef1c0ce ui: Home button redirect to main_link 2022-06-12 13:18:18 +02:00
223f44572e admin: Can import videos 2022-06-12 13:18:18 +02:00
dfe62e0b97 sync: Also allow overview.md for themes 2022-06-12 12:06:02 +02:00
cf75367b5b ui: Fix main_logo loading 2022-06-12 12:06:02 +02:00
01a9bb2e94 ui: Prefix $FILES$ by base path 2022-06-12 12:06:02 +02:00
4cce95245e dockerfile: Update to node 16 + alpine 3.15 2022-06-12 12:06:02 +02:00
d69c062d40 frontend: Fix chbase.sh using new version of svelte 2022-06-10 18:56:00 +02:00
9f45f10775 sync: Also ignore theme directories starting by _ 2022-06-10 18:54:25 +02:00
0f75b71f5f repochecker: Don't prefix stderr messages
Some checks failed
continuous-integration/drone/push Build is failing
2022-06-10 16:54:13 +02:00
7b300d4ffe backend: Check file error
Some checks failed
continuous-integration/drone/tag Build is failing
continuous-integration/drone/push Build is failing
2022-06-08 17:13:41 +02:00
32d003f7b7 admin: Fix public retrieval 2022-06-08 16:49:28 +02:00
4b1b5445f7 backend: Don't consider non error in MCQ as good response 2022-06-08 16:49:28 +02:00
0a8d0dad30 Can Unlock challenge up to a certain level 2022-06-08 16:47:03 +02:00
e922171f17 evdist: Fix some segv 2022-06-08 12:31:08 +02:00
7a5c1eeba7 admin: Fix flag edition 2022-06-08 12:24:38 +02:00
38857054ba dashboard: Fix dockerfile 2022-06-08 11:23:55 +02:00
36af72d616 admin: Fix team symlink for dex generation 2022-06-08 11:22:30 +02:00
498e3c5b63 evdist: Chmod temporary files 2022-06-08 10:00:21 +02:00
30a665ff72 Add theodore keys 2022-06-08 10:00:01 +02:00
750db69b06 settings: Can display a global message on all pages 2022-06-08 09:12:56 +02:00
e9dd35f8ac settings: Can disable all submission button for maintenance 2022-06-08 09:12:56 +02:00
329bd246c7 admin: Add stats about submissions rate 2022-06-08 04:02:06 +02:00
116c061715 dashboard: Update 2022-06-08 03:39:50 +02:00
9ea415b857 admin: Fix nil pointer when seeing public team 2022-06-08 03:06:29 +02:00
159672ec47 admin: Don't erase challenge.json if already exists 2022-06-08 03:00:50 +02:00
cfde1689cc Remove from frontend the settings distribution role 2022-06-08 02:57:29 +02:00
af6e86d4ef evdist: New project to handle settings programming 2022-06-08 02:57:29 +02:00
0831ea6088 remote-challenge-sync-airbus: WIP 2022-06-08 02:57:29 +02:00
a82defe2a7 fickit: Add missing gateway 2022-06-08 02:57:29 +02:00
a7dda3a999 fickit: Reenable dhcpd on admin 2022-06-08 02:57:29 +02:00
cdc342bea3 fickit: Missing SYNC dir 2022-06-08 02:57:29 +02:00
d2d7b35623 remote-challenge-sync-airbus: Handle interrupts 2022-06-08 02:57:29 +02:00
cc1b212cca remote-challenge-sync-airbus: Add inotify watcher 2022-06-08 02:57:29 +02:00
367e686e8a remote-challenge-sync-airbus: WIP 2022-06-07 16:18:48 +02:00
6d9fd1ff12 libfic: Update ScoreGridFormat format and expose stats 2022-06-07 16:05:41 +02:00
6aa0f4da95 ui: Use a PNG favicon 2022-06-07 12:42:52 +02:00
ba096c0af1 admin: Able to reset issues, QA and events 2022-06-07 12:37:35 +02:00
9a2fd85d57 sync: Unneeded log
Some checks failed
continuous-integration/drone/push Build is failing
2022-06-07 01:06:31 +02:00
83a579fbd2 admin: Don't fail if importer is not writable
All checks were successful
continuous-integration/drone/push Build is passing
2022-06-06 23:00:03 +02:00
1591ec4376 CI: Remove remote-challenge-sync-airbus temporaly
Some checks are pending
continuous-integration/drone/push Build is running
2022-06-06 21:25:40 +02:00
11a12e1d44 Import logos from challenge.json
Some checks are pending
continuous-integration/drone/push Build is running
2022-06-06 20:42:46 +02:00
f4188ec289 fix typo
Some checks failed
continuous-integration/drone/push Build is failing
Thanks-to: Elie Brami <elie.brami@epita.fr>
2022-06-06 15:13:33 +02:00
39acdee6b2 ui: Display score grid in team page
Some checks are pending
continuous-integration/drone/push Build is running
2022-06-06 14:40:18 +02:00
46d1bb21f7 backend: Also generate scores.json for each team 2022-06-06 13:01:09 +02:00
bfdb1c2bf7 Introduce remote-challenge-sync-airbus 2022-06-06 12:56:07 +02:00
cf502bd9d5 fickit: Allow connections to admin only from local (through ssh) 2022-06-06 11:27:24 +02:00
cf7482a14a configs: Update SSH keys 2022-06-06 11:26:39 +02:00
f2e7b30ace dashboard: Autoreload ip restricted list 2022-06-06 11:26:23 +02:00
0e6a6893f1 dashboard: Can restrict access by IP 2022-06-06 11:02:35 +02:00
437ed4d393 dashboard: Can restrict access through htpasswd 2022-06-06 10:49:14 +02:00
635e67c224 dashboard: Use gin-gonic instead of httprouter directly 2022-06-06 10:49:13 +02:00
8cb7bf8b96 chore(deps): update dependency sass-loader to v13
All checks were successful
continuous-integration/drone/push Build is passing
2022-06-06 07:23:37 +00:00
046b38cc32 frontend: Use latest node version
All checks were successful
continuous-integration/drone/push Build is passing
2022-06-04 18:47:13 +02:00
58af047a26 admin: Pick challenge title from challenge.json 2022-06-04 18:21:41 +02:00
d09c1741a2 admin: Also generate associations when generating dex.yaml 2022-06-04 18:11:10 +02:00
2ce95ccafc fickit: Fix IP and ifaces 2022-06-04 18:09:33 +02:00
59de4f66d8 fickit: Colorize prompt
All checks were successful
continuous-integration/drone/push Build is passing
2022-06-04 14:59:32 +02:00
722295989c fickit: Use DHCP during preparation and update 2022-06-04 14:59:32 +02:00
b92b107efe fickit: Add kexec image 2022-06-04 14:59:32 +02:00
d883926647 fickit: Update boot image 2022-06-04 14:59:32 +02:00
68bd43c7ce fickit: Add kexec 2022-06-04 14:59:32 +02:00
8d8d8d7c82 fickit: Define hostname 2022-06-04 14:59:32 +02:00
152bbe178f fickit: Handle raid and non-raid setup 2022-06-04 14:59:32 +02:00
f61b0a8e47 synchro: Start the synchronization by performing time sync 2022-06-04 14:59:32 +02:00
68fb332ed1 admin: Fix segv when settings.json doesn't exist
All checks were successful
continuous-integration/drone/push Build is passing
2022-06-01 22:49:43 +02:00
595318e7b1 admin: Fix summary table on home page
All checks were successful
continuous-integration/drone/push Build is passing
2022-06-01 13:25:58 +02:00
b7a36f906c fickit: Update images 2022-06-01 12:56:43 +02:00
a414cd22c8 Handle optionnal flags 2022-06-01 12:56:43 +02:00
e581630d5e chore(deps): update dependency alpine to v3.16 2022-05-31 22:23:19 +02:00
f0902dc023 chore(deps): update golang.org/x/oauth2 digest to 622c5d5 2022-05-31 22:23:19 +02:00
549f96535c chore(deps): update golang.org/x/crypto digest to 793ad66 2022-05-31 22:23:19 +02:00
eca15b394a ui: Upgrade packages 2022-05-31 22:23:19 +02:00
fafb778c9d ui: Use logos from challenge.info 2022-05-31 19:11:28 +02:00
cd03b99f9b dashboard: Take information from challenge.json 2022-05-31 18:42:41 +02:00
c0260da035 admin: Add resync button on theme 2022-05-31 18:41:42 +02:00
f65375b01f admin: Handle more info in challenge.json 2022-05-31 18:18:08 +02:00
4c84038b28 password_paper: Update 2022-05-31 16:42:17 +02:00
1856a78d10 admin: Improve title and toasts rendering 2022-05-31 16:41:36 +02:00
fbeb2cc42b admin: Update fill_teams.sh 2022-05-31 14:54:34 +02:00
70891bf0e9 admin: Fix old routes 2022-05-31 14:54:19 +02:00
6c31820178 admin: Return the updated team struct after password regeneration 2022-05-31 14:53:53 +02:00
8fd2a70894 admin: Fix nil dereference when asking password 2022-05-31 14:53:26 +02:00
ad41513654 Add Nix flakes
All checks were successful
continuous-integration/drone/push Build is passing
2022-05-31 00:09:27 +02:00
48895af3e8 ui: Don't change page title if challenge info are not loaded
All checks were successful
continuous-integration/drone/push Build is passing
2022-05-27 20:26:38 +02:00
5d25024481 old fix 2022-05-27 17:11:51 +02:00
5bfd1574f2 chore(deps): update dependency eslint-plugin-svelte3 to v4
Some checks are pending
continuous-integration/drone/push Build is running
2022-05-27 16:23:33 +02:00
565c55754d chore(deps): update module github.com/yuin/goldmark to v1.4.12 2022-05-27 16:23:33 +02:00
243f16d2b8 chore(deps): update golang.org/x/crypto digest to 6f7dac9 2022-05-27 16:23:33 +02:00
bd35705f58 admin: Make menu items active on rights pages 2022-05-27 16:23:33 +02:00
3c237819c3 settings: Save future changes in a dedicated file 2022-05-27 16:23:33 +02:00
465a48c1c0 admin: Show diff on settings form 2022-05-26 13:01:59 +02:00
eb07eadae0 admin: Copy challenge.json from sync to distsettings 2022-05-26 12:26:53 +02:00
4b2625c47d admin: Fix toast that wasn't hidden on button click 2022-05-26 11:37:43 +02:00
123467f3eb settings: Save duration in challenge.json 2022-05-24 23:09:43 +02:00
58217d1d8a admin: Save challenge info over importer 2022-05-24 22:57:16 +02:00
aab66bf612 sync: Implement writable importer 2022-05-24 22:57:16 +02:00
8ed9415c68 admin: Read challenge.json from imported directory 2022-05-24 21:54:45 +02:00
560110ba5e sync: Expose GetFile and GetFileContent functions 2022-05-24 21:52:58 +02:00
d24b1c5d4d libfic: Use MEDIUMTEXT to store resolution.md 2022-05-24 21:30:11 +02:00
2c76b5c7a3 admin: Add link to forge 2022-05-24 21:25:51 +02:00
80917ae436 admin: New page to list tags 2022-05-24 21:25:27 +02:00
a6adc1ac8c ui: Display writeup in interface 2022-05-24 17:53:44 +02:00
45a9240834 Handle special chars in exercice path 2022-05-24 17:36:33 +02:00
3bf0fc69ee admin: Handle resolution.md display 2022-05-24 13:32:02 +02:00
4a190f51c5 admin: Fix video route 2022-05-24 12:03:00 +02:00
b92381f007 admin/ui: Improve home page 2022-05-24 12:03:00 +02:00
8eb2bda539 admin/ui: Improve sync page 2022-05-24 12:03:00 +02:00
9fe66c563b admin/ui: Split settings page into sync and settings pages 2022-05-24 12:03:00 +02:00
70bad90756 sync: Handle overview.md and statement.md 2022-05-24 12:03:00 +02:00
630c065825 ui: When enter is pressed on vector flag, add an item 2022-05-22 19:10:17 +02:00
7cdca440e6 ui: Ensure images in statement fit container 2022-05-22 19:10:17 +02:00
f690a4e1c8 sync: Use goldmark instead of blackfriday 2022-05-22 19:10:17 +02:00
72add55723 ui: Open PDF, JPG, PNG, TXT in another tab 2022-05-22 19:10:17 +02:00
8b3fbdb64a admin: Use gin-gonic as router 2022-05-22 19:10:17 +02:00
83468ad723 admin: Fix toast with yes/no after sync 2022-05-17 18:18:33 +02:00
9d639a0315 sync: Non-empty directory without .git is Fatal 2022-05-17 18:18:33 +02:00
3c0751a78a sync: Fix division by zero 2022-05-17 18:18:33 +02:00
c25c11e70a chore(deps): update golang.org/x/crypto digest to 403b017
All checks were successful
continuous-integration/drone/push Build is passing
2022-05-16 17:27:52 +00:00
5a5e02e533 chore(deps): update golang.org/x/crypto digest to 4661260
All checks were successful
continuous-integration/drone/push Build is passing
2022-05-13 21:28:53 +00:00
a9b3a45fb1 chore(deps): update golang.org/x/crypto digest to c6db032
All checks were successful
continuous-integration/drone/push Build is passing
2022-05-11 20:25:50 +00:00
9cf33ee755 repochecker: Revert binary file checks
All checks were successful
continuous-integration/drone/push Build is passing
2022-05-10 10:23:52 +02:00
3fa53bc877 chore(deps): update golang.org/x/crypto digest to 2cf3ade
All checks were successful
continuous-integration/drone/push Build is passing
2022-05-07 03:26:25 +00:00
1313f2caf9 Include ID and dependancy in zqds file
All checks were successful
continuous-integration/drone/push Build is passing
2022-05-02 18:42:19 +02:00
53e70b1eba admin: Can reset challengeInfo 2022-05-02 18:42:19 +02:00
c525acff20 settings: Add challenge subtitle 2022-05-02 18:42:19 +02:00
48ee5321a8 admin: Handle challenge info on settings page 2022-05-02 18:42:19 +02:00
c713a0a25d ui: Update node modules 2022-05-02 18:42:19 +02:00
dff4f4eb63 Distribute and handle challenge.json 2022-05-02 18:42:19 +02:00
e8f6a03cd9 settings: Rename struct to remove FIC occurence 2022-05-01 22:15:16 +02:00
457cd307dd admin: Can extract exercices as ZQDS session.yml 2022-05-01 22:05:26 +02:00
15afbb8b87 settings: Use pointer 2022-05-01 21:32:19 +02:00
dc64eb549f chore(deps): update golang.org/x/oauth2 digest to 9780585
All checks were successful
continuous-integration/drone/push Build is passing
2022-04-27 21:21:19 +00:00
de4e97f033 chore(deps): update golang.org/x/image digest to 70e8d0d
All checks were successful
continuous-integration/drone/push Build is passing
2022-04-27 19:26:04 +00:00
86aaf5f519 chore(deps): update golang.org/x/crypto digest to eb4f295
All checks were successful
continuous-integration/drone/push Build is passing
2022-04-27 18:22:42 +00:00
14281b5485 chore(deps): update module github.com/burntsushi/toml to v1.1.0
All checks were successful
continuous-integration/drone/push Build is passing
2022-04-05 00:12:16 +00:00
fa7dc0b9b7 chore(deps): update golang.org/x/crypto digest to ae2d966
All checks were successful
continuous-integration/drone/push Build is passing
2022-03-31 23:13:21 +00:00
b5cb3b92ec chore(deps): update golang.org/x/crypto commit hash to 2c7772b
All checks were successful
continuous-integration/drone/push Build is passing
2022-03-24 23:07:40 +00:00
840e9c3534 chore(deps): update golang.org/x/image commit hash to a8550c1
All checks were successful
continuous-integration/drone/push Build is passing
2022-03-23 14:35:17 +00:00
c11eb8a315 CI: Fix compilation with go 1.18
Some checks failed
continuous-integration/drone/push Build is failing
2022-03-21 14:22:26 +01:00
b302de9218 chore(deps): update golang.org/x/crypto commit hash to 3147a52
All checks were successful
continuous-integration/drone/push Build is passing
2022-03-15 17:28:59 +00:00
85b2703d4a chore(deps): update golang.org/x/crypto commit hash to 5d542ad
All checks were successful
continuous-integration/drone/push Build is passing
2022-03-15 00:23:19 +00:00
cb0e503b1f chore(deps): update golang.org/x/crypto commit hash to b769efc
All checks were successful
continuous-integration/drone/push Build is passing
2022-03-13 01:18:46 +00:00
2539882b48 chore(deps): update golang.org/x/crypto commit hash to 6068a2e
All checks were successful
continuous-integration/drone/push Build is passing
2022-03-12 14:16:49 +00:00
c8fb852157 chore(deps): update golang.org/x/oauth2 commit hash to 6242fa9
All checks were successful
continuous-integration/drone/push Build is passing
2022-03-09 16:17:41 +00:00
18b5aca547 chore(deps): update golang.org/x/crypto commit hash to efcb850
All checks were successful
continuous-integration/drone/push Build is passing
2022-03-07 22:17:32 +00:00
2a85c55e18 chore(deps): update golang.org/x/image commit hash to 723b81c
All checks were successful
continuous-integration/drone/push Build is passing
2022-03-02 10:15:07 +00:00
d7247d6854 chore(deps): update golang.org/x/oauth2 commit hash to ee48083
All checks were successful
continuous-integration/drone/push Build is passing
2022-02-23 16:15:17 +00:00
70cd7ab571 chore(deps): update golang.org/x/crypto commit hash to 8634188
All checks were successful
continuous-integration/drone/push Build is passing
2022-02-14 21:14:53 +00:00
d97187af93 chore(deps): update golang.org/x/crypto commit hash to 1e6e349
All checks were successful
continuous-integration/drone/push Build is passing
2022-02-13 20:14:45 +00:00
45672db8b5 chore(deps): update golang.org/x/crypto commit hash to f4118a5
All checks were successful
continuous-integration/drone/push Build is passing
2022-02-10 16:14:09 +00:00
ad39e0fff5 chore(deps): update golang.org/x/crypto commit hash to db63837
All checks were successful
continuous-integration/drone/push Build is passing
2022-02-09 20:16:48 +00:00
63e775308f chore(deps): update golang.org/x/crypto commit hash to dad3315
All checks were successful
continuous-integration/drone/push Build is passing
2022-02-09 16:17:21 +00:00
2f748dc3f7 chore(deps): update golang.org/x/crypto commit hash to bba287d
All checks were successful
continuous-integration/drone/push Build is passing
2022-02-09 00:11:54 +00:00
38eaec6edc renovate: Automerge some go packages
All checks were successful
continuous-integration/drone/push Build is passing
2022-02-08 07:00:27 +01:00
1613b1780d chore(deps): update golang.org/x/crypto commit hash to 20e1d8d
Some checks are pending
continuous-integration/drone/push Build is running
2022-02-08 05:14:47 +00:00
7a7f90eeda fic: Refactor CountTries function to fix timeouted submission
All checks were successful
continuous-integration/drone/push Build is passing
2022-02-04 17:33:11 +01:00
40a9b0d187 fic: Define gain even if the estimation fails 2022-02-04 17:32:55 +01:00
25e2e60092 Overwrite column name
All checks were successful
continuous-integration/drone/push Build is passing
2022-02-03 17:37:10 +01:00
0355ec1a54 Improve logging of gain estimation errors 2022-02-03 17:36:59 +01:00
5e4c14c634 admin: Make propagation time smarter
Some checks are pending
continuous-integration/drone/push Build is running
2022-02-03 16:56:34 +01:00
2cd40e64ab admin: Add description to fields 2022-02-03 16:53:59 +01:00
02bd5f316a admin: When deleting team, also delete associations 2022-02-03 10:16:52 +01:00
2ca2018485 ui: Ensure jTeam, defined by RegistrationFormJoinTeam is not defined 2022-02-03 10:00:41 +01:00
277258814b ui: Fix dropdown alignment 2022-02-03 10:00:41 +01:00
1dcde1ba10 ui: Update FIC logotype 2022-02-03 10:00:41 +01:00
2b87449475 ui: Ensure $teams is correctly populated 2022-02-03 10:00:41 +01:00
5d36c8a2c2 ui: Update node packages 2022-02-03 10:00:41 +01:00
1d36de028d chore(deps): update github.com/studio-b12/gowebdav commit hash to c7b1ff8
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-31 22:15:00 +00:00
59ff2e5a3a chore(deps): update golang.org/x/crypto commit hash to 30dcbda
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-31 20:19:35 +00:00
b1961c7102 chore(deps): update module github.com/burntsushi/toml to v1
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-23 20:50:06 +00:00
3e8ab0bd1a chore(deps): update golang.org/x/crypto commit hash to 5e0467b
Some checks are pending
continuous-integration/drone/push Build is running
2022-01-23 16:10:01 +00:00
86748b36c8 sync: Use git reset --hard --recurse-submodule
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-22 08:10:53 +01:00
d4a81aa660 sync: Improve git sync reliability
Some checks are pending
continuous-integration/drone/push Build is running
2022-01-22 07:51:02 +01:00
01b05aaed0 Implement label only flag
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-21 13:26:52 +01:00
b98e23d060 Include ç and Ç in urlid 2022-01-21 12:12:35 +01:00
892bb99461 admin: Disable PKI regeneration in prod
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-21 11:09:16 +01:00
63b0ad4885 ui: Increase time between checks
Some checks are pending
continuous-integration/drone/push Build is running
2022-01-21 10:50:32 +01:00
76ccd25ae3 ui: Detect my refresh with more accuracy 2022-01-21 10:40:36 +01:00
ce41ab76eb Upgrade bootstrap to 4.6.1
This fixes a bug with toast
2022-01-21 09:59:13 +01:00
b1315a9eeb admin: Auto change default value for unlockedChallengeDepth 2022-01-21 09:17:46 +01:00
15c85c8f59 admin: Add setting to differenciate real challenge from common tests 2022-01-21 09:10:18 +01:00
252ff33b83 sync: Allow Markdown in flag help 2022-01-21 09:00:22 +01:00
5a79343af8 Implement sort_regexp_validator_groups 2022-01-21 08:44:51 +01:00
081ad1f928 fic: SaveNamedExercice on title and path 2022-01-21 08:02:23 +01:00
40a9078b70 sync: Handle title.txt in exercice dir 2022-01-21 08:02:23 +01:00
63d8ae4ecd settings: Add an option to show MCQ distance from good 2022-01-21 08:02:23 +01:00
4b82987bbb fic: Urlify also ł 2022-01-21 08:02:23 +01:00
2645109839 admin: Display commit ID in admin interface 2022-01-21 08:02:23 +01:00
92bb409764 admin: Fix panic on not provided password 2022-01-20 14:44:11 +01:00
0a15bd9756 ui: Keep the flag box on the right
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-11 11:20:44 +01:00
eb7697ed50 ui: Fix some bugs with teams without member 2021-12-11 11:20:44 +01:00
f8001653cd sync: Parse resolution.md 2021-12-11 11:20:44 +01:00
0cc72712a4 Trim flags to avoid mistakes due to empty lines or espaces... 2021-12-11 11:20:44 +01:00
e6d8f2db1b sync: Try to improve git-lfs support 2021-12-11 11:20:44 +01:00
7896579189 frontend: Increase input size allowed 2021-12-11 11:20:44 +01:00
c7569b5e54 Use pointer receiver more offen 2021-12-11 11:20:44 +01:00
6999b4e728 Pass hadolint 2021-12-11 02:02:06 +01:00
8a6d480d17 sync: Make value lowercase if flag is not case sensitive 2021-12-11 02:02:06 +01:00
63423c9c69 repochecker: Consider LFS errors as warnings 2021-12-11 02:02:06 +01:00
969c61017f repochecker: Improve detection and messages 2021-12-11 02:02:06 +01:00
2d5b34ee01 dashboard: Fix rank order in Chrome 2021-12-11 02:02:06 +01:00
57d351c6c1 sync: Allow free hint 2021-12-11 02:02:06 +01:00
43be59b97d admin: Create a GitImporter based on git binaries 2021-12-11 02:02:06 +01:00
23c43ad667 repochecker: Fix parsing of numstat (using -z option)
Also improve binary file detection and allow < 1M biary files
2021-12-11 02:02:06 +01:00
9fe1374a77 sync: Try to use a ssh key if no ssh-agent configured 2021-12-11 02:02:06 +01:00
3625af47d9 repochecker: Track and report binary files found in repository 2021-12-11 02:02:06 +01:00
724b00b825 repochecker: Enforce archive types 2021-12-11 02:02:06 +01:00
038abe450d admin: Add a route and a button to sync the filesystem 2021-12-11 02:02:06 +01:00
a06602a7e8 admin: Add new API route to perform smart theme update controled by hooks 2021-12-11 02:02:06 +01:00
1a1343596a fic: Add theme recursive deletion 2021-12-11 02:02:06 +01:00
995740e275 admin: Add a new option -4real to avoid mass progression deletion 2021-12-11 02:02:06 +01:00
49664c3dfe Implement radio flag type 2021-12-11 02:02:06 +01:00
41565729fd ui: Update node modules 2021-12-11 02:02:06 +01:00
61fccca070 Implement unit property for flags 2021-12-11 02:02:06 +01:00
c3742ade4e Implement number flags 2021-12-11 02:02:06 +01:00
30d0afe43f CI: Limit CI triggers to certain events only 2021-11-14 17:30:29 +01:00
495b08463f sync: Check that UCQ value is not 'true' nor 'false' 2021-11-14 17:30:29 +01:00
89ca192890 sync: Add Git Importer 2021-11-14 17:30:29 +01:00
aebfb7bf96 sync: Add Init and Sync functions
Init initializes the directory/repository before the first use.
Sync is called to unsure the directory is up-to-date.
2021-11-14 17:30:29 +01:00
281056a723 docker: Don't redo chbase if container has already been launched
All checks were successful
continuous-integration/drone/push Build is passing
2021-10-25 20:51:55 +02:00
c85c1f0354 chore(deps): update golang.org/x/crypto commit hash to 089bfa5 2021-10-25 20:51:55 +02:00
87583fbd17 ui: Update modules 2021-10-25 20:51:55 +02:00
315fb1efae chore(deps): update dependency prettier to ~2.4.0
All checks were successful
continuous-integration/drone/push Build is passing
2021-09-28 08:23:26 +00:00
744aabf821 chore(deps): update github.com/studio-b12/gowebdav commit hash to a3a8697
Some checks reported errors
continuous-integration/drone/pr Build was killed
continuous-integration/drone/push Build was killed
2021-09-28 08:06:56 +00:00
e29802b731 WIP Try to built a new htdocs-frontend tarball
All checks were successful
continuous-integration/drone/push Build is passing
2021-09-28 09:11:46 +02:00
8d9269c635 ui: Fix bootstrap 5 embed items 2021-09-26 12:25:02 +02:00
MCharpy
2e4513c00f fic: Tag now in Title format
Signed-off-by: MCharpy <mahe.charpy@epita.fr>
2021-09-26 12:25:02 +02:00
56d8d49304 ui: Prepare ui for public interface 2021-09-26 12:25:02 +02:00
d010b86fa0 Remove old frontend ui 2021-09-26 12:25:02 +02:00
2875d3eb59 Update README.md
Some checks reported errors
continuous-integration/drone/push Build was killed
2021-09-09 15:00:23 +02:00
48b65e0d39 Add dummy script to animate the challenge launch
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is failing
2021-09-09 11:33:59 +02:00
7c2bb0df23 dashboard: Fix sort on Chrome 2021-09-09 11:33:59 +02:00
08da42e273 dashboard/admin: Adapt to 16/9 2021-09-09 11:33:59 +02:00
105034ec8c Add global score coefficient 2021-09-09 11:33:59 +02:00
cd73622cae New settings to only count bad submissions 2021-09-09 11:33:59 +02:00
b887288c78 admin: Update way of modulus calculation 2021-09-09 11:33:59 +02:00
203450df32 dashboard: Add SII logo 2021-09-09 11:33:59 +02:00
f6fec437f9 admin: Change challenge duration to 6h 2021-09-09 11:33:59 +02:00
debfd2a894 score-sync-zqds: It works! + some optimizations 2021-09-09 11:33:59 +02:00
22e25a384a password_paper: Update LaTeX 2021-09-09 11:33:59 +02:00
1488ee60e5 ui: Ensure team exists before displaying it 2021-09-09 11:33:59 +02:00
82c2af57cb ui: Fix retrieval of server time 2021-09-09 11:33:59 +02:00
bd8db24997 ui: Add Accept header to retrieve JSON errors 2021-09-09 11:33:59 +02:00
de03863f1b backend: Also remove file if no description given 2021-09-09 11:33:59 +02:00
89979eac8f Update .gitignore 2021-09-09 11:33:59 +02:00
71fa7f67ea configs: Add nginx config for OIDC 2021-09-09 11:30:13 +02:00
cf1d8d9516 settings: Add IgnoreTeamMembers 2021-09-09 11:30:13 +02:00
e6aadfdd8b ui: Don't suggest team to change their name if name change is disabled 2021-09-09 11:30:13 +02:00
b6ed4fd966 admin: Hide team's password by default on team page 2021-09-09 11:30:13 +02:00
ac4fc633ce admin: Bump to 1. 2021-09-09 11:30:13 +02:00
8a383719b4 fickit: IP setup 2021 2021-09-09 11:30:13 +02:00
b9a220c359 fickit: Add scores-sync-zqds to frontend 2021-09-09 11:28:06 +02:00
5eeb1a6297 admin: Handle team password 2021-09-09 11:21:29 +02:00
ed69dc6ba4 fickit: Fix some bugs 2021-09-09 11:01:20 +02:00
75d288000f fickit: Add dexidp on frontend 2021-09-09 11:01:19 +02:00
e48ee589e5 fickit: Provide frontend htdocs from image 2021-09-08 02:07:37 +02:00
86e15bd80b fickit: Disable dhcp/dns services 2021-09-08 02:07:37 +02:00
9367d99a05 CI: Add Dockerfile just containing the ui files 2021-09-08 02:07:37 +02:00
5fa94ecbed New project remote-scores-sync-zqds 2021-09-08 02:07:37 +02:00
fb53c9a4f1 configs: Rework nginx configs 2021-09-08 02:07:37 +02:00
a0a1a717ee fic: Expose exportedTeam struct 2021-09-08 02:07:37 +02:00
b2c8b735f4 fic: Fix color export when no red is present 2021-09-08 02:07:37 +02:00
63de5d64b1 fic: Pick HSL function to generate random colors 2021-09-08 02:07:37 +02:00
6395eaaf5f fickit: Update kernel config used for 2021 2021-09-08 02:07:37 +02:00
84115b89f7 Commit kernel config used in 2020 2021-09-08 02:07:37 +02:00
dc6d7152f9 fickit: Update images 2021-09-08 02:07:37 +02:00
25116cca59 Ignore vendor directory 2021-09-08 02:07:37 +02:00
12a6fcf461 fickit: Limit to amd64 arch 2021-09-08 02:07:37 +02:00
f2bc4b015f fic: Replace accentuated letters by non-accentuated ones 2021-09-08 02:07:37 +02:00
5c12963da8 fic: Add team's external_id to allow team and score synchronisation 2021-09-08 02:07:37 +02:00
342d216b3e Clean go.sum 2021-09-08 02:07:37 +02:00
abf0715dbf admin: Insert $team assignee in db automatically 2021-09-08 02:07:37 +02:00
f5941dcece ui: Improve CardTheme colors 2021-09-08 02:07:37 +02:00
a812a6a5c6 ui: Refresh issues after submit 2021-09-08 02:07:37 +02:00
f5f450f456 ui: Sort tags without considering case 2021-09-08 02:07:37 +02:00
2b58e707ca admin: precise kind of error when filling claim 2021-09-08 02:07:37 +02:00
864f78e8fa ui: Sort ranking ... 2021-09-08 02:07:37 +02:00
986fbb8f74 ui: Hide issue form after sending 2021-09-08 02:07:37 +02:00
b0a7daf1f4 ui: Include badge on CardTheme 2021-09-08 02:07:37 +02:00
9dcd43664a ui: Fix niceborder color 2021-09-08 02:07:37 +02:00
46261af751 ui: Center display of hints buttons 2021-09-08 02:07:37 +02:00
2a9d2cacda ui: Always recreate the settings object to remove previously defined 2021-09-08 02:07:37 +02:00
8683e78213 ui: Fix ?fill-issue link 2021-09-08 02:07:37 +02:00
eca6a4238d ui: Avoid floating expansion 2021-09-08 02:07:37 +02:00
a5bf9d2600 ui: write Scenarii the correct way 2021-09-08 02:07:37 +02:00
ab11446be2 ui: Always show scrollbar to avoid effects 2021-09-08 02:07:37 +02:00
dedfab1c7d ui: Fix next button link 2021-09-08 02:07:37 +02:00
d319587dab ui: Fix responsiveness on home page 2021-09-08 02:07:37 +02:00
2246e00948 fic: Fix exercice deletion 2021-09-08 02:07:37 +02:00
17839474e1 ui: Fix base url mess 2021-09-08 02:07:37 +02:00
74d77dce9f sync: Add partner's info 2021-09-08 02:07:37 +02:00
f2bf07fd28 ui: Add helpers for formating date and file size
All checks were successful
continuous-integration/drone/push Build is passing
2021-09-01 02:21:49 +02:00
83a47af391 ui: Add theme from bootswatch
All checks were successful
continuous-integration/drone/push Build is passing
2021-09-01 01:49:28 +02:00
e6557c8c06 ui: Fix erasing of downloadable files 2021-09-01 01:49:28 +02:00
941e1c16d5 ui: Fix loading problems when themes arrived to late 2021-09-01 01:49:28 +02:00
e3057726e8 ui: Implement hint discovery 2021-09-01 01:49:28 +02:00
815f4b9037 ui: Add a progress bar indicating total number of flags 2021-09-01 01:49:28 +02:00
3c42bef298 CI: Fix compilation problems 2021-09-01 01:49:28 +02:00
a255480195 ui: Ensure themes menu kept in screen
Some checks failed
continuous-integration/drone/push Build is failing
2021-08-31 23:32:07 +02:00
23d5ea7c97 ui: Randomize themes list 2021-08-31 23:32:07 +02:00
451b678e73 CI: Build frontend ui 2021-08-31 23:32:07 +02:00
dcb0cb315b admin: Can modify help and order props in ui
Some checks failed
continuous-integration/drone/push Build is failing
2021-08-31 20:39:24 +02:00
6223d2be36 sync: Also import hints during speed sync 2021-08-31 19:34:47 +02:00
1def2c97c1 ui: Working flags 2021-08-31 02:58:24 +02:00
ef899ee99b fic: Sort mixed flags by order before packing them 2021-08-30 20:03:17 +02:00
102a0878ac configs: Update scripts and config for new ui 2021-08-30 19:43:35 +02:00
74e8c3801a fic: Add Order, Help and Type values in struct 2021-08-30 18:33:14 +02:00
867e9bb345 sync: Fix a div by 0 when no exercice detected in theme 2021-08-30 18:31:32 +02:00
7e13cf28bd ui: Almost all interface done with Svelte 2021-08-30 12:46:18 +02:00
9fa1ede69c frontend: Start new interface with svelte 2021-08-25 13:23:52 +02:00
234ce066cf Don't change BASEURL again on container restart
All checks were successful
continuous-integration/drone/push Build is passing
2021-08-25 03:43:06 +02:00
4fd4fb01b4 chore(deps): update golang.org/x/crypto commit hash to 32db794
Some checks reported errors
continuous-integration/drone/pr Build was killed
continuous-integration/drone/push Build is passing
2021-08-17 18:49:31 +00:00
993af78fd1 chore(deps): update module github.com/burntsushi/toml to v0.4.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2021-08-05 09:04:13 +00:00
25ba010987 chore(deps): update golang.org/x/crypto commit hash to a769d52
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2021-08-03 10:21:56 +00:00
96c629f27c chore(deps): update golang.org/x/image commit hash to a66eb64
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2021-08-03 09:09:07 +00:00
688f97016a chore(deps): update module github.com/burntsushi/toml to v0.4.0
Some checks reported errors
continuous-integration/drone/push Build was killed
2021-08-03 08:51:08 +00:00
0681c1ebe3 chore(deps): update github.com/studio-b12/gowebdav commit hash to 7ff61aa
Some checks reported errors
continuous-integration/drone/pr Build was killed
continuous-integration/drone/push Build was killed
2021-08-02 09:05:40 +00:00
66344fa0db Add renovate.json
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2021-08-01 21:01:22 +00:00
77c71c5364 CI: don't do deployment on PR
All checks were successful
continuous-integration/drone/push Build is passing
2021-08-01 22:30:09 +02:00
7e9c2ccbe9 sync: Ignore some hidden files/dirs
All checks were successful
continuous-integration/drone/push Build is passing
2021-07-30 11:32:23 +02:00
357035deba qa: Use relative path to real website from QA
All checks were successful
continuous-integration/drone/push Build is passing
2021-07-23 11:51:44 +02:00
9a9d742e21 configs: Fix a problem with submissions routing
All checks were successful
continuous-integration/drone/push Build is passing
2021-07-22 17:53:56 +02:00
d701331436 frontend: Fix issue with redirecting URL for chname and issue
All checks were successful
continuous-integration/drone/push Build is passing
2021-07-22 16:17:03 +02:00
29607981e4 admin: Use relative path to call API 2021-07-22 16:17:03 +02:00
8f1b44e3dd New env variable FIC_BASEURL to change the base URL 2021-07-22 16:17:03 +02:00
74ae52ef41 CD: Fix deployment of fic-backend image
All checks were successful
continuous-integration/drone/push Build is passing
2021-07-21 11:23:06 +02:00
c2fe14c0d7 db: Handle connection through unix socket
All checks were successful
continuous-integration/drone/push Build is passing
2021-07-21 10:43:10 +02:00
8e95cec104 Introduce fic-nginx
All checks were successful
continuous-integration/drone/push Build is passing
2021-07-21 03:03:25 +02:00
e0dd5ea789 frontend: Include chbase.sh in entrypoint
All checks were successful
continuous-integration/drone/push Build is passing
2021-06-09 23:26:40 +02:00
7fc860edec admin: Embed static assets into binary 2021-06-09 23:26:40 +02:00
eb26879c74 CI: add checkupdate tags for repochecker
All checks were successful
continuous-integration/drone/push Build is passing
2021-05-14 01:36:13 +02:00
57fe1a7517 sync: Ignore exercice directories not containing at least - sep
Some checks reported errors
continuous-integration/drone/push Build was killed
2021-05-14 01:25:08 +02:00
8b261011b6 repochecker: new option avoiding failure if resolution.mp4 missing 2021-05-14 01:14:30 +02:00
d458ac963a CI: add Dockerfile for repochecker (used for student's CI)
All checks were successful
continuous-integration/drone/push Build is passing
2021-05-14 00:44:55 +02:00
5c990de2a0 repochecker: version bump 2021-05-14 00:42:40 +02:00
9fa89e0793 repochecker: fix file concatenation 2021-05-14 00:42:40 +02:00
9dc1f401b7 Use go modules 2021-05-14 00:42:40 +02:00
99862b6daa CI: disable new go1.16 module behaviour
All checks were successful
continuous-integration/drone/push Build is passing
2021-05-08 14:17:01 +02:00
5d13cfe01e CI: create Docker manifest image
Some checks failed
continuous-integration/drone/push Build is failing
2021-02-23 09:11:47 +01:00
10c408eda6 CI: add repochecker built for Apple M1 2021-02-23 09:00:05 +01:00
0d792dcd8f frontend: don't use path to give team's ID, use a dedicated header
All checks were successful
continuous-integration/drone/push Build is passing
2021-02-08 09:27:12 +01:00
f4dcaa23a3 QA: Add new script to migrate QA content from a DB to another 2021-02-05 16:56:27 +01:00
2cf9723c6c qa: Add tested exercices to Todo list
All checks were successful
continuous-integration/drone/push Build is passing
2021-02-05 12:05:23 +01:00
3434535f51 CI: try generating static binaries
All checks were successful
continuous-integration/drone/push Build is passing
2021-02-04 19:04:12 +01:00
1445917fec Include all existing associations when generating htpasswd
All checks were successful
continuous-integration/drone/push Build is passing
2021-01-30 05:13:56 +01:00
f7c15925c6 frontend: Fix random error when validating challenge
Some checks reported errors
continuous-integration/drone/push Build was killed
2020-12-11 23:28:24 +01:00
8e8fa7c61c sync: use Separator attribute
All checks were successful
continuous-integration/drone/push Build is passing
2020-12-11 21:03:12 +01:00
f53a5dbcf9 admin: also delete useless QA content with exercice
All checks were successful
continuous-integration/drone/push Build is passing
2020-12-11 19:38:57 +01:00
c10becba91 CI: also deploy tarballs for static files
All checks were successful
continuous-integration/drone/push Build is passing
2020-11-14 16:18:57 +01:00
a93d6c8c49 backend: fix bad printf format, thanks to go vet
All checks were successful
continuous-integration/drone/push Build is passing
2020-11-13 15:25:16 +01:00
38c18ef1aa CI: build repochecker and generate repochecker.version
Some checks reported errors
continuous-integration/drone/push Build was killed
2020-11-13 15:22:45 +01:00
3bc8f0bf95 CI: add Go vet 2020-11-13 15:22:45 +01:00
1ad4382e97 CI: also build qa
All checks were successful
continuous-integration/drone/push Build is passing
2020-11-13 14:55:36 +01:00
1a3e14040c Settings: avoid transmitting false variables 2020-11-13 14:29:23 +01:00
ea334a8a2f QA: add a list of team's exercices 2020-11-13 13:11:58 +01:00
911bcb032e Nouvelle option pour avoir un lien vers le rapport QA de l'exercice 2020-11-13 11:38:47 +01:00
1436d9ca81 admin: New route to reset settings to sane default values 2020-11-13 11:34:31 +01:00
a8f25471f1 Build and deploy for amd64 2020-11-13 11:16:58 +01:00
74c3599b5d Update FIC logo
All checks were successful
continuous-integration/drone/push Build is passing
2020-10-24 17:41:47 +02:00
95ca255d75 qa: Add multiple color on home page
Some checks reported errors
continuous-integration/drone/push Build was killed
2020-09-09 21:20:04 +02:00
4490eb7036 qa: Add rapid access to corresponding challenge 2020-09-09 21:20:00 +02:00
ce2f42cdc8 qa: Fix new line formating in comments 2020-09-09 21:19:33 +02:00
70afa61814 qa: new button to speed up +1 2020-09-09 21:19:09 +02:00
7c51ce7c4f qa: Use relative addresses 2020-09-09 21:15:47 +02:00
5aa21d2679 qa: Permit empty content if all is Ok 2020-09-09 19:26:47 +02:00
940b32debc qa: delete comment related to a query 2020-09-08 19:06:36 +02:00
42d594ccac qa: Add todo list on home page
All checks were successful
continuous-integration/drone/push Build is passing
2020-09-08 13:30:43 +02:00
a237936feb qa: New service to handle QA testing by students 2020-09-08 12:50:41 +02:00
a0155c6deb Replace old Help term by Placeholder
All checks were successful
continuous-integration/drone/push Build is passing
2020-09-07 19:34:10 +02:00
eca814ca4b Add drone CI
All checks were successful
continuous-integration/drone/push Build is passing
2020-09-06 15:02:05 +02:00
7ad36f6141 libfic: fix exercice delete cascade: bad db column name and dependancy 2020-09-06 12:36:11 +02:00
c9932cdaf6 admin: change exercice's Delete button to cascade deletion 2020-09-06 12:21:45 +02:00
2d6d09852b dashboard: make title more explicit and avoid leaving the page 2020-05-16 03:54:18 +02:00
90151ce498 frontend: fix error on registration validated 2020-05-16 03:53:32 +02:00
130bb92dc8 admin: Fix some toast unreadable 2020-05-16 03:53:07 +02:00
5b84e4bfdb Fix exercices' theme loading in admin 2020-05-16 03:52:43 +02:00
64b9e9a251 New option to disallow team creation: join only 2020-05-16 03:51:36 +02:00
5d3ef96f3f Animate lighter the clock before start 2020-05-16 03:50:12 +02:00
16abc95b4f Fix DB issues 2020-05-16 03:49:27 +02:00
21cc875cc0 Update ficicon 2020-04-15 07:39:57 +02:00
adb424ea03 Use fmt.Errorf 2020-04-15 07:39:38 +02:00
45069d4fbb admin: replace notifications with bootstrap toast 2020-03-08 12:48:25 +01:00
3bc8d7064b fickit: fix VLAN and do NAT to Internet 2020-01-30 19:08:28 +01:00
d45a6841db fickit: update kernels 2020-01-30 19:07:11 +01:00
ed2a38511e fickit: fix a parallelisation problem on init 2020-01-30 19:06:38 +01:00
ff56933a93 fickit: add acpid to handle power button press 2020-01-30 19:04:41 +01:00
56c6f282c4 fickit: crypt main partition 2020-01-30 19:04:00 +01:00
0c8bc261d9 fickit: save ssh keys between reboots 2020-01-30 19:02:19 +01:00
bb9dd10f00 Refactor fickit-update and fickit-prepare 2020-01-30 19:00:46 +01:00
429cd3010c backend: fix XSS in team name and events 2020-01-30 19:00:14 +01:00
ca8bac1ac8 dashboard: sort teams before displaying rank 2020-01-30 19:00:14 +01:00
0536da2472 dashboard: start graph on origin 2020-01-30 19:00:14 +01:00
a82a0fb170 synchro: synchronize logs from frontend 2020-01-30 19:00:14 +01:00
f1c5681a37 dashboard: improve display 2020-01-30 19:00:14 +01:00
e017e11f68 dashboard: add graph on side 2020-01-30 19:00:14 +01:00
5849648c70 admin: fix handling of description in claims 2020-01-30 19:00:14 +01:00
83ba3b88a5 dashboard: add trophee scene 2020-01-30 19:00:14 +01:00
66a72633d6 dashboard: generate a special teams.json with members for trophee scene 2020-01-30 19:00:14 +01:00
b9fa5accff dashboard: add graph score 2020-01-30 18:57:07 +01:00
d66de6fb3c libfic: avoid infinite loop in db 2020-01-30 18:55:57 +01:00
0ab637faed dashboard: don't get legacy /stats.json 2020-01-30 18:55:57 +01:00
35bd908374 admin: add graphique in public 2020-01-30 18:55:57 +01:00
7c84301c04 admin: implement Enter keypress on search 2020-01-30 18:55:57 +01:00
cb97af2f8a admin: redesign home page 2020-01-30 18:55:57 +01:00
4f237677e2 admin: version bump 2020-01-30 18:55:57 +01:00
5df1cc6e93 admin: add some stats about exercices 2020-01-30 18:55:57 +01:00
007efc6118 health: done 2020-01-30 18:55:57 +01:00
a0b19f6184 backend: handle SIGUSR1 to retreat all files left in submissions directory and SIGUSR2 to dump statistics 2020-01-30 18:55:57 +01:00
6f17fc0760 frontend: pluralize points on index 2020-01-30 18:55:56 +01:00
05a795ad49 frontend: add hint on special SE page 2020-01-30 18:55:56 +01:00
23b6b2b005 admin: handle case insensitive ucq 2020-01-30 18:55:56 +01:00
32fe61f557 admin: refresh claims list each 10s 2020-01-30 18:55:56 +01:00
7f691779f7 Hardenize nginx config 2020-01-30 18:55:56 +01:00
d093f3670b admin: on pki page, press enter to associate certificate 2020-01-30 18:55:56 +01:00
3f692984c7 admin: new page to display exercices flags 2020-01-30 18:55:56 +01:00
15ae32090f frontend: avoid decoration when hover a list-item link 2020-01-30 18:55:56 +01:00
0bc42282aa frontend: resize heading pictures as thumb file 2020-01-30 18:55:56 +01:00
b387f011d8 admin: add exercices stats accordion 2020-01-30 18:55:56 +01:00
f4c74f57d6 admin: Fix bad names in update choices 2020-01-30 18:55:56 +01:00
3cb4e98bd1 admin: display team and exercice in a new window 2020-01-30 18:55:56 +01:00
d944be349a admin: increase claim filtering 2020-01-30 18:55:56 +01:00
edbac43423 frontend: allow players to respond to issues 2020-01-30 18:55:56 +01:00
73eb3ab1c0 admin: count only levels of new claims owned 2020-01-30 18:55:56 +01:00
d8584a8a31 admin: can sort claim by last_update 2020-01-30 18:55:56 +01:00
83b7df7e69 admin: add message on claim state change and assignee change 2020-01-30 18:55:56 +01:00
e45a674937 admin: validate team association on enter press 2020-01-30 18:55:56 +01:00
e945071a10 admin: add a route and buttons to generate/delete fichtpasswd, if needed 2020-01-30 18:55:56 +01:00
590522e7ed frontend: add an item Issues in main site menu 2020-01-30 18:55:56 +01:00
a3ffdeae17 frontend: display issues related to the team 2020-01-30 18:55:56 +01:00
7bec409ab8 sync: fix hint dependancies error not reported 2020-01-30 18:55:56 +01:00
9d93331868 admin: display {hint,flag,mcq} dependancies on interface 2020-01-30 18:55:56 +01:00
ac9361b4ce admin: redesign propagation time button + can use propagation time in public timer 2020-01-30 18:55:56 +01:00
caea02bb4d frontend: copy settings.json on settings reload (to handle delayed settings propagation) 2020-01-30 18:55:56 +01:00
4820d42327 Implement hint dependancy on mcq 2020-01-29 16:02:30 +01:00
6d5e2bcb65 password_paper: sort the name in sheets 2020-01-29 16:02:30 +01:00
34a2370236 admin: can renew the PKI from interface 2020-01-29 16:02:30 +01:00
5c17dd4605 admin: add indication on how to use exercice dependancies 2020-01-29 16:02:30 +01:00
99e53ccfe6 admin: use hexadecimal certificate ID 2020-01-29 16:02:30 +01:00
6921431a77 Flag MCQ can now depend on MCQ 2020-01-29 16:02:30 +01:00
e937073588 Files can now depends on MCQ 2020-01-29 16:02:30 +01:00
823328ead2 sync: fix file merging when using symlinks 2020-01-29 16:02:30 +01:00
4e258cb30d frontend: don't display hours/seconds on small screens 2020-01-29 16:02:30 +01:00
0937b4a2b8 frontend: redesign theme page with a path 2020-01-29 16:02:30 +01:00
f3fdb36929 Allow store files bigger than 4GB 2020-01-29 16:02:30 +01:00
c9cacb80a7 frontend: Fix orthograph 2020-01-29 16:02:30 +01:00
6f64eaed95 admin: improve claims with menu 2020-01-29 16:02:30 +01:00
9186bbc229 frontend: add players possibility to report problems with exercices 2020-01-23 18:27:14 +01:00
32dc9c1a8c admin: improve claims with related exercices 2020-01-23 18:27:14 +01:00
2e3f7c6894 admin: claims now reference exercices 2020-01-23 18:27:14 +01:00
56b79cae2d admin: make claims more responsive 2020-01-23 18:27:14 +01:00
80a4192cb4 admin: add badge of new/mines tasks in menu 2020-01-23 18:27:14 +01:00
a4c87b92a5 admin: introducing speedy deep sync and themed deep sync 2020-01-23 18:26:30 +01:00
f7762c0828 sync: don't try to import part of splitted files, just import the whole file 2020-01-23 18:26:30 +01:00
2bae30a841 admin/api: new route to list remote files and their properties 2020-01-23 18:26:30 +01:00
084d39f6cf Fix typos 2020-01-23 18:26:30 +01:00
08aa7d278c frontend: redisign some elements 2020-01-23 18:26:30 +01:00
22c4835875 admin: use default bootstrap theme, even when served with frontend 2020-01-23 18:26:30 +01:00
ccf32f8a48 configs: DHCPd config now indicates also default route 2020-01-23 18:26:30 +01:00
91cc8b9314 nginx: fix redirection from HTTP 2020-01-23 18:26:30 +01:00
4f6480d7f8 sync: add some precision around Empty flags detection 2020-01-23 18:26:30 +01:00
a6e799a635 backend: also create .tmp directory required by rsync 2020-01-23 18:26:30 +01:00
f251d30162 settings: reload also on file creation (when rsync do atomic moves) 2020-01-23 18:26:30 +01:00
9c9d4edd74 fickit: fix iptables script on frontend 2020-01-23 18:26:30 +01:00
b35a8a9200 unbound: fix missing options for containers 2020-01-23 18:26:30 +01:00
769158a9d7 repochecker: add new option -skipfiledigests to speed up the checks and avoid downloading lots of content 2020-01-23 18:26:30 +01:00
aee3500fdf sync: avoid depending on database when importing files 2020-01-23 18:26:30 +01:00
16c337c2bc Update angularJS, jQuery and bootstrap 2020-01-23 18:26:30 +01:00
1833a7550d frontend: hardcode special social engineering challenge 2020-01-23 18:26:30 +01:00
04345b33a2 frontend: remove puncts at the end of list items 2020-01-23 18:26:30 +01:00
e7c1812cb0 synchro.sh: sync files in a separate thread 2020-01-23 18:26:30 +01:00
aba311aebd sync: detect bad label wording 2020-01-23 18:26:30 +01:00
47ba134b55 Implement flag type 'text': this is like keys, but on multiple lines 2020-01-23 18:26:30 +01:00
8f998485bb sync: resize heading pictures 2020-01-23 18:26:30 +01:00
a8b6e9fba5 fickit: update images 2020-01-23 18:26:30 +01:00
0cfa695873 fickit: update kernels 2020-01-23 18:26:30 +01:00
9983542653 admin: always use normalized hexadecimal certificate ID 2020-01-23 18:26:30 +01:00
546cae869b admin: passwd authentication can be made with team name or certificate ID 2020-01-23 18:26:30 +01:00
141c5dd33d frontend: update notification icons 2020-01-17 14:57:04 +01:00
104cb067ea fickit: save ssh-keys between reboots 2020-01-17 14:57:04 +01:00
bea31faa8e fickit: frontend has 4 eth cards 2020-01-17 14:57:04 +01:00
f078919459 fickit: update unbound image 2020-01-17 14:57:04 +01:00
5e1f314822 fickit: update mdadm image 2020-01-17 14:57:04 +01:00
778c30035b fickit-pkg/mdadm: add busybox to include a shell to pass command 2020-01-17 14:57:04 +01:00
3465406b6f libfic: add missing tables during reset 2020-01-17 14:57:04 +01:00
e00a67832e Fix missing lower/ part 2020-01-17 14:57:04 +01:00
e4b740b5bc admin: Use SSHA password instead of APR1 2020-01-17 14:57:04 +01:00
572082cd5f fill_teams: also generate apr1 htpasswd 2020-01-17 14:57:04 +01:00
225f6d2c99 fill_team: fix generation of htpasswd 2020-01-17 14:57:04 +01:00
5ffbeabf5b fill_team: avoid \ char in password + fix substitution of UTF-8 chars 2020-01-17 14:57:04 +01:00
13548d913f fickit-frontend: update unbound container 2020-01-17 14:57:03 +01:00
91e40c1e1a pkg/unbound: define default command to run 2020-01-17 14:57:03 +01:00
f3f14dcd25 password_paper: fix ^ char 2020-01-17 14:57:03 +01:00
a475617657 admin: heath api now checks untreated files 2020-01-17 14:57:03 +01:00
b4fa57f9c9 sync: introducing showlines property for vectors
It allows players to know in advance how many items the vector is composed.
2020-01-17 14:57:03 +01:00
a545112cb2 frontend: highlight current questions 2020-01-17 14:57:03 +01:00
11a66346e7 repochecker: use a temporary directory to import files 2020-01-17 14:57:03 +01:00
56053f3350 backend: implement hint dependencies 2020-01-17 14:57:03 +01:00
f3a34c00db sync: implement hint dependency on flags 2020-01-17 14:57:03 +01:00
6ad11e49d5 backend: add an identifier to each treated file 2020-01-17 14:57:03 +01:00
9693940d8c sync: add logs on stderr when doing deepsync 2020-01-17 14:57:03 +01:00
d97ecde3fb sync: return binding between challenge.txt IDs and DB item 2020-01-17 14:57:03 +01:00
4a490b1a33 admin: PKI validity no more hardcoded 2020-01-17 14:57:03 +01:00
14f5cf29b7 dashboard: parametrize URL in welcome team 2020-01-17 14:57:03 +01:00
1db035e050 dashboard: fix urlbase 2020-01-17 14:57:03 +01:00
698c2f1a47 libfic: add new functions to retrieve the Id of some contents from Title, Label, ... 2020-01-17 14:57:03 +01:00
fbae34ee4f sync: add error message when missing heading.jpg 2020-01-17 14:57:03 +01:00
26eab7ed67 sync: import heading.jpg only in Sync phase 2020-01-17 14:57:03 +01:00
5dcb13629a admin: display on interface time synchronization diff 2020-01-17 14:57:03 +01:00
fa33fac003 frontend: add a timestamp file for time checking on backend 2020-01-17 07:02:40 +01:00
6740256a32 sync: implement hint dependency on flags 2020-01-17 07:02:40 +01:00
20f2597248 backend: fix a pointer reuse 2019-11-25 14:52:19 +01:00
cefed3bf23 admin: fix synchronisation when idtheme is not in url 2019-11-25 14:52:19 +01:00
c2c5cf4ce3 backend: add parameter to launch a number of generation workers 2019-11-25 14:52:19 +01:00
97a3aa713f sync: fix hash computation by factorizing 2019-11-25 14:52:19 +01:00
0766fbe480 sync: don't rely on map order to import flags
Sometimes, maps order doesn't match file order. Return flag ID as
list to keep the order.
2019-11-25 14:52:19 +01:00
Tristan Ruter-Naon
cb7f3326c4 admin: fix typo 2019-11-25 14:52:19 +01:00
48a941e2c5 compose: use /dashboard baseurl to be proxified by nginx 2019-11-25 14:52:19 +01:00
0c8099a639 config: add a route to fic-dashboard 2019-11-25 14:52:19 +01:00
f2fc142869 api: remote route takes advantage from builds functions 2019-11-25 14:52:19 +01:00
ded583008a dashboard: handle correctly baseurl option
On the first public.html GET, load the index.html file, as Go
Template.
2019-11-25 14:52:19 +01:00
ca891cd9b2 sync: Fix non-import of MCQ during sync 2019-11-25 14:52:19 +01:00
6265f85149 sync: Implement vector flags 2019-11-25 14:52:19 +01:00
99fcc99e82 sync: turn IgnoreCase on by default with reverse field CaseSensitive 2019-11-25 14:52:19 +01:00
33f7d104e4 sync: MCQ justifications are given in the choice tag directly 2019-11-25 14:52:19 +01:00
d7f0425d8a repochecker: Fix given URL to documentation 2019-11-25 14:52:19 +01:00
4e01377a29 sync: search theme's label in a title.txt file, fallback on dirname 2019-11-25 14:52:19 +01:00
6f7bd3b785 admin interface need rw access to TEAMS directory to do associations 2019-11-25 14:52:19 +01:00
168be0f7cc config: Allow unconditional access to admin interface with compose 2019-11-25 14:52:19 +01:00
846f2ce8a4 admin: Double check before doing dangerous actions in settings panel
Suggested-by: Nicolas Ribeyrolle <nicolas.ribeyrolle@epita.fr>
2019-11-25 14:52:19 +01:00
8e618565ad sync: Fix long running bug known as "why my fresh uploaded file is now empty again"
Thanks to Nicolas Ribeyrolle
2019-11-25 14:52:19 +01:00
0cbd6390ba docker-compose: fix started detection 2019-11-25 14:52:19 +01:00
6d72e6b970 Complete commit 75463dcebb: if not configured, challenge should not be considered as started 2019-11-25 14:52:19 +01:00
41a3279bf8 libfic: avoid stange MYSQL_HOST variable, expect IP or DN 2019-11-25 14:52:19 +01:00
8131fda0e7 admin: display file dependancies and be able to remove them 2019-07-21 23:50:26 +02:00
c8ece39cb2 sync: alert about unknown keys in challenge.txt 2019-07-21 22:38:45 +02:00
936ef09e33 admin: fix strange behaviour when deleting some items 2019-07-21 21:55:36 +02:00
973363b3da admin/api: refactor file API 2019-07-21 21:55:11 +02:00
3e5b4ebad2 admin: add missing default settings 2019-07-12 19:22:05 +02:00
ba5642da8f admin: new form to update history coefficient 2019-07-12 19:21:07 +02:00
2b75287d16 backend: multithread generation 2019-07-11 19:52:13 +02:00
3bcac39f5f FIC2020 logo 2019-07-10 17:41:07 +02:00
4c7fd839b6 fixup! sync: Extract function that import flags from importer 2019-07-10 16:58:20 +02:00
a3d473983d fickit: Include /boot partition into RAID1 array, see https://outflux.net/blog/archives/2018/04/19/uefi-booting-and-raid1/ 2019-07-10 13:09:08 +02:00
444ba85f11 repochecker: add a mean to automatically check update on run
go build -tags "checkupdate"
2019-07-10 13:09:08 +02:00
32f1c86519 sync: ignore directory beginning with . (like .git) 2019-07-10 13:09:08 +02:00
9a9675bcf5 repochecker: introduce new project 2019-07-10 13:09:08 +02:00
eb95d861d3 sync: Extract function that import hints from importer 2019-07-10 13:09:08 +02:00
4039a394b5 sync: Extract function that import flags from importer 2019-07-10 13:09:08 +02:00
3f55845374 sync: Extract function that import files from importer 2019-07-06 04:08:29 +02:00
3104bf4e65 Speak about dashboard 2019-07-06 03:28:44 +02:00
3f99771910 sync: Extract function that builds an exercice from importer 2019-07-06 03:28:44 +02:00
682598fdbb sync: Extract function that builds a theme from importer 2019-07-06 03:28:44 +02:00
536dc0eb6b frontend: allow partial settings (used when publicly published) 2019-02-18 18:00:54 +01:00
15d108497e backend: check the team has access to the exercice/flag before doing the action 2019-02-06 03:40:49 +01:00
ff7c89af9f synchro: back to the default behaviour: don't synchronize/erase files that aren't treated yet 2019-02-06 03:40:49 +01:00
771627a0da pki: fix team association, complement to 68e5c4cd2b
The fix introduced in the referenced commit was not working.

This time, it has been tested with the following commands:

	# Associate all certificate to a team
	curl http://localhost:8081/api/certs/ | jq -r .[].id | while read CERTID; do curl -X PUT -d '{"id_team":1}' http://localhost:8081/api/certs/$CERTID; done

	# For each certificate associated with the team, try to connect to the server with each certificate. Report failing certificates.
	curl -s http://localhost:8081/api/teams/1/certificates | jq -r '.[] | .id + " " + .password' | while read CERTID PASSWORD; do curl -sf --cert-type P12 --cert $CERTID.p12:$PASSWORD https://fic.srs.epita.fr/my.json > /dev/null || echo $CERTID; done
2019-02-06 03:40:49 +01:00
b778d29dd9 admin: allow certid to finish by .p12, to permit downloading .p12 file 2019-02-06 03:40:49 +01:00
703eaef880 admin: display serial in hexadecimal 2019-02-06 03:40:49 +01:00
14d31737e0 admin: new route and interface to manage symlink for team association exclusing certificates 2019-02-06 03:40:49 +01:00
2b95995104 settings: add canJoinTeam parameter 2019-02-06 03:40:49 +01:00
921644deb4 frontend: rely on angular base path 2019-02-06 03:40:49 +01:00
a35aa7be70 admin: add a new route to update team history coefficient 2019-02-06 03:40:49 +01:00
6a1f73c895 admin: include coefficient in history.json 2019-02-06 03:40:49 +01:00
60ec9704e3 admin: add exercice history.json 2019-02-06 03:40:49 +01:00
2381fb490b libfic: fix checks in handling of team history deletiion 2019-02-06 03:40:49 +01:00
34d2054e04 sync: avoid useless line break at the end of markdown processing 2019-02-06 03:40:49 +01:00
2ab9cb2eaa frontend: display hint cost on public interface 2019-02-06 03:40:49 +01:00
6715fb10a9 frontend: public interface: keep number of tries between refresh 2019-02-06 03:40:48 +01:00
c4aa220b2c frontend: don't reuse tries in public interface; use a separate field to store total tries count for an exercice; and display it in interface 2019-02-06 03:40:48 +01:00
473332e101 admin: show only active team in export 2019-02-06 03:40:48 +01:00
73db9da682 admin: thanks to ng-base, don't need other modifications 2019-02-06 03:40:48 +01:00
650f1f4d59 admin: add a new route to generate a file for movie links 2019-02-06 03:40:48 +01:00
41ef7f2555 frontend: prefer default border color in home public screen 2019-02-06 03:40:48 +01:00
1757b40a9c Remove old unused files 2019-02-06 03:40:48 +01:00
af73b2b872 frontend: avoid fetching events.json on public interface 2019-02-06 03:40:48 +01:00
e97cf884ce libfic: add igncorecase flag to regexp related to ignorecase flag 2019-02-06 03:40:48 +01:00
db22c4af1b frontend: polish public version checks 2019-02-06 03:40:48 +01:00
85d326db6b dashboard: improve readability with a legend and bold text in summary table 2019-01-23 18:33:48 +01:00
c9347f45e2 dashboard: fix solved number in summary table 2019-01-23 18:29:52 +01:00
5c5dadbee0 dashboard: avoid fake decimals in score 2019-01-23 18:29:24 +01:00
5714e8f41b admin: start adding monitor 2019-01-23 02:25:19 +01:00
42a17ab450 dashboard: use server time to calculate event time 2019-01-23 02:25:19 +01:00
17a9d39556 frontend: add a label for hint file b2sum 2019-01-23 01:39:44 +01:00
fa2d514bbc dashboard: add rank on the side 2019-01-23 01:38:00 +01:00
c1bc86d3c9 dashboard: better description of COMCYBER? 2019-01-23 00:18:13 +01:00
547184a40a dashboard: expose others screens 2019-01-23 00:17:46 +01:00
8abe57ffb6 admin: new API route to display local monitoring infos 2019-01-22 08:50:18 +01:00
3f7e217316 db: store file size as unsigned int 2019-01-22 08:49:44 +01:00
eee2cd6a2f admin: add a button to edit the raw flag value instead of the checksum 2019-01-22 08:49:44 +01:00
b205409679 dashboard: add the ability to use a remote dashboard, serve only local files: assets and eventualy public.json (to override given ones) 2019-01-22 08:49:44 +01:00
8c754fe265 fickit: install grub on disks instead of syslinux which cannot be used as both UEFI and BIOS bootloader 2019-01-22 08:49:44 +01:00
f983da9815 fickit-pkg/syslinux: add grub2, as syslinux is crap 2019-01-22 08:49:44 +01:00
088c2402cd admin: add button to disable inactive teams 2019-01-22 08:49:44 +01:00
48fcfec0d0 backend: use a new team field 'active', to avoid some team generation 2019-01-22 08:49:44 +01:00
bf426d2ed2 configs: nginx-demo config support both SSL cert + http auth 2019-01-22 08:49:44 +01:00
525b3d6b56 frontend: update the page title when navigate 2019-01-22 08:49:44 +01:00
56faf7b8db fickit: don't include routing things into frontend 2019-01-22 08:49:44 +01:00
f32e46c699 libfic: db: increase db boot time to 90 seconds 2019-01-22 08:49:44 +01:00
19b57f5908 admin: read sync import in settings page 2019-01-22 08:49:44 +01:00
58dbd9499b sync: fix report display with some security headers 2019-01-22 08:49:44 +01:00
50a51ba628 libfic: Fix MCQ dependency on flag 2019-01-22 08:49:44 +01:00
65908f8880 frontend: fix display of timeout message when validating a flag 2019-01-22 08:49:44 +01:00
7d9ad18f42 settings: new parameter to don't respect flag dependancies 2019-01-22 08:49:44 +01:00
09b24cd401 udev: ready to FIC2019 2019-01-22 08:49:44 +01:00
0812fe5000 fickit-pkg: find the minimal set of capabilities to run 2019-01-22 08:49:44 +01:00
e59e02e4fc udev: change with the 2019 vendorId/productId 2019-01-22 08:49:44 +01:00
9784310dc0 fickit: add helper script to simplify nsenter 2019-01-22 08:49:44 +01:00
6e612df2e9 fickit: ready for prod 2019-01-21 09:58:37 +01:00
1fd8f4ee42 fickit: nginx downloading problem with nginx under pressure
See: https://hub.docker.com/_/nginx#running-nginx-in-read-only-mode
See: https://stackoverflow.com/questions/25993826/err-content-length-mismatch-on-nginx-and-proxy-on-chrome-when-loading-large-file
2019-01-21 09:58:37 +01:00
9a3d3bf038 configs: add security headers
For more information, see https://securityheaders.com/?q=fic.srs.epita.fr&hide=on&followRedirects=on
2019-01-21 09:58:37 +01:00
dfffb18de1 Add udev rule and scripts used to flash the USB sticks
Original work by Alexis Daviot <alexis.daviot@epita.fr>
2019-01-21 09:58:36 +01:00
ef35879dde frontend: new parameters to setup kind of notifications allowed 2019-01-21 09:58:36 +01:00
24989c4cfa settings: new option to disable event fetch from server side 2019-01-21 09:58:36 +01:00
a4e0a90adf dashboard: can now change the sidebar 2019-01-21 09:58:36 +01:00
196f10dc9f dashboard: some improvements 2019-01-21 09:58:36 +01:00
8190bbfdc0 Update bootstrap 2019-01-21 09:58:36 +01:00
6042f9b477 sync: check video file size during import process 2019-01-21 09:58:36 +01:00
2ac205bf83 admin: add a page to view resolution video 2019-01-21 09:58:36 +01:00
4ee70a8781 settings: change param to enable/disable depends by the depth 2019-01-21 09:58:36 +01:00
5d432cdcfc admin: API version bump 2019-01-21 09:58:36 +01:00
cfb06009c9 Revert "db: cap the maximum number of simultaneous connections to the database"
This reverts commit 29ea78f0394a175100666894a15de056ce286b57.
2019-01-21 09:58:36 +01:00
7227c7109e admin: add a progression indicator for the deep synchronization 2019-01-21 09:58:36 +01:00
d9fb261232 sync: import files first during the full import, to permit file dependency to flag 2019-01-21 09:58:36 +01:00
41f815f54d frontend: fix undefined variable 2019-01-21 09:58:36 +01:00
6be7ba09a5 libfic: fix wipefiles: it didn't delete its dependencies 2019-01-21 09:58:36 +01:00
3b15fda470 frontend: Add a message on submission timeout 2019-01-21 09:58:36 +01:00
4f98536f91 sync: import MCQ justification as Flag 2019-01-21 09:58:36 +01:00
07ec6cb613 sync: Use hint title if provided 2019-01-21 09:58:36 +01:00
c1eeb382f8 frontend: fix one case hang after submission 2019-01-21 09:58:36 +01:00
b6769086c2 frontend: treat MCQ justification as key flag, instead of special case 2019-01-21 09:58:36 +01:00
2879b697c0 sync: fix ordered import 2019-01-21 09:58:36 +01:00
e57ff1be8d frontend: use the new set of icons for notification 2019-01-21 09:58:36 +01:00
19bd8cca0d admin: readd poppler in Dockerfile 2019-01-21 09:58:36 +01:00
ad9ab881dd frontend: add new set of icons 2019-01-21 09:58:36 +01:00
cf3c4b998f admin: add new event button on event-details page 2019-01-21 09:58:36 +01:00
108814b8b7 frontend: fix angular syntax error 2019-01-21 09:58:36 +01:00
f4c3f9b511 Update favicon for 2019 2019-01-21 09:58:36 +01:00
12eddadc07 frontend: browser notifications of challenge events 2019-01-21 09:58:36 +01:00
8749a7c164 Make go vet -strictshadow mostly happy 2019-01-21 09:58:36 +01:00
75463dcebb backend: rely on configuration instead of started file to determine if the challenge is launched or not 2019-01-21 09:58:36 +01:00
af1cecd3ce admin: highlight revoked certificates on PKI page 2019-01-21 09:58:36 +01:00
891b2ea6bf libfic: fix a potential memory/SQL connection leak 2019-01-21 09:58:36 +01:00
024ae04f45 admin: new page to see score details 2019-01-21 09:58:36 +01:00
4a4d0f634a settings: add new coefficient for all exercices 2019-01-21 09:58:36 +01:00
42e6a4d386 frontend: fix label selection 2019-01-21 09:58:36 +01:00
9be56fb9a2 settings: new option to postpone the activation of the given settings file 2019-01-21 09:58:36 +01:00
8e6b8829ea libfic: new way to handle exercice dependancies 2019-01-21 09:58:36 +01:00
c5f8288f39 settings: add coefficient to hint and wchoices 2019-01-21 09:58:36 +01:00
2623d9dd61 admin: new route to generate htpasswd corresponding to certificate in use by team 2019-01-21 09:58:36 +01:00
6925614f49 admin/api: use libfic struct instead of api one 2019-01-21 09:58:36 +01:00
322c53b086 frontend: add missing Biolinum font 2019-01-21 09:58:36 +01:00
f79c0ad254 fickit: add mysql backup to backend 2019-01-21 09:58:36 +01:00
9263946c88 fickit: new pkg mariadb-client 2019-01-21 09:58:36 +01:00
47006d76fe fickit: re-added sysctl 2019-01-21 09:58:36 +01:00
e5a9a2ecba fickit: dedicate an IP address to DNS/routing on frontend 2019-01-21 09:58:36 +01:00
dd2f7b0bd5 fickit: add DHCP server on admin link 2019-01-21 09:58:35 +01:00
ff5bd63eb0 fickit: allow maximal number of connections to MySQL 2019-01-21 09:58:35 +01:00
9466b1d7e6 fixkit: update to latest images 2019-01-21 09:58:35 +01:00
5a144a26f9 fickit/rsync: increase overall security 2019-01-21 09:58:35 +01:00
5e9e45da03 fickit: add DNS server 2019-01-21 09:58:35 +01:00
5516dfc3f5 fickit: upstream on VLAN2 2019-01-21 09:58:35 +01:00
7cbd7b6eeb fickit: include config to forward auth to CRI 2019-01-21 09:58:35 +01:00
5d644fa366 fickit: include local pkg 2019-01-21 09:58:35 +01:00
bcbf5b35cf backend: use TEAMS dir to resolve symlinks instead of relying on duplicates symlink in submissions 2019-01-21 09:58:35 +01:00
2582b9e208 backend: read links from TEAMS dir 2019-01-21 09:58:35 +01:00
5d31ac6e04 libfic: implement more dependancies kind 2019-01-21 09:58:35 +01:00
ff3dec059c sync: Refactor exercice flags 2019-01-21 09:58:35 +01:00
1e183f60ee libfic: extract validation_regexp execution 2019-01-21 09:58:35 +01:00
0f9cc39cc7 Update PKI dates 2019-01-21 09:58:35 +01:00
a66d6885e7 Refactor flags
Both QCM and Key are Flag
2019-01-21 09:58:35 +01:00
e029ec5414 frontend: rank: don't be too precise 2019-01-21 09:58:35 +01:00
93f36faafe admin: new route to export nginx translation file from team name to team_id 2019-01-21 09:58:35 +01:00
20df137eeb Update fickit 2019-01-21 09:58:35 +01:00
a499d23149 Update .gitignore 2019-01-21 03:08:06 +01:00
d60e9264e3 dashboard: perfect view 2019-01-21 03:08:06 +01:00
25b23e7ae0 sync: fix message 2019-01-21 03:08:06 +01:00
ba9bf4ef45 sync: ignore bad named directory when looking for dependancies 2019-01-21 03:08:06 +01:00
d1e98fc4f9 admin: fix bad location change after exercice deletion 2019-01-21 03:08:06 +01:00
03e3bb8118 frontend: change exercice border coloration when solved or bonus are active 2019-01-21 03:08:06 +01:00
6f5d7828db frontend: in rank, hilight current team line 2019-01-21 03:08:06 +01:00
0d8505131e sync: automatically add &nbsp; before ponctuation 2019-01-21 03:08:06 +01:00
0075bdeb52 admin: update public screen presets 2019-01-21 03:08:06 +01:00
6df8ee8eb7 Avoid too much useless precision when displaying scores 2019-01-21 03:08:06 +01:00
3a372b85c5 settings: reload through SIGHUP 2019-01-21 03:08:06 +01:00
ab67146c0f backend: new option --skipInitialGeneration to skip the full static files regeneration on start 2019-01-21 03:08:06 +01:00
8c87451d80 sync: better trim authors lines 2019-01-21 03:08:06 +01:00
f3eabd74fc admin: add wchoices in team_history.json 2019-01-21 03:08:06 +01:00
1b75547308 db: cap the maximum number of simultaneous connections to the database 2019-01-21 03:08:06 +01:00
99ef5046db admin: add button to move to previous and next exercice 2019-01-21 03:08:06 +01:00
aa3750bb68 dashboard: improve general design (mostly events related) 2019-01-21 03:08:06 +01:00
83ad6340b2 admin: display important information first
No more useless column, link with theme when possible
2019-01-21 03:08:06 +01:00
81ce648b5d admin: add related theme in exercice list page 2019-01-21 03:08:06 +01:00
78b6211b94 include id_theme in Exercice struct 2019-01-21 03:08:06 +01:00
69979ced1d admin: able to download files through /files/ route 2019-01-21 03:08:06 +01:00
e2fdce10ef frontend: click on card to go to the related theme/defi 2019-01-21 03:08:06 +01:00
f2f94a399b synchro: copy symlink as symlink 2019-01-21 03:08:06 +01:00
a5eb6ca285 frontend: move helper string as input placeholder 2019-01-21 03:08:06 +01:00
8d5504205e frontend: add a warning about malicious files 2019-01-21 03:08:06 +01:00
2b0d16aa0d backend: format events with non-breakable spaces 2019-01-21 03:08:05 +01:00
0c5aa65092 frontend: use monospaced font in flag input 2019-01-21 03:08:05 +01:00
85658bb3c6 admin: secondary formating 2019-01-21 03:08:05 +01:00
4de0c64672 fill_teams.sh: add a new option to generate password 2019-01-21 03:08:05 +01:00
a1c94d582d admin: score grid is a JSON to display scoring detail for a team 2019-01-21 03:08:05 +01:00
5dbf60eaa2 admin: new route to try a flag 2019-01-21 03:08:05 +01:00
d89cd2f0ca sync: Allow \r at EOL in DIGESTS 2019-01-21 03:08:05 +01:00
910ec94fd8 Add a new setting to don't count same responses in scores 2019-01-21 03:08:05 +01:00
8dc460b507 rank: count wchoices in score 2019-01-21 03:08:05 +01:00
2c5144aac0 fic: improve ranking query lisibility 2019-01-21 03:08:05 +01:00
74550f8907 rank: fix long running scoring error 2019-01-21 03:08:05 +01:00
7edd70c3c0 admin: apply settings to internal structures
This allows scores and rank to be properly generated in admin interface.
2019-01-21 03:08:05 +01:00
8edc8e697c infra: dusting 2019-01-21 03:08:05 +01:00
93519e5f62 dashboard: improve animation 2019-01-21 03:08:05 +01:00
2daa04bb5f backend: wording defi instead of challenge 2019-01-21 03:08:05 +01:00
d6dfdbc238 admin: generate events file on delete 2019-01-21 03:08:05 +01:00
7970b552e9 dashboard: move public.json files into a dedicated directory 2019-01-21 03:08:05 +01:00
485ffafc9a admin: display errmsg 2019-01-21 03:08:05 +01:00
2c5325c507 frontend: CSS formating in markdown 2019-01-21 03:08:05 +01:00
3f9e5f887a frontend: allow two defi in 2 differents themes to have the same name 2019-01-21 03:08:05 +01:00
d1ce2a0740 Wording: tentative is better than soumission 2019-01-21 03:08:05 +01:00
2402097012 frontend: design 2019-01-21 03:08:05 +01:00
819614278f Update bootstrap 2019-01-21 03:08:05 +01:00
4ea34e0136 frontend: sticky-top navbar 2019-01-21 03:08:05 +01:00
c02b30409b use clearfix feature from bootstrap 2019-01-21 03:08:05 +01:00
c877da1161 admin: use accordeon on exercice page 2019-01-21 03:08:05 +01:00
07dcc1804b admin: new button in navbar to regenerate static files 2019-01-21 03:08:05 +01:00
f2e1268398 admin: rearrange settings page 2019-01-21 03:08:05 +01:00
6aacce23ca admin: add the ability to deep sync from interface 2019-01-21 03:08:05 +01:00
4dba8dc882 admin: improve notify with HTML, margins, ... 2019-01-21 03:08:05 +01:00
b2e639697f admin: place notify at the bottom of the screen 2019-01-21 03:08:05 +01:00
5ad7d208b3 admin: fix notify closing 2019-01-21 03:08:05 +01:00
d1a41bbcb7 admin: add time progress bar 2019-01-21 03:08:05 +01:00
f27072db16 common.js: handle compound names 2019-01-21 03:08:05 +01:00
ff3e83e9ee admin: theme page format 2019-01-21 03:08:05 +01:00
2ccc59b4fa admin: add the ability to sync only one exercice 2019-01-21 03:08:05 +01:00
dc4a4925e3 sync: refactor exercice synchronization 2019-01-21 03:08:05 +01:00
5b53fbda0b common.js: add stripHTML filter 2019-01-21 03:08:05 +01:00
c8cbbcb84d admin: use common.js as well 2019-01-21 03:08:05 +01:00
598f4a5076 frontend: replace the niceborder under the menu by a time progressbar 2019-01-21 03:08:05 +01:00
dff8431e8b frontend: improve responsiveness 2019-01-21 03:08:05 +01:00
255a567e5c frontend: fix MCQ alignment 2019-01-21 03:08:05 +01:00
9d18d0733b frontend: add animation on frontpage 2019-01-21 03:08:05 +01:00
9ac3fc7e35 frontend: make tags fit in screen with a scrollbar 2019-01-21 03:08:04 +01:00
7f2ae673d0 sync: try to remove old exercice without any player try 2019-01-21 03:08:04 +01:00
c05609f85f fic: fix exercice indicated as solved 2019-01-21 03:08:04 +01:00
4f088d1cdb frontend: tag page includes theme image 2019-01-21 03:08:04 +01:00
592db2dbba frontend: tags are now ordered 2019-01-21 03:08:04 +01:00
0e36a850cf Array flags can be non-ordered 2019-01-21 03:08:04 +01:00
dbf1985d25 Implement flag arrays 2019-01-21 03:08:04 +01:00
3056a19d09 dashboard: refactor interface 2019-01-21 03:08:04 +01:00
b9f1822a65 dashboard: adapt planning from challenge start settings 2019-01-21 03:08:04 +01:00
f9237d2dcf css: clock is now a css class 2019-01-21 03:08:04 +01:00
46aaa7cb4a dashboard: use last-modified field instead of etag 2019-01-21 03:08:04 +01:00
e9fd9c4e9a Mutualise some common JS functions 2019-01-21 03:08:04 +01:00
3df8d24e33 dashboard: rename CountdownController to TimerController 2019-01-21 03:08:04 +01:00
deb12052b5 sync: include headline in exercice overview, as it is difficult to retrieve otherwise 2019-01-21 03:08:04 +01:00
99024ee5ce admin: Improve public interface 2019-01-21 03:08:04 +01:00
d25462177e admin: improve claim interface 2019-01-21 03:08:04 +01:00
8463993581 frontend: Add field to filter tag list 2019-01-21 03:08:04 +01:00
3ad7976e4b libfic: simplify git usability of the reset list 2019-01-21 03:08:04 +01:00
476f0f553c implement choices_cost 2019-01-21 03:08:04 +01:00
f9abdd23c6 Dependancy between flags 2019-01-21 03:08:04 +01:00
711db60a4c frontend: fix wording and tooltips 2019-01-21 03:08:04 +01:00
21697f01ca New field for exercice to display a text after exercice validation 2019-01-21 03:08:04 +01:00
87471acf98 sync: import files in markdown, relative to theme/exercice dir 2019-01-21 03:08:04 +01:00
8c95782eff Implement and display headlines in interface 2019-01-21 03:08:02 +01:00
abd7fc6bef sync: randomize imports: themes order, MCQ and UCQ choices 2019-01-21 03:07:47 +01:00
614003a7cd libfic: handle mcq in team history 2019-01-21 03:07:47 +01:00
c5b65289d3 Add new helper string related to justified MCQ flag 2019-01-21 03:07:47 +01:00
11e0b46034 pki: fix out-of-bound error when a symlink directory doesn't contain a serial 2019-01-21 03:07:47 +01:00
024d34f0e4 frontend: registration is Ok 2019-01-21 03:07:47 +01:00
7b0e8195ff frontend: keep answers on screen after submission (lost on refresh) 2019-01-21 03:07:47 +01:00
87b41ab3cc frontend: save flag fields between 2 refresh and pages 2019-01-21 03:07:47 +01:00
63a55a8a0b nginx: error pages are now respond as json if accept header request it.
As a consequence, we can rely on them to display a correct information on user pages through angularJS.
2019-01-21 03:07:47 +01:00
cf290732dc frontend: css: add bottom border to most of cards and jumboframe 2019-01-21 03:07:47 +01:00
a06a256c21 frontend: deny hint reveal after challenge's end + respond with 410 GONE 2019-01-21 03:07:47 +01:00
0f48b27a04 Avoid Atoi to avoid int convertion 2019-01-21 03:07:47 +01:00
8702db568c frontend: rework refresh loop 2019-01-21 03:07:47 +01:00
0414c392bf frontend: console.log is not a good way to handle errors 2019-01-21 03:07:47 +01:00
07cea2e04a frontend: use settings to display change name form or not and registration 2019-01-21 03:07:47 +01:00
5f660702eb backend: fix handling of invalid mcq justification 2019-01-21 03:07:47 +01:00
d40922629b Utilise a new field to send justifications instead of too complex guessing crap 2019-01-21 03:07:47 +01:00
69a866bbbf frontend: when a justification is valid, check the MCQ box 2019-01-21 03:07:47 +01:00
ad6fe0394f admin: API version bump 2019-01-21 03:07:47 +01:00
3838f7645d frontend: fix race condition in interface 2019-01-21 03:07:47 +01:00
3dcb233c3f handle justified MCQ in interface and submission 2019-01-21 03:07:47 +01:00
01368dd6f4 frontend: expose UCQ choices 2019-01-21 03:07:47 +01:00
c9152c90e6 frontend: fix exercice icon mess 2019-01-21 03:07:47 +01:00
5c742834ea frontend: public part now validate through blake2b.js flags and MCQs 2019-01-21 03:07:47 +01:00
8ac2776cca Export help format string in my.json 2019-01-21 03:07:47 +01:00
195490484c Change exported flags format in my.json 2019-01-21 03:07:47 +01:00
d6ae1551ba backend: fix formating issue, thanks to go vet 2019-01-21 03:07:47 +01:00
ef26e46ac9 frontend: improve theme page, with icons 2019-01-21 03:07:47 +01:00
bb33572b19 frontend: really implement next challenge button 2019-01-21 03:07:47 +01:00
17ef0b0a32 libfic: fix hint deletion in team history 2019-01-21 03:07:47 +01:00
521507b8e3 frontend: add active class on tag menu 2019-01-21 03:07:47 +01:00
c11f2403d2 frontend: why so much useless style? 2019-01-21 03:07:47 +01:00
d0bd722c92 frontend: add a menu items regrouping tags 2019-01-21 03:07:47 +01:00
c43bafa21b frontend: cap the size of heading image 2019-01-21 03:07:47 +01:00
3a0c892148 sync: import heading theme image 2019-01-21 03:07:45 +01:00
0effdbcf5e Themes can have header image 2018-12-09 20:41:43 +01:00
26295dd978 frontend: new page theme 2018-12-09 20:41:43 +01:00
ea56219fa1 frontend: display tags and add new page to filter exercices by tag 2018-12-09 20:41:43 +01:00
9e2c0b2610 frontend: some spelling in rules page 2018-12-09 20:41:43 +01:00
bc2d09e14e frontend: refactor home page 2018-12-09 20:41:43 +01:00
168e7cd636 frontend: use a menu to group scenarii 2018-12-09 20:41:43 +01:00
a5dbde7fb5 frontend: don't animate twice countdown time separator when time expired 2018-12-08 20:35:36 +01:00
bd0416eede frontend: fix race condition in interface 2018-12-08 20:35:36 +01:00
129baaacee Update Dockerfiles 2018-12-08 20:35:36 +01:00
2259c78730 dashboard: came back online 2018-12-08 20:35:36 +01:00
d7553f0392 Handle justified MCQ in admin and sync part 2018-12-08 20:35:36 +01:00
488a032eba Handle choices in UCQ (db, sync done) 2018-12-08 20:35:36 +01:00
333bb408e1 backend: save the checksum of each try, to be able to detect duplicates after 2018-12-08 20:35:36 +01:00
44d335bc9f Add issue field for exercice, to be able to communicate about problem with exercice 2018-12-08 20:35:36 +01:00
0654033721 admin: use toolbar inside exercice details 2018-12-08 20:35:36 +01:00
6b54704d59 admin: can perform mass editing on exercices 2018-12-08 20:35:36 +01:00
1166a925fe frontend: display key helper 2018-12-08 20:35:36 +01:00
e85a41d713 frontend: don't show point lost after unlock hint 2018-12-08 20:35:36 +01:00
f183985982 admin: Add exercice's tags: sync, api, interface done 2018-12-08 20:35:36 +01:00
665fd301c6 admin: avoid HTML button without type 2018-12-08 20:35:36 +01:00
1c09ae2fa8 admin: Continue refactoring of exercice view 2018-12-08 20:35:36 +01:00
06dcd0c2b7 admin: Refactor exercice page to include regexp validator 2018-12-08 20:35:36 +01:00
5eaf1926c1 Update angulasJS 2018-12-08 20:35:36 +01:00
ff56ec9fe3 libfic/flag: add validatorRegexp field 2018-12-08 20:35:32 +01:00
c2558fe0ec backend: refactor submissions 2018-12-08 20:34:05 +01:00
c5017c83bd Update logo to FIC 2019 one 2018-12-08 20:34:05 +01:00
232327e89e Use new ComCyber logotype 2018-12-08 20:34:05 +01:00
d21f3b0b83 Rename Exercice's Keys as Flags 2018-12-08 20:34:04 +01:00
f36e1c4e4d Stores ignorecase property for flags 2018-12-08 20:33:39 +01:00
3146e75ead sync: rehandle dependency, trivial processing 2018-12-08 20:32:36 +01:00
2a6fbd4e32 admin&sync: insert format helper in database 2018-12-08 20:32:33 +01:00
971273a185 admin: improve usability of theme edition page 2018-12-08 03:23:08 +01:00
1e2a74f3ca sync: add dependency on flag to download file 2018-12-08 03:23:08 +01:00
dcfb34c6fd libfic: fix missing field retrieve in EFile 2018-12-08 03:23:08 +01:00
f9e1cf6691 sync: add a new section to allow locking file waiting flag validation 2018-12-08 03:23:08 +01:00
da2a88a3a6 sync: parse complex AUTHORS.txt as described in README 2018-12-08 03:23:08 +01:00
2a941a4fc7 db: support real UTF-8
https://medium.com/@adamhooper/in-mysql-never-use-utf8-use-utf8mb4-11761243e434
2018-12-08 03:23:08 +01:00
20dfd99ec0 admin: new route to check file on disk 2018-12-08 03:23:08 +01:00
5b30788cff libfic: Fix a nil dereference when checking size of not found files 2018-12-08 03:23:08 +01:00
af2fe21d73 admin: initialize directory structure and required files at launch 2018-12-08 03:23:08 +01:00
7da6f5cd0c settings: add VideosLink parameter 2018-12-08 03:23:08 +01:00
6034246015 Retrieve time through X-FIC-Time header instead of time.json 2018-12-08 03:23:08 +01:00
c33390fa80 sync: import texts as Markdown 2018-12-08 03:23:08 +01:00
be7a159815 sync: Perfers content from challenge.txt to import hints 2018-12-08 03:23:08 +01:00
baf12f87a3 frontend: Add -simulator option to serve file without nginx (usefull for some development purposes) 2018-12-08 03:23:07 +01:00
9ab5738cff admin/sync: theme's name is now part of the theme's dirname 2018-12-08 03:23:07 +01:00
cb02fa98dd libfic: fix WipeMCQ which didn't work 2018-12-08 03:23:07 +01:00
66391baeef sync: alert when imported file is empty 2018-12-08 03:23:07 +01:00
a6bc0727b2 admin: use spacing bootstrap utility 2018-12-08 03:23:07 +01:00
3a65363ebb admin: implement MCQ edition in interface 2018-12-08 03:23:07 +01:00
92ba880006 sync: save import_report into StaticDir 2018-12-08 03:23:07 +01:00
e6b1b932f4 sync: fix synchronized URLId 2018-12-08 03:23:07 +01:00
d05c211a7c sync/file: hide by default the whole calculated digest
This is to avoid direct copy/paste to DIGESTS.txt without real local calculation.
2018-12-08 03:23:07 +01:00
Thibaut
3b7d9a2a75 sync: handle new sync format: flags 2018-12-08 03:23:07 +01:00
Thibaut
af55c5af9f sync: handle new sync format: extends challenge defines 2018-12-08 03:23:07 +01:00
Thibaut
d303ecfa38 sync: handle new sync format: filenames and locations 2018-06-22 20:34:35 +02:00
Thibaut
12cb4e95f4 libfic: update SQL modes to be compatible with lastest MySQL version 2018-06-05 13:37:03 +02:00
Harish SEGAR
76c10e92a4 [Docker] Pass FICCA env var to admin container to remove the WARNING about emptiness
Launch your docker-compose like that:
	42sh$ FICCA=`pwgen -1 20` docker-compose up
2018-05-17 17:02:25 +02:00
Harish SEGAR
351cc5943a [Core] Updated the README for docker env build. 2018-05-17 17:01:07 +02:00
0697bede3e compose: prefers path in $HOME instead of root fs 2018-05-14 21:10:28 +02:00
492ab72dcd sync: read UTF8 string, don't expect sane encoding from imported files, just force it 2018-05-13 14:16:06 +02:00
e126743d69 sync: Update specifications
Add same constraint for hint files than files (need DIGESTS.txt).
Update flag_ucq usage (difference between value and label).
2018-05-13 14:16:06 +02:00
f70fb20a70 reset db: use transaction when turncating tables 2018-05-13 14:16:06 +02:00
dcb67fba63 Docs, docs, docs! 2018-05-13 14:15:07 +02:00
12a85ee804 admin: fix bootstrap 4.0 custom checkbox 2018-05-11 15:03:11 +02:00
72db1c92e7 frontend: update RCC naming 2018-05-11 15:03:11 +02:00
3466f4956a Add LICENSE: chose MIT 2018-05-11 15:03:11 +02:00
5377d15313 Start writing hands-on documentation 2018-05-11 15:03:11 +02:00
df4bcd9786 Add Dockerfiles and docker-compose 2018-05-11 15:03:10 +02:00
0d125071ef admin/sync: update README.md to introduce new syntax of the year 2018-05-11 15:03:10 +02:00
1a959862f1 admin/sync: fill README.md with synchronisation instructions for students 2018-05-11 15:03:10 +02:00
156a87abc0 admin/pki: use symlink instead of DB to associate certificate to team 2018-05-11 15:03:10 +02:00
73eb04bcf0 Extract public interface into a separate project: dashboard 2018-05-11 15:03:10 +02:00
b0f81c59d4 Update logo to FIC 2019 developers 2018-05-11 15:03:10 +02:00
2fbe7a327e password_paper: fix handling of ^ 2018-05-11 15:03:10 +02:00
bcc598ebd5 Write docs! 2018-05-11 15:03:09 +02:00
c460bb7bf5 Handle graceful http shutdown 2018-05-11 05:27:51 +02:00
0ec90b14c6 Fixes thanks to go vet 2018-05-11 05:27:51 +02:00
4b538cdea8 admin: improve exercice-list 2018-05-11 05:27:51 +02:00
a78973be29 admin: fix margin mess in menus 2018-05-11 05:27:51 +02:00
4077431ef0 admin: remove last occurence of initialName 2018-05-11 05:27:51 +02:00
faab83e037 admin: improve claim-list usability 2018-05-11 05:27:51 +02:00
b8cbb9f758 frontend: improve exercice selection menu 2018-05-11 05:27:51 +02:00
bd51d177b5 frontend: display an error message when the team is not registered and registration are not allowed 2018-05-11 05:27:51 +02:00
25d242b76c public: make teams number dynamic 2018-05-11 05:27:51 +02:00
00688cb996 public: news carousel for ranking 2018-05-11 05:27:51 +02:00
62aea5413e use more official blake2b checksum 2018-05-11 05:27:51 +02:00
1dcebc4eca public: new carousels in pubic interface: teams and exercices
+ fix autocarousel directive
2018-05-11 05:27:51 +02:00
68e5c4cd2b pki: improve serial number generation + fix team association
Replace math/rand by crypto/rand.

Fix big when associating certificate with leading zero: nginx prepend 0 wherehas we don't.
2018-05-11 05:27:51 +02:00
3ed8c619b1 admin: disable revoke button when already revoked 2018-05-11 05:27:51 +02:00
fc456a41f2 Add configuration for prod 2018-05-11 05:27:51 +02:00
4b21931ff0 synchro: add synchronization script 2018-05-11 05:27:51 +02:00
eeaff28b31 Add nginx config 2018-05-11 05:27:51 +02:00
a5111aa2fb Add sample hosts 2018-05-11 05:27:51 +02:00
0b2e61faef frontend/public: minor fixes 2018-05-11 05:27:51 +02:00
9d36e55227 frontend/public: Update RCC name 2018-05-11 05:27:51 +02:00
fb89ca5938 frontend/public: don't require etag header 2018-05-11 05:27:51 +02:00
ff98bfa79f admin: can edit flags after creation 2018-05-11 05:27:51 +02:00
fdafbed237 frontend: last minute css fixes 2018-05-11 05:27:51 +02:00
167a6ae1cb admin: fix some details about times 2018-05-11 05:27:51 +02:00
45620ba4c2 admin: try to generate events.json file 2018-05-11 05:27:51 +02:00
3aadab40b0 public interface: random fixes 2018-05-11 05:27:51 +02:00
e53bcadc05 admin: add happy hour preselect public scene 2018-05-11 05:27:51 +02:00
1110afa058 public front: can choose levels to display in levels table 2018-05-11 05:27:51 +02:00
1e36eb8b2f events.json: display 10 events instead of 6 2018-05-11 05:27:51 +02:00
bd924150eb public front: add new carousel displaying themes 2018-05-11 05:27:51 +02:00
91663f55af my-public.json: prefer display overview instead of statement, if available 2018-05-11 05:27:51 +02:00
4bf9262e79 frontend: display challenge introduction when there is access restrinction 2018-05-11 05:27:51 +02:00
1b6587de24 frontend: fix crazy events 2018-05-11 05:27:51 +02:00
3c8f9e55b6 db: allow multiple identical events 2018-05-11 05:27:51 +02:00
e02e98f8f3 frontend: don't suggest answers as autocompletion 2018-05-11 05:27:51 +02:00
0047c48e72 frontend: fix display of hint cost 2018-05-11 05:27:51 +02:00
1c18b797e0 backend: generate events.json when needed 2018-05-11 05:27:51 +02:00
627c28cd94 libfic: don't show useless decimals to gain 2018-05-11 05:27:50 +02:00
6d1ef0f51c admin: new route to fill URLIds if they are not defined 2018-05-11 05:27:50 +02:00
8ae4d1c42f libfic: remove trailing - at the end of some urlids 2018-05-11 05:27:50 +02:00
2e3b262a78 frontend: move started file at a dedicated path 2018-05-11 05:27:50 +02:00
3f1f60030c db: increase DB connection retries 2018-05-11 05:27:50 +02:00
72bb5acc0a frontend: don't display prems rate if this bonus is disabled 2018-05-11 05:27:50 +02:00
4dbfffef7b admin: bump new API version 2018-05-11 05:27:50 +02:00
59beafb314 admin/pki: avoid some hard to read characters in password 2018-05-11 05:27:50 +02:00
c18465d498 Reserved directory for public interface now lives in public instead of _public 2018-05-11 05:27:50 +02:00
c118035c33 Introducing new PKI management 2018-05-11 05:27:50 +02:00
5b558bcf00 admin/api: bytes handlers now returned data as application/octet-stream 2018-05-11 05:27:50 +02:00
992221a6da Remove old PKI 2018-05-11 05:27:50 +02:00
e083da2f72 Remove team's initial_name, replaced by their ID 2018-05-11 05:27:50 +02:00
191c89f7ad backend: Don't watch symlinks nor temporary directories 2018-05-11 05:27:50 +02:00
b8f573ce86 Update to bootstrap 4.0 2018-05-11 05:27:50 +02:00
bbaf0ed9d9 Update to angularJS 1.6.8 2018-01-26 12:02:32 +01:00
0c540a39eb frontend: beautiful URLs 2018-01-26 12:02:32 +01:00
bd75157a79 frontend: improve readality and fix typo 2018-01-18 12:08:12 +01:00
1eef71923a admin: new interface to manage claims 2018-01-18 12:08:12 +01:00
3932bba83d db: when DB is unreachable, retry 5 times before fatal return 2018-01-18 12:08:12 +01:00
5b2dc909e2 frontend: add autofocus directive 2018-01-18 12:08:12 +01:00
9d4c048f0e backend: add debug logs 2018-01-18 12:08:12 +01:00
2bf88089b3 frontend: use FIC2018 logo 2018-01-18 12:08:12 +01:00
ca41e4edee admin/sync: defines.txt can also contains informations about hints 2018-01-18 12:08:12 +01:00
332aa90931 admin/sync: handle exercice gain from toml file: defines.txt 2018-01-18 12:08:12 +01:00
e7d3e34c0b libfic: explode theme file 2018-01-18 12:08:12 +01:00
db4cc9ce85 Update favicon 2018-01-18 12:08:12 +01:00
494ccb740b admin/sync: tiny refactor 2018-01-18 12:08:12 +01:00
5edccf21cd fixup! Update bootstrap to 4.0-beta 2018-01-18 12:08:12 +01:00
1b852f255e admin: autofocus search fields 2018-01-18 12:08:12 +01:00
beba0a615f improve overall exercices interfaces 2018-01-18 12:08:12 +01:00
11d0fe8d1f admin/sync: handle hint files download 2018-01-18 12:08:12 +01:00
6cc40be36a admin/sync: rename Indice to Astuce 2018-01-18 12:08:12 +01:00
db9077a85c admin/sync: add stat method to importer 2018-01-18 12:08:12 +01:00
57758cd018 admin/sync: avoid error when no depends.txt exists 2018-01-18 12:08:12 +01:00
24f527ab8a admin/sync: includes mcq and ucq in full synchronization 2018-01-18 12:08:12 +01:00
935b1666ac admin/sync: Add last sync date into full_import_report 2018-01-18 12:08:12 +01:00
809d166a2d admin/sync: avoid false positive when no files are distributed 2018-01-18 12:08:12 +01:00
fc902e1063 admin/sync: import resolution movies 2018-01-18 12:08:12 +01:00
2aa1d6eeca libfic: fix bad hints displayed 2018-01-18 12:08:11 +01:00
5e2e03f5e9 admin: can edit theme introductions 2018-01-18 12:08:11 +01:00
0f9c8e0335 admin: use light text on dark background in interface 2018-01-18 12:08:11 +01:00
87428909b2 admin: avoid CSRF: use POST instead of GET, mainly for synchronisation methods 2018-01-18 12:08:11 +01:00
9a1a64c41c admin: complet API and interface with files checking page 2018-01-18 12:08:11 +01:00
184714aeeb frontend: team registration 2018-01-18 12:08:11 +01:00
bc135d00c5 admin: general statistics page 2018-01-18 12:08:11 +01:00
ea3f3b709d admin/sync: import theme introductions 2018-01-18 12:08:10 +01:00
39b57119fe frontend: improve partial validation visibility 2018-01-17 18:52:48 +01:00
e9910fe827 admin: can delete team history item 2018-01-17 18:52:48 +01:00
a0737d91b9 libfic: force MySQL charset 2018-01-17 18:52:47 +01:00
76597280f5 frontend: add button to next challenge 2018-01-17 18:52:47 +01:00
bc9d27aa94 public: can control up to 9 separate displays 2018-01-17 18:52:47 +01:00
baf992bccb admin: fix camembert size overflow 2018-01-17 18:52:47 +01:00
ba88129580 Improve public screen page 2018-01-17 18:52:47 +01:00
55f87f7a67 Bring back glyphicons to life 2018-01-17 18:52:46 +01:00
4052969304 libfic/mcq: remove Kind, as we can only handle checkbox; another kind of record should be created to handle select/radio 2018-01-17 18:52:46 +01:00
eee1558dd9 admin/sync: new error on flags import 2018-01-17 18:52:46 +01:00
a0a2313924 admin: fix display of b2sums 2018-01-17 18:52:46 +01:00
e630bc3d75 Improve bootstrap 4 support 2018-01-17 18:52:46 +01:00
6329f44d42 admin/sync: escape cloud URL 2018-01-17 18:52:46 +01:00
11c8a56f14 admin/sync: handle dependancy between exercices 2018-01-17 18:52:45 +01:00
edc6ca9b7a change request log format, close to nginx ones 2018-01-17 18:52:45 +01:00
838918da66 Exercice: add overview field
This field is use as a high level description of the exercice. It will be
displayed on the public interface only: not to players.
2018-01-17 18:52:45 +01:00
48e6ba7861 admin: add route to handle quiz 2018-01-17 18:52:45 +01:00
830dacd6f5 Save MCQ diff 2018-01-17 18:52:45 +01:00
b079f7891c admin: sync mcq/ucq 2018-01-17 18:52:45 +01:00
d6012dfffb frontend: display MCQ in interface 2018-01-17 18:52:45 +01:00
6903c91df2 Able to check MCQ 2018-01-17 18:52:44 +01:00
037f27c62c frontend: fix orthograph, typography, ... 2018-01-17 18:52:44 +01:00
b9182786bf frontend: improve design 2018-01-17 18:52:44 +01:00
c36cd202e8 libfic: start working on MCQ: structures done 2018-01-17 18:52:44 +01:00
31b80a5b2a admin: msgbox can contains lists 2018-01-17 18:52:44 +01:00
33bf5a0f34 Update angularJS to 1.6.6 2018-01-17 18:52:44 +01:00
73080d7d0d Update bootstrap to 4.0-beta 2018-01-17 18:52:43 +01:00
978f260c64 js: compatible with angular 1.6 2018-01-17 18:52:43 +01:00
292fef7d12 admin/sync: remove old exercices no more in tree 2018-01-17 18:52:43 +01:00
68bb0e6b21 admin/sync: hide full URI from error message 2018-01-17 18:52:43 +01:00
1ab94862d0 admin/sync: can only perform one deep sync at a time 2018-01-17 18:52:43 +01:00
de3c78b2ee admin/sync: don't show error when no hints directory to import 2018-01-17 18:52:42 +01:00
bb3e4547bb admin/sync: regenerate backend after deep sync 2018-01-17 18:52:42 +01:00
09d6acc65a settings: new function to regenerate files 2018-01-17 18:52:42 +01:00
92c7de942b admin: API version bump 2018-01-17 18:52:42 +01:00
8ed23ddc7a admin: localimporter can make symlink instead of copying whole files 2018-01-17 18:52:42 +01:00
d81f068eba admin: new route to display import report 2018-01-17 18:52:42 +01:00
a543be0255 admin: able to sync splitted files 2018-01-17 18:52:42 +01:00
b4057c1a2c admin/sync: generate report on full import 2018-01-17 18:52:41 +01:00
3d59042802 admin: sync.ImportFile takes Importer as first arg 2018-01-17 18:52:41 +01:00
9a9d5fcda4 libfic: Type key is now Label 2018-01-17 18:52:41 +01:00
a1c6eadbe5 Display read-only settings for information purpose 2018-01-17 18:52:41 +01:00
6ef91a92e5 Perform full deep synchronisation 2018-01-17 18:52:41 +01:00
9225038ffa admin: interface to synchronize 2018-01-17 18:52:41 +01:00
993b83f8e7 admin: can sync exercices 2018-01-17 18:52:40 +01:00
762d3a5222 admin: synchronization of exercices, files, hints and keys 2018-01-17 18:52:40 +01:00
a033f81f5f admin: new function to retrieve file content 2018-01-17 18:52:40 +01:00
ae7e1ede14 libfic: increase authors field size 2018-01-17 18:52:40 +01:00
38d7cb00b6 libfic: add function to get exercice by title 2018-01-17 18:52:40 +01:00
f97e114a81 libfic: add functions to wipe {files,hints,keys} 2018-01-17 18:52:40 +01:00
38a0f4c9b5 libfic: Add new row in exercices table, to store relative path to exercice 2018-01-17 18:52:40 +01:00
bfd7126e1e tmp 2018-01-17 18:52:40 +01:00
4d1dde4528 admin: Implement theme synchronization 2018-01-17 18:52:39 +01:00
38606f28c7 libfic: new function to get theme by name 2018-01-17 18:52:39 +01:00
8f7de926d3 admin: Implement sychronization backends
We are now able, depending on configuration, to retrieve files from either WebDAV or local file system.
2018-01-17 18:52:39 +01:00
6237f7755a Change Key.Value to Key.Checksum 2018-01-17 18:52:39 +01:00
b84fe87f48 New functions to get file by path 2018-01-17 18:52:39 +01:00
99975d9df4 admin: Take cloud URL, user and pass from environment 2018-01-17 18:52:39 +01:00
cd5a9d06ea Define global default value at initialisation 2018-01-17 18:52:39 +01:00
bf86e40db0 fill_exercices: we are in 2018! 2018-01-17 18:52:39 +01:00
07a372ab79 fill_teams: fix path to import team members 2018-01-17 18:52:38 +01:00
e6e6e6c206 Use BLAKE2b checksum instead of SHA-1 and SHA-512 2018-01-17 18:52:38 +01:00
9325419002 import: avoid ugly padding = at the end of base32 pathname 2018-01-17 18:52:38 +01:00
21590655cb backend: detect non-atomic file operation to look at another event 2018-01-17 18:52:38 +01:00
4bd8d5f93e backend: new parameter to debug inotify 2018-01-17 18:52:38 +01:00
ddd1773777 backend: prefer watching Create event 2018-01-17 18:52:38 +01:00
557b576da5 backend: don't watch inotification under .tmp 2018-01-17 18:52:38 +01:00
3f13d81eb3 frontend: light treatment on prefix to avoid multiple / 2018-01-17 18:52:38 +01:00
f17541e252 Move settings and started file into SETTINGS directory 2018-01-17 18:52:38 +01:00
31d98285a4 frontend: refactor submission handlers 2018-01-17 18:52:37 +01:00
fb1d8f90ed frontend: don't give too much right on created files 2018-01-17 18:52:37 +01:00
b31f009d2e frontend: add script to change frontend base URL 2018-01-17 18:52:37 +01:00
76a4c09f37 admin: add comments 2018-01-17 18:52:37 +01:00
eefac93091 admin: display publication confirmation; show an alert when empty scene 2018-01-17 18:52:37 +01:00
f2089c4d96 admin: display team history 2018-01-17 18:52:37 +01:00
41400a8710 admin: add history route in API 2018-01-17 18:52:37 +01:00
e362700031 frontend: inside public interface, hide hints 2018-01-17 18:52:36 +01:00
7b2fdaf0ad admin: alert can contains yes/no buttons 2018-01-17 18:52:36 +01:00
070807b485 admin: can dismiss alert 2018-01-17 18:52:36 +01:00
def822cd45 frontend: avoid RW access to TEAMS dir by placing startedFile into submissions 2018-01-17 18:52:36 +01:00
df5c9532cd admin: add confirmation message box on error and some success 2018-01-17 18:52:36 +01:00
1458c71cfa admin: improve team-print view 2018-01-17 18:52:36 +01:00
88ef2f64c0 admin: ensure _public is created at startup 2018-01-17 18:52:36 +01:00
17d983221d libfic: split team removal in two requests 2018-01-17 18:52:36 +01:00
510e25e351 frontend: fix timer location 2018-01-17 18:52:35 +01:00
4a97b06520 Set SQL_MODES, waiting https://jira.mariadb.org/browse/MDEV-10426 to be solved 2018-01-17 18:52:35 +01:00
a15b285090 admin: fix form to append teams 2018-01-17 18:52:35 +01:00
82ecd0d6dd admin: Fix redirections when using baseurl 2018-01-17 18:52:35 +01:00
d03350f6b3 Fix generated JSON in case of error 2018-01-17 18:52:35 +01:00
ce46313dd1 admin: make baseurl optional 2018-01-17 18:52:35 +01:00
410ab529ae admin: don't need submission directory anymore 2018-01-17 18:52:35 +01:00
8a93f4bdd1 Use /bin/sh instead of bash 2018-01-17 18:52:35 +01:00
d0f588e47d Generate DNS from env 2018-01-17 18:52:34 +01:00
fad2534267 frontend: improve home page 2018-01-17 18:52:34 +01:00
e824f4982e backend: simplify condition 2018-01-17 18:52:34 +01:00
db210ebc5e admin: improve design of settings page 2018-01-17 18:52:34 +01:00
2235470d9d admin: manage team certificate from interface 2018-01-17 18:52:34 +01:00
2e8d28542e admin: unify API to revoke certificates 2018-01-17 18:52:34 +01:00
5a6b27ff18 frontend: new page that list videos 2018-01-17 18:52:34 +01:00
31c079701f admin: Add a page to list teams and members 2018-01-17 18:52:34 +01:00
6148897dac settings: add title and authors 2018-01-17 18:52:33 +01:00
963c6ff4f2 admin: fix and generalize team stats 2018-01-17 18:52:33 +01:00
dea178b7ba admin: add danger alert in select 2018-01-17 18:52:33 +01:00
1ebcdd7687 Move PKI scripts at root 2018-01-17 18:52:33 +01:00
2eb94c8ddb frontend: use ng-cloak and ng-if 2018-01-17 18:52:33 +01:00
60192a0a02 Add password paper generator 2018-01-17 18:52:33 +01:00
cc3892463a Compute hint mime type in a variable and display it instead of the hint content 2018-01-17 18:52:33 +01:00
50ec3df2d6 admin: add a route to simulate time.json on backend machine 2018-01-17 18:52:33 +01:00
4f6d4a82b0 db: add constraints to avoid multiple records of unique values 2018-01-17 18:52:32 +01:00
7597fcfe5b admin: add button and route to reset some parts 2018-01-17 18:52:32 +01:00
7478051425 admin: interface to edit teams 2018-01-17 18:52:32 +01:00
da0e7facfd frontend: improve 401 page thank to initial guide 2018-01-17 18:52:32 +01:00
c1c84ba3d1 backend: generate an event when a team open an hint 2018-01-17 18:52:32 +01:00
cb1fe0847b frontend: move file (on the same partition) instead of open, write, close the final file 2018-01-17 18:52:32 +01:00
cab95b7985 libfic: new function to retrieve exercices from a hint 2018-01-17 18:52:32 +01:00
4fe641a9f5 change the way themes are stored in stats 2018-01-17 18:52:31 +01:00
17f51f5e7b admin: can force page regeneration 2018-01-17 18:52:31 +01:00
318bc4bc4d Update openssl settings 2018-01-17 18:52:31 +01:00
544bbb745c admin: new route /members/ 2018-01-17 18:52:31 +01:00
416ad65c87 admin: add public interface management 2018-01-17 18:52:31 +01:00
7240cbb414 public interface: rework 2018-01-17 18:52:31 +01:00
d4177f6228 admin: allow import of remote hint and partials remote parts 2018-01-17 18:52:31 +01:00
b8b1f14806 admin: restore function to add team and members 2018-01-17 18:52:31 +01:00
8e91e7edbe admin: sanitize use of InitialName when needed 2018-01-17 18:52:30 +01:00
51815862f7 frontend: move time in a separate package to be used elsewhere 2018-01-17 18:52:30 +01:00
49933059f3 certificates: avoid error on noexec partition 2018-01-17 18:52:30 +01:00
4550f653ea admin: Display time before start in UI 2018-01-17 18:52:30 +01:00
8fd2cd66c1 backend: don't regenerate files if config doesn't change 2018-01-17 18:52:30 +01:00
67f27d3d8b Force cd into PKI directory 2018-01-17 18:52:30 +01:00
8d03a08717 frontend: fix partial solved flags display 2018-01-17 18:52:30 +01:00
b1c4ebfe45 settings: admin interface see default params 2018-01-17 18:52:30 +01:00
ef4a738672 admin: control settings 2018-01-17 18:52:29 +01:00
b42016c74a Coefficients transit and display on UI 2018-01-17 18:52:29 +01:00
78ce24f3f7 fixup! fixup! WIP esthetic changes 2018-01-17 18:52:29 +01:00
21e4b04c19 frontend: dedicate a field in JSON to file hint 2018-01-17 18:52:29 +01:00
b772a22705 Hints can something else than text 2018-01-17 18:52:29 +01:00
8c2e8a19d1 front: use ng-pluralize 2018-01-17 18:52:29 +01:00
31af092203 WIP esthetic changes 2018-01-17 18:52:29 +01:00
9dd376ba22 libfic: refactor rank/points SQL query 2018-01-17 18:52:28 +01:00
91182b1877 admin: Improve CA API 2018-01-17 18:52:28 +01:00
1c879fe50e squash! WIP: apply a coeff on given points 2018-01-17 18:52:14 +01:00
da29071ad1 frontend: improve rank rendering 2018-01-17 18:51:55 +01:00
3f80b89a4c fill_exercices: flags.txt files can use tabulation char as separator instead of : 2018-01-17 18:51:54 +01:00
09d1a397c0 frontend: use a common JS file to contain common features between challenger and public interface 2018-01-17 18:51:54 +01:00
0cde350c5e WIP: apply a coeff on given points 2018-01-17 18:51:54 +01:00
e1d1a8d1b1 frontend: add /rules page 2018-01-17 18:51:54 +01:00
10fe40e4a8 Settings are now given through TEAMS/settings.json instead of been given through command line arguments 2018-01-17 18:51:54 +01:00
37310e41f5 New rank and score calculation 2018-01-17 18:51:54 +01:00
80d06f237c backend: log generation errors 2018-01-17 18:51:54 +01:00
f3a484fb67 fill_exercice: define HINT_COST 2018-01-17 18:51:54 +01:00
1bd403cd8c Handle file import digest 2018-01-17 18:51:53 +01:00
0b4e8a233c admin: various fixes in fill_exercices 2018-01-17 18:51:53 +01:00
119280d814 admin: can pass args to fill_exercices to limit the fill to a theme or an exercice 2018-01-17 18:51:53 +01:00
63931f73ba admin: new argument --rapidimport to speed up the import but don't ensure consistency 2018-01-17 18:51:53 +01:00
c57b612205 Split team.go into multiple files 2018-01-17 18:51:53 +01:00
f0621fa191 [admin] Add new routes to manage hints, files and keys 2018-01-17 18:51:53 +01:00
b6782962f1 [admin] Add events 2018-01-17 18:51:53 +01:00
863070c037 [admin] Add exercices related pages 2018-01-17 18:51:53 +01:00
6a4868b9b3 [admin] Add page title 2018-01-17 18:51:52 +01:00
27ca960b2a [admin] Add ng-sanitize 2018-01-17 18:51:52 +01:00
2e718b22b6 Merge exercices API routes 2018-01-17 18:51:52 +01:00
4b4c6881c7 Bump new version API 2018-01-17 18:51:52 +01:00
3b320469b5 Use github.com/julienschmidt/httprouter instead of gorilla 2018-01-17 18:51:52 +01:00
5a0b81ba32 Merge big splitted files before import 2018-01-17 18:51:52 +01:00
ac27893a01 Use 2017 logos 2018-01-17 18:51:51 +01:00
234e0460d8 frontend: interface can open hints 2018-01-17 18:51:51 +01:00
19e73dcaa1 frontend: able to receive opening hint 2018-01-17 18:51:51 +01:00
d1c5a545d9 backend: can open hint 2018-01-17 18:51:51 +01:00
220c26d9c5 frontend: refactor and dispatch in many routes 2018-01-17 18:51:51 +01:00
7fe35c5f1c WIP misc 2018-01-17 18:51:51 +01:00
e76d055bdb Partial resolution of exercices 2018-01-17 18:51:51 +01:00
25bf34e82c Multiple hints 2018-01-17 18:51:50 +01:00
22e8937879 backend: use fsnotify instead of the deprecated inotify 2018-01-17 18:51:50 +01:00
1054dd7086 admin/api: use gorilla/mux instead of Go router 2018-01-17 18:51:44 +01:00
599 changed files with 63486 additions and 5625 deletions

33
.dockerignore Normal file
View file

@ -0,0 +1,33 @@
admin/admin
checker/checker
dashboard/dashboard
evdist/evdist
generator/generator
receiver/receiver
repochecker/repochecker
frontend/fic/build
frontend/fic/node_modules
qa/ui/build
qa/ui/node_modules
fickit-backend-initrd.img
fickit-backend-kernel
fickit-backend-squashfs.img
fickit-backend-state
fickit-frontend-initrd.img
fickit-frontend-kernel
fickit-frontend-squashfs.img
fickit-frontend-state
fickit-prepare-initrd.img
fickit-prepare-kernel
fickit-update-initrd.img
fickit-update-kernel
DASHBOARD
FILES
PKI
REMOTE
repochecker/*.so
SETTINGS
SETTINGSDIST
submissions
TEAMS
vendor

View file

@ -0,0 +1,22 @@
image: nemunaire/fic-admin:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}
{{#if build.tags}}
tags:
{{#each build.tags}}
- {{this}}
{{/each}}
{{/if}}
manifests:
- image: nemunaire/fic-admin:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64
platform:
architecture: amd64
os: linux
- image: nemunaire/fic-admin:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64
platform:
architecture: arm64
os: linux
variant: v8
- image: nemunaire/fic-admin:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm
platform:
architecture: arm
os: linux
variant: v7

View file

@ -0,0 +1,22 @@
image: nemunaire/fic-checker:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}
{{#if build.tags}}
tags:
{{#each build.tags}}
- {{this}}
{{/each}}
{{/if}}
manifests:
- image: nemunaire/fic-checker:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64
platform:
architecture: amd64
os: linux
- image: nemunaire/fic-checker:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64
platform:
architecture: arm64
os: linux
variant: v8
- image: nemunaire/fic-checker:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm
platform:
architecture: arm
os: linux
variant: v7

View file

@ -0,0 +1,22 @@
image: nemunaire/fic-dashboard:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}
{{#if build.tags}}
tags:
{{#each build.tags}}
- {{this}}
{{/each}}
{{/if}}
manifests:
- image: nemunaire/fic-dashboard:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64
platform:
architecture: amd64
os: linux
- image: nemunaire/fic-dashboard:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64
platform:
architecture: arm64
os: linux
variant: v8
- image: nemunaire/fic-dashboard:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm
platform:
architecture: arm
os: linux
variant: v7

View file

@ -0,0 +1,22 @@
image: nemunaire/fic-evdist:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}
{{#if build.tags}}
tags:
{{#each build.tags}}
- {{this}}
{{/each}}
{{/if}}
manifests:
- image: nemunaire/fic-evdist:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64
platform:
architecture: amd64
os: linux
- image: nemunaire/fic-evdist:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64
platform:
architecture: arm64
os: linux
variant: v8
- image: nemunaire/fic-evdist:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm
platform:
architecture: arm
os: linux
variant: v7

View file

@ -0,0 +1,22 @@
image: nemunaire/fic-frontend-ui:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}
{{#if build.tags}}
tags:
{{#each build.tags}}
- {{this}}
{{/each}}
{{/if}}
manifests:
- image: nemunaire/fic-frontend-ui:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64
platform:
architecture: amd64
os: linux
- image: nemunaire/fic-frontend-ui:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64
platform:
architecture: arm64
os: linux
variant: v8
- image: nemunaire/fic-frontend-ui:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm
platform:
architecture: arm
os: linux
variant: v7

View file

@ -0,0 +1,22 @@
image: nemunaire/fic-generator:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}
{{#if build.tags}}
tags:
{{#each build.tags}}
- {{this}}
{{/each}}
{{/if}}
manifests:
- image: nemunaire/fic-generator:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64
platform:
architecture: amd64
os: linux
- image: nemunaire/fic-generator:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64
platform:
architecture: arm64
os: linux
variant: v8
- image: nemunaire/fic-generator:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm
platform:
architecture: arm
os: linux
variant: v7

View file

@ -0,0 +1,22 @@
image: nemunaire/fic-get-remote-files:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}
{{#if build.tags}}
tags:
{{#each build.tags}}
- {{this}}
{{/each}}
{{/if}}
manifests:
- image: nemunaire/fic-get-remote-files:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64
platform:
architecture: amd64
os: linux
- image: nemunaire/fic-get-remote-files:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64
platform:
architecture: arm64
os: linux
variant: v8
- image: nemunaire/fic-get-remote-files:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm
platform:
architecture: arm
os: linux
variant: v7

View file

@ -0,0 +1,22 @@
image: nemunaire/fic-nginx:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}
{{#if build.tags}}
tags:
{{#each build.tags}}
- {{this}}
{{/each}}
{{/if}}
manifests:
- image: nemunaire/fic-nginx:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64
platform:
architecture: amd64
os: linux
- image: nemunaire/fic-nginx:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64
platform:
architecture: arm64
os: linux
variant: v8
- image: nemunaire/fic-nginx:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm
platform:
architecture: arm
os: linux
variant: v7

View file

@ -0,0 +1,22 @@
image: nemunaire/fic-qa:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}
{{#if build.tags}}
tags:
{{#each build.tags}}
- {{this}}
{{/each}}
{{/if}}
manifests:
- image: nemunaire/fic-qa:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64
platform:
architecture: amd64
os: linux
- image: nemunaire/fic-qa:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64
platform:
architecture: arm64
os: linux
variant: v8
- image: nemunaire/fic-qa:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm
platform:
architecture: arm
os: linux
variant: v7

View file

@ -0,0 +1,22 @@
image: nemunaire/fic-receiver:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}
{{#if build.tags}}
tags:
{{#each build.tags}}
- {{this}}
{{/each}}
{{/if}}
manifests:
- image: nemunaire/fic-receiver:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64
platform:
architecture: amd64
os: linux
- image: nemunaire/fic-receiver:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64
platform:
architecture: arm64
os: linux
variant: v8
- image: nemunaire/fic-receiver:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm
platform:
architecture: arm
os: linux
variant: v7

View file

@ -0,0 +1,22 @@
image: nemunaire/fic-repochecker:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}
{{#if build.tags}}
tags:
{{#each build.tags}}
- {{this}}
{{/each}}
{{/if}}
manifests:
- image: nemunaire/fic-repochecker:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64
platform:
architecture: amd64
os: linux
- image: nemunaire/fic-repochecker:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64
platform:
architecture: arm64
os: linux
variant: v8
- image: nemunaire/fic-repochecker:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm
platform:
architecture: arm
os: linux
variant: v7

View file

@ -0,0 +1,22 @@
image: nemunaire/fickit-deploy:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}
{{#if build.tags}}
tags:
{{#each build.tags}}
- {{this}}
{{/each}}
{{/if}}
manifests:
- image: nemunaire/fickit-deploy:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64
platform:
architecture: amd64
os: linux
- image: nemunaire/fickit-deploy:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64
platform:
architecture: arm64
os: linux
variant: v8
- image: nemunaire/fickit-deploy:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm
platform:
architecture: arm
os: linux
variant: v7

816
.drone.yml Normal file
View file

@ -0,0 +1,816 @@
---
kind: pipeline
type: docker
name: build-amd64
platform:
os: linux
arch: amd64
workspace:
base: /go
path: src/srs.epita.fr/fic-server
steps:
- name: get deps
image: golang:alpine
commands:
- apk --no-cache add git
- go get -v -d ./...
- mkdir deploy
- name: build qa ui
image: node:23-alpine
commands:
- cd qa/ui
- npm install --network-timeout=100000
- npm run build
- tar chjf ../../deploy/htdocs-qa.tar.bz2 build
- name: vet and tests
image: golang:alpine
commands:
- apk --no-cache add build-base
- go vet -buildvcs=false -tags gitgo ./...
- go vet -buildvcs=false ./...
- go test ./...
- name: build admin
image: golang:alpine
commands:
- go build -buildvcs=false -tags gitgo -o deploy/admin-gitgo-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/admin
- go build -buildvcs=false -o deploy/admin-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/admin
- tar chjf deploy/htdocs-admin.tar.bz2 htdocs-admin
environment:
CGO_ENABLED: 0
when:
branch:
exclude:
- master
- name: build checker
image: golang:alpine
commands:
- go build -buildvcs=false -o deploy/checker-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/checker
environment:
CGO_ENABLED: 0
when:
branch:
exclude:
- master
- name: build evdist
image: golang:alpine
commands:
- go build -buildvcs=false -o deploy/evdist-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/evdist
environment:
CGO_ENABLED: 0
when:
branch:
exclude:
- master
- name: build generator
image: golang:alpine
commands:
- go build -buildvcs=false -o deploy/generator-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/generator
environment:
CGO_ENABLED: 0
when:
branch:
exclude:
- master
- name: build receiver
image: golang:alpine
commands:
- go build -buildvcs=false -o deploy/receiver-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/receiver
environment:
CGO_ENABLED: 0
when:
branch:
exclude:
- master
- name: build frontend fic ui
image: node:23-alpine
commands:
- cd frontend/fic
- npm install --network-timeout=100000
- npm run build
- tar chjf ../../deploy/htdocs-frontend-fic.tar.bz2 build
when:
branch:
exclude:
- master
- name: build dashboard
image: golang:alpine
commands:
- go build -buildvcs=false -o deploy/dashboard-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/dashboard
- tar chjf deploy/htdocs-dashboard.tar.bz2 htdocs-dashboard
environment:
CGO_ENABLED: 0
when:
branch:
exclude:
- master
- name: build repochecker
image: golang:alpine
commands:
- apk --no-cache add build-base
- go build -buildvcs=false --tags checkupdate -o deploy/repochecker-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/repochecker
- go build -buildvcs=false -buildmode=plugin -o deploy/repochecker-epita-rules-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}.so srs.epita.fr/fic-server/repochecker/epita
- go build -buildvcs=false -buildmode=plugin -o deploy/repochecker-file-inspector-rules-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}.so srs.epita.fr/fic-server/repochecker/file-inspector
- go build -buildvcs=false -buildmode=plugin -o deploy/repochecker-grammalecte-rules-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}.so srs.epita.fr/fic-server/repochecker/grammalecte
- go build -buildvcs=false -buildmode=plugin -o deploy/repochecker-pcap-inspector-rules-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}.so srs.epita.fr/fic-server/repochecker/pcap-inspector
- go build -buildvcs=false -buildmode=plugin -o deploy/repochecker-videos-rules-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}.so srs.epita.fr/fic-server/repochecker/videos
- grep "const version" repochecker/update.go | sed -r 's/^.*=\s*(\S.*)$/\1/' > deploy/repochecker.version
when:
branch:
exclude:
- master
- name: build qa
image: golang:alpine
commands:
- go build -buildvcs=false -o deploy/qa-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/qa
environment:
CGO_ENABLED: 0
when:
branch:
exclude:
- master
- name: docker admin
image: plugins/docker
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: nemunaire/fic-admin
auto_tag: true
auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
dockerfile: Dockerfile-admin
when:
branch:
- master
- name: docker checker
image: plugins/docker
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: nemunaire/fic-checker
auto_tag: true
auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
dockerfile: Dockerfile-checker
when:
branch:
- master
- name: docker evdist
image: plugins/docker
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: nemunaire/fic-evdist
auto_tag: true
auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
dockerfile: Dockerfile-evdist
when:
branch:
- master
- name: docker generator
image: plugins/docker
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: nemunaire/fic-generator
auto_tag: true
auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
dockerfile: Dockerfile-generator
when:
branch:
- master
- name: docker receiver
image: plugins/docker
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: nemunaire/fic-receiver
auto_tag: true
auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
dockerfile: Dockerfile-receiver
when:
branch:
- master
- name: docker frontend nginx
image: plugins/docker
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: nemunaire/fic-nginx
auto_tag: true
auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
dockerfile: Dockerfile-nginx
when:
branch:
- master
- name: docker frontend ui
image: plugins/docker
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: nemunaire/fic-frontend-ui
auto_tag: true
auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
dockerfile: Dockerfile-frontend-ui
when:
branch:
- master
- name: docker dashboard
image: plugins/docker
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: nemunaire/fic-dashboard
auto_tag: true
auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
dockerfile: Dockerfile-dashboard
when:
branch:
- master
- name: docker qa
image: plugins/docker
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: nemunaire/fic-qa
auto_tag: true
auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
dockerfile: Dockerfile-qa
when:
branch:
- master
- name: docker repochecker
image: plugins/docker
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: nemunaire/fic-repochecker
auto_tag: true
auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
dockerfile: Dockerfile-repochecker
when:
branch:
- master
- name: docker remote-scores-sync-zqds
image: plugins/docker
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: nemunaire/fic-remote-scores-sync-zqds
auto_tag: true
auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
dockerfile: Dockerfile-remote-scores-sync-zqds
when:
branch:
- master
- name: docker remote-challenge-sync-airbus
image: plugins/docker
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: nemunaire/fic-remote-challenge-sync-airbus
auto_tag: true
auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
dockerfile: Dockerfile-remote-challenge-sync-airbus
when:
branch:
- master
- name: docker fic-get-remote-files
failure: ignore
image: plugins/docker
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: nemunaire/fic-get-remote-files
auto_tag: true
auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
dockerfile: Dockerfile-get-remote-files
when:
branch:
- master
- name: docker fickit-deploy
image: plugins/docker
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: nemunaire/fickit-deploy
auto_tag: true
auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
dockerfile: Dockerfile-deploy
when:
branch:
- master
trigger:
event:
- cron
- push
- tag
---
kind: pipeline
type: docker
name: build-arm64
platform:
os: linux
arch: arm64
workspace:
base: /go
path: src/srs.epita.fr/fic-server
steps:
- name: get deps
image: golang:alpine
commands:
- apk --no-cache add git
- go get -d ./...
- mkdir deploy
- name: build admin
image: golang:alpine
commands:
- apk --no-cache add build-base
- go build -buildvcs=false -o deploy/admin-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/admin
environment:
CGO_ENABLED: 0
when:
branch:
exclude:
- master
- name: build checker
image: golang:alpine
commands:
- go build -buildvcs=false -o deploy/checker-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/checker
environment:
CGO_ENABLED: 0
when:
branch:
exclude:
- master
- name: build evdist
image: golang:alpine
commands:
- go build -buildvcs=false -o deploy/evdist-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/evdist
environment:
CGO_ENABLED: 0
when:
branch:
exclude:
- master
- name: build generator
image: golang:alpine
commands:
- go build -buildvcs=false -o deploy/generator-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/generator
environment:
CGO_ENABLED: 0
when:
branch:
exclude:
- master
- name: build receiver
image: golang:alpine
commands:
- go build -buildvcs=false -o deploy/receiver-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/receiver
environment:
CGO_ENABLED: 0
when:
branch:
exclude:
- master
- name: build frontend fic ui
image: node:23-alpine
commands:
- cd frontend/fic
- npm install --network-timeout=100000
- npm run build
when:
branch:
exclude:
- master
- name: build dashboard
image: golang:alpine
commands:
- go build -buildvcs=false -o deploy/dashboard-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/dashboard
environment:
CGO_ENABLED: 0
when:
branch:
exclude:
- master
- name: build repochecker
image: golang:alpine
commands:
- apk --no-cache add build-base
- go build -buildvcs=false --tags checkupdate -o deploy/repochecker-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/repochecker
environment:
CGO_ENABLED: 0
when:
branch:
exclude:
- master
- name: build repochecker for macOS
image: golang:alpine
commands:
- apk --no-cache add build-base
- go build -buildvcs=false --tags checkupdate -o deploy/repochecker-darwin-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/repochecker
environment:
CGO_ENABLED: 0
GOOS: darwin
GOARCH: arm64
when:
branch:
exclude:
- master
- name: build qa ui
image: node:23-alpine
commands:
- cd qa/ui
- npm install --network-timeout=100000
- npm run build
- tar chjf ../../deploy/htdocs-qa.tar.bz2 build
when:
branch:
exclude:
- master
- name: build qa
image: golang:alpine
commands:
- go build -buildvcs=false -o deploy/qa-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/qa
environment:
CGO_ENABLED: 0
when:
branch:
exclude:
- master
- name: docker admin
image: plugins/docker
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: nemunaire/fic-admin
auto_tag: true
auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
dockerfile: Dockerfile-admin
when:
branch:
- master
- name: docker checker
image: plugins/docker
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: nemunaire/fic-checker
auto_tag: true
auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
dockerfile: Dockerfile-checker
when:
branch:
- master
- name: docker evdist
image: plugins/docker
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: nemunaire/fic-evdist
auto_tag: true
auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
dockerfile: Dockerfile-evdist
when:
branch:
- master
- name: docker generator
image: plugins/docker
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: nemunaire/fic-generator
auto_tag: true
auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
dockerfile: Dockerfile-generator
when:
branch:
- master
- name: docker fic-get-remote-files
failure: ignore
image: plugins/docker
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: nemunaire/fic-get-remote-files
auto_tag: true
auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
dockerfile: Dockerfile-get-remote-files
when:
branch:
- master
- name: docker receiver
image: plugins/docker
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: nemunaire/fic-receiver
auto_tag: true
auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
dockerfile: Dockerfile-receiver
when:
branch:
- master
- name: docker frontend nginx
image: plugins/docker
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: nemunaire/fic-nginx
auto_tag: true
auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
dockerfile: Dockerfile-nginx
when:
branch:
- master
- name: docker dashboard
image: plugins/docker
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: nemunaire/fic-dashboard
auto_tag: true
auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
dockerfile: Dockerfile-dashboard
when:
branch:
- master
- name: docker qa
image: plugins/docker
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: nemunaire/fic-qa
auto_tag: true
auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
dockerfile: Dockerfile-qa
when:
branch:
- master
- name: docker repochecker
image: plugins/docker
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: nemunaire/fic-repochecker
auto_tag: true
auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
dockerfile: Dockerfile-repochecker
when:
branch:
- master
trigger:
event:
- cron
- push
- tag
---
kind: pipeline
name: docker-manifest
steps:
- name: publish admin
image: plugins/manifest
settings:
auto_tag: true
ignore_missing: true
spec: .drone-manifest-fic-admin.yml
username:
from_secret: docker_username
password:
from_secret: docker_password
- name: publish checker
image: plugins/manifest
settings:
auto_tag: true
ignore_missing: true
spec: .drone-manifest-fic-checker.yml
username:
from_secret: docker_username
password:
from_secret: docker_password
- name: publish evdist
image: plugins/manifest
settings:
auto_tag: true
ignore_missing: true
spec: .drone-manifest-fic-evdist.yml
username:
from_secret: docker_username
password:
from_secret: docker_password
- name: publish generator
image: plugins/manifest
settings:
auto_tag: true
ignore_missing: true
spec: .drone-manifest-fic-generator.yml
username:
from_secret: docker_username
password:
from_secret: docker_password
- name: publish receiver
image: plugins/manifest
settings:
auto_tag: true
ignore_missing: true
spec: .drone-manifest-fic-receiver.yml
username:
from_secret: docker_username
password:
from_secret: docker_password
- name: publish frontend nginx
image: plugins/manifest
settings:
auto_tag: true
ignore_missing: true
spec: .drone-manifest-fic-nginx.yml
username:
from_secret: docker_username
password:
from_secret: docker_password
- name: publish frontend ui
image: plugins/manifest
settings:
auto_tag: true
ignore_missing: true
spec: .drone-manifest-fic-frontend-ui.yml
username:
from_secret: docker_username
password:
from_secret: docker_password
- name: publish dashboard
image: plugins/manifest
settings:
auto_tag: true
ignore_missing: true
spec: .drone-manifest-fic-dashboard.yml
username:
from_secret: docker_username
password:
from_secret: docker_password
- name: publish repochecker
image: plugins/manifest
settings:
auto_tag: true
ignore_missing: true
spec: .drone-manifest-fic-repochecker.yml
username:
from_secret: docker_username
password:
from_secret: docker_password
- name: publish qa
image: plugins/manifest
settings:
auto_tag: true
ignore_missing: true
spec: .drone-manifest-fic-qa.yml
username:
from_secret: docker_username
password:
from_secret: docker_password
- name: docker fic-get-remote-files
failure: ignore
image: plugins/docker
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: nemunaire/fic-get-remote-files
auto_tag: true
auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
dockerfile: Dockerfile-get-remote-files
when:
branch:
- master
- name: publish fickit-deploy
image: plugins/manifest
settings:
auto_tag: true
ignore_missing: true
spec: .drone-manifest-fickit-deploy.yml
username:
from_secret: docker_username
password:
from_secret: docker_password
trigger:
event:
- push
- tag
depends_on:
- build-amd64
- build-arm64

43
.gitignore vendored Normal file
View file

@ -0,0 +1,43 @@
vendor/
DASHBOARD/
FILES/
PKI/
REMOTE/
SETTINGS/
SETTINGSDIST/
TEAMS/
submissions/
admin/sync/README.html
fickit-boot-cmdline
fickit-boot-initrd.img
fickit-boot-kernel
fickit-backend-cmdline
fickit-backend-initrd.img
fickit-backend-squashfs.img
fickit-backend-kernel
fickit-backend-state
fickit-frontend-cmdline
fickit-frontend-initrd.img
fickit-frontend-squashfs.img
fickit-frontend-kernel
fickit-frontend-state
fickit-prepare-bios.img
fickit-prepare-cmdline
fickit-prepare-initrd.img
fickit-prepare-kernel
fickit-prepare-state
fickit-update-cmdline
fickit-update-initrd.img
fickit-update-kernel
fickit-update-squashfs.img
result
started
# Standalone binaries
admin/get-remote-files/get-remote-files
fic-admin
fic-backend
fic-dashboard
fic-frontend
fic-qa
fic-repochecker

122
.gitlab-ci.yml Normal file
View file

@ -0,0 +1,122 @@
---
stages:
- deps
- build
- fickit
- sast
- qa
- image
- container_scanning
cache:
paths:
- .go/pkg/mod/
- qa/ui/node_modules/
- frontend/ui/node_modules/
include:
- '.gitlab-ci/build.yml'
- '.gitlab-ci/image.yml'
- template: SAST.gitlab-ci.yml
- template: Security/Dependency-Scanning.gitlab-ci.yml
- template: Security/Secret-Detection.gitlab-ci.yml
- template: Security/Container-Scanning.gitlab-ci.yml
.scanners-matrix:
parallel:
matrix:
- IMAGE_NAME: [checker, admin, evdist, frontend-ui, nginx, dashboard, repochecker, qa, receiver, generator, remote-challenge-sync-airbus]
container_scanning:
stage: container_scanning
extends:
- .scanners-matrix
variables:
DOCKER_SERVICE: localhost
DOCKERFILE_PATH: Dockerfile-${IMAGE_NAME}
CI_APPLICATION_REPOSITORY: ${CI_REGISTRY_IMAGE}/${CI_COMMIT_REF_SLUG}/${IMAGE_NAME}
CI_APPLICATION_TAG: latest
GIT_STRATEGY: fetch
before_script:
- 'echo "Scanning: ${IMAGE_NAME}"'
rules:
- if: '$CI_COMMIT_BRANCH == "master"'
sast:
stage: sast
interruptible: true
needs: []
before_script:
- rm -rf .go/
secret_detection:
stage: sast
interruptible: true
needs: []
dependency_scanning:
stage: qa
interruptible: true
needs: []
get-deps:
stage: deps
image: golang:1-alpine
before_script:
- export GOPATH="$CI_PROJECT_DIR/.go"
- mkdir -p .go
script:
- apk --no-cache add git
- go get -v -d ./...
vet:
stage: sast
needs: ["build-qa-ui"]
dependencies:
- build-qa-ui
image: golang:1-alpine
before_script:
- export GOPATH="$CI_PROJECT_DIR/.go"
- mkdir -p .go
script:
- apk --no-cache add build-base
- go vet -v -buildvcs=false -tags gitgo ./...
- go vet -v -buildvcs=false ./...
fickit:
stage: fickit
interruptible: true
needs: ["build-admin","build-checker","build-dashboard","build-evdist","build-generator","build-qa","build-receiver","build-repochecker"]
image: nemunaire/linuxkit
tags: ['docker']
before_script:
- mkdir -p ~/.docker
- echo "{\"auths\":{\"${CI_REGISTRY}\":{\"username\":\"${CI_REGISTRY_USER}\",\"password\":\"${CI_REGISTRY_PASSWORD}\"}}}" > ~/.docker/config.json
script:
- dockerd & sleep 5
- linuxkit pkg push -force -org "${CI_REGISTRY_IMAGE}/${CI_COMMIT_REF_SLUG}" fickit-pkg/boot/
- linuxkit pkg push -force -org "${CI_REGISTRY_IMAGE}/${CI_COMMIT_REF_SLUG}" fickit-pkg/kexec/
- linuxkit pkg push -force -org "${CI_REGISTRY_IMAGE}/${CI_COMMIT_REF_SLUG}" fickit-pkg/mariadb-client/
- linuxkit pkg push -force -org "${CI_REGISTRY_IMAGE}/${CI_COMMIT_REF_SLUG}" fickit-pkg/mdadm/
- linuxkit pkg push -force -org "${CI_REGISTRY_IMAGE}/${CI_COMMIT_REF_SLUG}" fickit-pkg/rsync/
- linuxkit pkg push -force -org "${CI_REGISTRY_IMAGE}/${CI_COMMIT_REF_SLUG}" fickit-pkg/syslinux/
- linuxkit pkg push -force -org "${CI_REGISTRY_IMAGE}/${CI_COMMIT_REF_SLUG}" fickit-pkg/unbound/
- sed -i "s@nemunaire/fic-@${CI_REGISTRY_IMAGE}/master/@;s@nemunaire/@${CI_REGISTRY_IMAGE}/${CI_COMMIT_REF_SLUG}/@" fickit-backend.yml fickit-boot.yml fickit-frontend.yml fickit-prepare.yml fickit-update.yml
- linuxkit build -format kernel+squashfs fickit-backend.yml
- linuxkit build -format kernel+squashfs fickit-frontend.yml
- linuxkit build -format kernel+initrd fickit-boot.yml
- linuxkit build -format kernel+initrd fickit-prepare.yml
- linuxkit build -format kernel+initrd fickit-update.yml
artifacts:
expire_in: 8 hours
paths:
- fickit-backend-squashfs.img
- fickit-frontend-squashfs.img
- fickit-boot-kernel
- fickit-boot-initrd.img
- fickit-prepare-initrd.img
- fickit-update-initrd.img

93
.gitlab-ci/build.yml Normal file
View file

@ -0,0 +1,93 @@
---
.build:
stage: build
image: golang:1-alpine
before_script:
- export GOPATH="$CI_PROJECT_DIR/.go"
- mkdir -p .go
variables:
CGO_ENABLED: 0
build-qa-ui:
stage: build
image: node:21-alpine
before_script:
script:
- cd qa/ui
- npm install --network-timeout=100000
- npm run build
artifacts:
paths:
- qa/ui/build/
when: on_success
build-checker:
extends:
- .build
script:
- go build -v -buildvcs=false -o deploy/checker srs.epita.fr/fic-server/checker
build-generator:
extends:
- .build
script:
- go build -v -buildvcs=false -o deploy/generator srs.epita.fr/fic-server/generator
build-receiver:
extends:
- .build
script:
- go build -v -buildvcs=false -o deploy/receiver srs.epita.fr/fic-server/receiver
build-admin:
extends:
- .build
script:
- go build -v -buildvcs=false -tags gitgo -o deploy/admin-gitgo srs.epita.fr/fic-server/admin
- go build -v -buildvcs=false -o deploy/admin srs.epita.fr/fic-server/admin
build-evdist:
extends:
- .build
script:
- go build -v -buildvcs=false -o deploy/evdist srs.epita.fr/fic-server/evdist
build-frontend-ui:
stage: build
image: node:21-alpine
before_script:
script:
- cd frontend/fic
- npm install --network-timeout=100000
- npm run build
build-dashboard:
extends:
- .build
script:
- go build -v -buildvcs=false -o deploy/dashboard srs.epita.fr/fic-server/dashboard
build-repochecker:
extends:
- .build
variables:
CGO_ENABLED: 1
script:
- apk --no-cache add build-base
- go build -buildvcs=false --tags checkupdate -v -o deploy/repochecker srs.epita.fr/fic-server/repochecker
- go build -buildvcs=false -buildmode=plugin -v -o deploy/repochecker-epita-rules.so srs.epita.fr/fic-server/repochecker/epita
- go build -buildvcs=false -buildmode=plugin -v -o deploy/repochecker-file-inspector-rules.so srs.epita.fr/fic-server/repochecker/file-inspector
- go build -buildvcs=false -buildmode=plugin -v -o deploy/repochecker-grammalecte-rules.so srs.epita.fr/fic-server/repochecker/grammalecte
- go build -buildvcs=false -buildmode=plugin -v -o deploy/repochecker-pcap-inspector-rules.so srs.epita.fr/fic-server/repochecker/pcap-inspector
- go build -buildvcs=false -buildmode=plugin -v -o deploy/repochecker-videos-rules.so srs.epita.fr/fic-server/repochecker/videos
- grep "const version" repochecker/update.go | sed -r 's/^.*=\s*(\S.*)$/\1/' > deploy/repochecker.version
build-qa:
extends:
- .build
needs: ["build-qa-ui"]
dependencies:
- build-qa-ui
script:
- go build -v -buildvcs=false -o deploy/qa srs.epita.fr/fic-server/qa

99
.gitlab-ci/image.yml Normal file
View file

@ -0,0 +1,99 @@
---
.push:
stage: image
interruptible: true
needs: []
image:
name: gcr.io/kaniko-project/executor:v1.9.0-debug
entrypoint: [""]
before_script:
- mkdir -p /kaniko/.docker
- echo "{\"auths\":{\"${CI_REGISTRY}\":{\"username\":\"${CI_REGISTRY_USER}\",\"password\":\"${CI_REGISTRY_PASSWORD}\"}}}" > /kaniko/.docker/config.json
script:
- |
/kaniko/executor \
--context . \
--dockerfile "${DOCKERFILE}" \
--destination "${CI_REGISTRY_IMAGE}/${CI_COMMIT_REF_SLUG}/${CI_JOB_NAME}:${CI_COMMIT_SHA}" \
--destination "${CI_REGISTRY_IMAGE}/${CI_COMMIT_REF_SLUG}/${CI_JOB_NAME}:latest"
only:
- master
checker:
extends:
- .push
variables:
DOCKERFILE: Dockerfile-checker
receiver:
extends:
- .push
variables:
DOCKERFILE: Dockerfile-receiver
generator:
extends:
- .push
variables:
DOCKERFILE: Dockerfile-generator
admin:
extends:
- .push
variables:
DOCKERFILE: Dockerfile-admin
fickit-deploy:
extends:
- .push
variables:
DOCKERFILE: Dockerfile-deploy
get-remote-files:
extends:
- .push
variables:
DOCKERFILE: Dockerfile-get-remote-files
evdist:
extends:
- .push
variables:
DOCKERFILE: Dockerfile-evdist
frontend-ui:
extends:
- .push
variables:
DOCKERFILE: Dockerfile-frontend-ui
nginx:
extends:
- .push
variables:
DOCKERFILE: Dockerfile-nginx
dashboard:
extends:
- .push
variables:
DOCKERFILE: Dockerfile-dashboard
repochecker:
extends:
- .push
variables:
DOCKERFILE: Dockerfile-repochecker
qa:
extends:
- .push
variables:
DOCKERFILE: Dockerfile-qa
remote-challenge-sync-airbus:
extends:
- .push
variables:
DOCKERFILE: Dockerfile-remote-challenge-sync-airbus

42
Dockerfile-admin Normal file
View file

@ -0,0 +1,42 @@
FROM golang:1-alpine AS gobuild
RUN apk add --no-cache git
WORKDIR /go/src/srs.epita.fr/fic-server/
RUN apk add --no-cache binutils-gold build-base
COPY go.mod go.sum ./
COPY settings settings/
COPY libfic ./libfic/
COPY admin ./admin/
COPY repochecker ./repochecker/
RUN go get -d -v ./admin && \
go build -v -o admin/admin ./admin && \
go build -v -buildmode=plugin -o repochecker/epita-rules.so ./repochecker/epita && \
go build -v -buildmode=plugin -o repochecker/file-inspector.so ./repochecker/file-inspector && \
go build -v -buildmode=plugin -o repochecker/grammalecte-rules.so ./repochecker/grammalecte && \
go build -v -buildmode=plugin -o repochecker/videos-rules.so ./repochecker/videos
FROM alpine:3.21
RUN apk add --no-cache \
ca-certificates \
git \
git-lfs \
openssh-client-default \
openssl
EXPOSE 8081
WORKDIR /srv
ENTRYPOINT ["/srv/admin", "-bind=:8081", "-baseurl=/admin/"]
COPY --from=gobuild /go/src/srs.epita.fr/fic-server/admin/admin /srv/admin
COPY --from=gobuild /go/src/srs.epita.fr/fic-server/repochecker/epita-rules.so /srv/epita-rules.so
COPY --from=gobuild /go/src/srs.epita.fr/fic-server/repochecker/file-inspector.so /usr/lib/file-inspector.so
COPY --from=gobuild /go/src/srs.epita.fr/fic-server/repochecker/grammalecte-rules.so /usr/lib/grammalecte-rules.so
COPY --from=gobuild /go/src/srs.epita.fr/fic-server/repochecker/videos-rules.so /usr/lib/videos-rules.so

22
Dockerfile-checker Normal file
View file

@ -0,0 +1,22 @@
FROM golang:1-alpine AS gobuild
RUN apk add --no-cache git
WORKDIR /go/src/srs.epita.fr/fic-server/
COPY go.mod go.sum ./
COPY settings settings/
COPY libfic ./libfic/
COPY checker ./checker/
RUN go get -d -v ./checker && \
go build -v -buildvcs=false -o checker/checker ./checker
FROM alpine:3.21
WORKDIR /srv
ENTRYPOINT ["/srv/checker"]
COPY --from=gobuild /go/src/srs.epita.fr/fic-server/checker/checker /srv/checker

32
Dockerfile-dashboard Normal file
View file

@ -0,0 +1,32 @@
FROM golang:1-alpine AS gobuild
RUN apk add --no-cache git
WORKDIR /go/src/srs.epita.fr/fic-server/
COPY go.mod go.sum ./
COPY settings settings/
COPY libfic ./libfic/
COPY dashboard ./dashboard/
RUN go get -d -v ./dashboard && \
go build -v -buildvcs=false -o dashboard/dashboard ./dashboard
FROM alpine:3.21
EXPOSE 8082
WORKDIR /srv
ENTRYPOINT ["/srv/dashboard", "--bind=:8082"]
VOLUME /srv/htdocs-dashboard/
COPY --from=gobuild /go/src/srs.epita.fr/fic-server/dashboard/dashboard /srv/dashboard
COPY dashboard/static/index.html /srv/htdocs-dashboard/
COPY admin/static/css/bootstrap.min.css dashboard/static/css/fic.css admin/static/css/glyphicon.css /srv/htdocs-dashboard/css/
COPY admin/static/fonts /srv/htdocs-dashboard/fonts
COPY dashboard/static/img/srs.png /srv/htdocs-dashboard/img/
COPY dashboard/static/js/dashboard.js admin/static/js/angular.min.js dashboard/static/js/angular-animate.min.js admin/static/js/angular-route.min.js admin/static/js/angular-sanitize.min.js admin/static/js/bootstrap.min.js admin/static/js/common.js admin/static/js/d3.v3.min.js admin/static/js/jquery.min.js /srv/htdocs-dashboard/js/
COPY admin/static/js/i18n/* /srv/htdocs-dashboard/js/i18n/

24
Dockerfile-deploy Normal file
View file

@ -0,0 +1,24 @@
FROM alpine:3.21
EXPOSE 67/udp
EXPOSE 69/udp
EXPOSE 80/tcp
ENTRYPOINT ["/usr/sbin/initial-config.sh"]
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]
WORKDIR /srv/s
RUN apk add --no-cache \
busybox-extras \
supervisor \
syslinux \
tftp-hpa
RUN touch /var/lib/udhcpd/udhcpd.leases && \
mv /usr/share/syslinux/* /srv
COPY configs/deploy-initial-config.sh /usr/sbin/initial-config.sh
COPY configs/deploy-supervisord.conf /etc/supervisord.conf
COPY configs/udhcpd-sample.conf /etc/udhcpd.conf
COPY configs/pxelinux.cfg /srv/pxelinux.cfg/default

21
Dockerfile-evdist Normal file
View file

@ -0,0 +1,21 @@
FROM golang:1-alpine AS gobuild
RUN apk add --no-cache git
WORKDIR /go/src/srs.epita.fr/fic-server/
COPY go.mod go.sum ./
COPY settings settings/
COPY evdist ./evdist/
RUN go get -d -v ./evdist && \
go build -v -buildvcs=false -o evdist/evdist ./evdist
FROM alpine:3.21
WORKDIR /srv
ENTRYPOINT ["/srv/evdist"]
COPY --from=gobuild /go/src/srs.epita.fr/fic-server/evdist/evdist /srv/evdist

13
Dockerfile-frontend-ui Normal file
View file

@ -0,0 +1,13 @@
FROM node:23-alpine AS nodebuild
WORKDIR /ui
COPY frontend/fic/ .
RUN npm install --network-timeout=100000 && \
npm run build
FROM scratch
COPY --from=nodebuild /ui/build/ /www/htdocs-frontend

22
Dockerfile-generator Normal file
View file

@ -0,0 +1,22 @@
FROM golang:1-alpine AS gobuild
RUN apk add --no-cache git
WORKDIR /go/src/srs.epita.fr/fic-server/
COPY go.mod go.sum ./
COPY settings settings/
COPY libfic ./libfic/
COPY generator ./generator/
RUN go get -d -v ./generator && \
go build -v -buildvcs=false -o generator/generator ./generator
FROM alpine:3.21
WORKDIR /srv
ENTRYPOINT ["/srv/generator"]
COPY --from=gobuild /go/src/srs.epita.fr/fic-server/generator/generator /srv/generator

View file

@ -0,0 +1,27 @@
FROM golang:1-alpine AS gobuild
RUN apk add --no-cache git
WORKDIR /go/src/srs.epita.fr/fic-server/
RUN apk add --no-cache build-base
COPY go.mod go.sum ./
COPY settings settings/
COPY libfic ./libfic/
COPY admin ./admin/
RUN go get -d -v ./admin && \
go build -v -o get-remote-files ./admin/get-remote-files
FROM alpine:3.21
RUN apk add --no-cache \
ca-certificates
WORKDIR /srv
ENTRYPOINT ["/srv/get-remote-files", "/mnt/fic/"]
COPY --from=gobuild /go/src/srs.epita.fr/fic-server/get-remote-files /srv/get-remote-files

32
Dockerfile-nginx Normal file
View file

@ -0,0 +1,32 @@
FROM node:23-alpine AS nodebuild
WORKDIR /ui
COPY frontend/fic/ .
RUN npm install --network-timeout=100000 && \
npm run build
FROM nginx:stable-alpine-slim
ENV FIC_BASEURL /
ENV HOST_RECEIVER receiver:8080
ENV HOST_ADMIN admin:8081
ENV HOST_DASHBOARD dashboard:8082
ENV HOST_QA qa:8083
ENV PATH_FILES /srv/FILES
ENV PATH_STARTINGBLOCK /srv/STARTINGBLOCK
ENV PATH_STATIC /srv/htdocs-frontend
ENV PATH_SETTINGS /srv/SETTINGSDIST
ENV PATH_TEAMS /srv/TEAMS
EXPOSE 80
COPY configs/nginx-chbase.sh /docker-entrypoint.d/40-update-baseurl.sh
COPY configs/nginx/get-team/upstream.conf /etc/nginx/fic-get-team.conf
COPY configs/nginx/auth/none.conf /etc/nginx/fic-auth.conf
COPY configs/nginx/base/docker.conf /etc/nginx/templates/default.conf.template
COPY --from=nodebuild /ui/build/ /srv/htdocs-frontend

38
Dockerfile-qa Normal file
View file

@ -0,0 +1,38 @@
FROM node:23-alpine AS nodebuild
WORKDIR /ui
COPY qa/ui/ .
RUN npm install --network-timeout=100000 && \
npm run build
FROM golang:1-alpine AS gobuild
RUN apk add --no-cache git
WORKDIR /go/src/srs.epita.fr/fic-server/
COPY go.mod go.sum ./
COPY settings settings/
COPY libfic ./libfic/
COPY --from=nodebuild /ui ./qa/ui
COPY qa ./qa/
COPY admin ./admin/
RUN go get -d -v ./qa && \
go build -v -buildvcs=false -o qa/qa ./qa
FROM alpine:3.21
EXPOSE 8083
WORKDIR /srv
ENTRYPOINT ["/srv/qa", "--bind=:8083"]
VOLUME /srv/htdocs-qa/
COPY --from=gobuild /go/src/srs.epita.fr/fic-server/qa/qa /srv/qa

27
Dockerfile-receiver Normal file
View file

@ -0,0 +1,27 @@
FROM golang:1-alpine AS gobuild
RUN apk add --no-cache git
WORKDIR /go/src/srs.epita.fr/fic-server/
COPY go.mod go.sum ./
COPY settings settings/
COPY libfic ./libfic/
COPY receiver ./receiver/
RUN go get -d -v ./receiver && \
go build -v -buildvcs=false -o ./receiver/receiver ./receiver
FROM alpine:3.21
EXPOSE 8080
WORKDIR /srv
ENTRYPOINT ["/usr/sbin/entrypoint.sh"]
CMD ["--bind=:8080"]
COPY entrypoint-receiver.sh /usr/sbin/entrypoint.sh
COPY --from=gobuild /go/src/srs.epita.fr/fic-server/receiver/receiver /srv/receiver

View file

@ -0,0 +1,24 @@
FROM golang:1-alpine AS gobuild
RUN apk add --no-cache git
WORKDIR /go/src/srs.epita.fr/fic-server/
COPY go.mod go.sum ./
COPY libfic ./libfic/
COPY settings ./settings/
COPY remote/challenge-sync-airbus ./remote/challenge-sync-airbus/
RUN go get -d -v ./remote/challenge-sync-airbus && \
go build -v -buildvcs=false -o ./challenge-sync-airbus ./remote/challenge-sync-airbus
FROM alpine:3.21
RUN apk add --no-cache openssl ca-certificates
WORKDIR /srv
ENTRYPOINT ["/srv/challenge-sync-airbus"]
COPY --from=gobuild /go/src/srs.epita.fr/fic-server/challenge-sync-airbus /srv/challenge-sync-airbus

View file

@ -0,0 +1,24 @@
FROM golang:1-alpine AS gobuild
RUN apk add --no-cache git
WORKDIR /go/src/srs.epita.fr/fic-server/
COPY go.mod go.sum ./
COPY libfic ./libfic/
COPY settings ./settings/
COPY remote/scores-sync-zqds ./remote/scores-sync-zqds/
RUN go get -d -v ./remote/scores-sync-zqds && \
go build -v -buildvcs=false -o ./scores-sync-zqds ./remote/scores-sync-zqds
FROM alpine:3.21
RUN apk add --no-cache openssl ca-certificates
WORKDIR /srv
ENTRYPOINT ["/srv/scores-sync-zqds"]
COPY --from=gobuild /go/src/srs.epita.fr/fic-server/scores-sync-zqds /srv/scores-sync-zqds

42
Dockerfile-repochecker Normal file
View file

@ -0,0 +1,42 @@
FROM golang:1-alpine AS gobuild
RUN apk add --no-cache git
WORKDIR /go/src/srs.epita.fr/fic-server/
RUN apk add --no-cache binutils-gold build-base
COPY go.mod go.sum ./
COPY settings settings/
COPY libfic ./libfic/
COPY admin ./admin/
COPY repochecker ./repochecker/
RUN go get -d -v ./repochecker && \
go build -v -o repochecker/repochecker ./repochecker && \
go build -v -buildmode=plugin -o repochecker/epita-rules.so ./repochecker/epita && \
go build -v -buildmode=plugin -o repochecker/file-inspector.so ./repochecker/file-inspector && \
go build -v -buildmode=plugin -o repochecker/grammalecte-rules.so ./repochecker/grammalecte && \
go build -v -buildmode=plugin -o repochecker/pcap-inspector.so ./repochecker/pcap-inspector && \
go build -v -buildmode=plugin -o repochecker/videos-rules.so ./repochecker/videos
ENV GRAMMALECTE_VERSION 2.1.1
ADD https://web.archive.org/web/20240926154729if_/https://grammalecte.net/zip/Grammalecte-fr-v$GRAMMALECTE_VERSION.zip /srv/grammalecte.zip
RUN mkdir /srv/grammalecte && cd /srv/grammalecte && unzip /srv/grammalecte.zip && sed -i 's/if sys.version_info.major < (3, 7):/if False:/' /srv/grammalecte/grammalecte-server.py
FROM alpine:3.19
ENTRYPOINT ["/usr/bin/repochecker", "--rules-plugins=/usr/lib/epita-rules.so", "--rules-plugins=/usr/lib/file-inspector.so", "--rules-plugins=/usr/lib/grammalecte-rules.so", "--rules-plugins=/usr/lib/pcap-inspector.so", "--rules-plugins=/usr/lib/videos-rules.so"]
RUN apk add --no-cache git python3 ffmpeg
COPY --from=gobuild /srv/grammalecte /srv/grammalecte
COPY --from=gobuild /go/src/srs.epita.fr/fic-server/repochecker/repochecker /usr/bin/repochecker
COPY --from=gobuild /go/src/srs.epita.fr/fic-server/repochecker/epita-rules.so /usr/lib/epita-rules.so
COPY --from=gobuild /go/src/srs.epita.fr/fic-server/repochecker/file-inspector.so /usr/lib/file-inspector.so
COPY --from=gobuild /go/src/srs.epita.fr/fic-server/repochecker/grammalecte-rules.so /usr/lib/grammalecte-rules.so
COPY --from=gobuild /go/src/srs.epita.fr/fic-server/repochecker/pcap-inspector.so /usr/lib/pcap-inspector.so
COPY --from=gobuild /go/src/srs.epita.fr/fic-server/repochecker/videos-rules.so /usr/lib/videos-rules.so

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2016-2018 Pierre-Olivier Mercier <nemunaire@nemunai.re>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

238
README.md Normal file
View file

@ -0,0 +1,238 @@
FIC Forensic CTF Platform
=========================
This is a CTF server for distributing and validating challenges. It is design
to be robust, so it uses some uncommon technics like client certificate for
authentication, lots of state of the art cryptographic methods and aims to be
deployed in a DMZ network architecture.
## Features
- **Collaborative Challenge Design and Review:** Facilitates large team collaboration for challenge creation and review.
- **Versatile Flag Formats:** Supports flags as strings, numbers, multiple-choice questions, unique-choice questions, selects, multiline inputs, and strings with capture regexp.
- **Engaging Challenge Interface:** A visually appealing interface that incorporates images to illustrate exercises.
- **Public Dashboard:** Allow spectators to follow the competition alongside players.
- **Archival Mode:** Preserve past challenges and data in a static form, with no code. Your archive can lied on a S3 bucket.
- **Export Capabilities:** Export challenges to other CTF platforms.
- **Security-Focused:** Designed with security as a top priority. Each service aims to be isolated with right restrictions. Answers are not stored in the database, ...
- **Choose your Authentication:** Authentication is not part of this project, integrate your own authentication methods.
- **Extensible:** Easily extend and customize the platform. The main codebase in Golang is highly documented, each frontend part can be recreated in another language with ease.
- **Comprehensive Settings:** A wide range of settings for challenge customization. You can have first blood or not, dynamic exercice gain, evenemential bonus, ...
- **Git Integration:** Seamless verification and integration with Git.
- **Infrastructure as Code (IaC):** Ensure read-only and reproducible infrastructure.
- **Last-Minute Checks:** Ensure your challenge is ready with a comprehensive set of checks that can be performed anytime, verifying that downloadable files are as expected by the challenge creators.
- **Lightweight:** Optimized for minimal resource consumption, supporting features like serving gzipped files directly to browsers without CPU usage.
- **Scalable:** Designed to handle large-scale competitions with multiple receivers and frontend servers, smoothly queuing activity peaks on the backend.
- **Offline Capability:** Run your challenges offline.
- **Integrated Exercise Issue Ticketing System:** Manage and track issues related to exercises during the competition directly with teams. During designing phase, this transform in a complete dedicated QA platform.
- **Detailed Statistics:** Provide administrators with insights into exercise preferences and complexity.
- **Change Planning:** Schedule events in advance, such as new exercise availability or ephemeral bonuses, with second-by-second precision.
- **Frontend Time Synchronization:** Ensure accurate remaining time and event synchronization between servers and players.
## Overview
This is a [monorepo](https://danluu.com/monorepo/), containing several
micro-services :
- `admin` is the web interface and API used to control the challenge
and doing synchronization.
- `checker` is an inotify reacting service that handles submissions
checking.
- `dashboard` is a public interface to explain and follow the
conquest, aims to animate the challenge for visitors.
- `evdist` is an inotify reacting service that handles settings
changes during the challenge (eg. a 30 minutes event where hints are
free, ...).
- `generator` takes care of global and team's files generation.
- `qa` is an interface dedicated to challenge development, it stores
reports to be treated by challenges creators.
- `receiver` is only responsible for receiving submissions. It is the
only dynamic part accessibe to players, so it's codebase is reduce
to the minimum. It does not parse or try to understand players
submissions, it just write it down to a file in the file
system. Parsing and treatment is made by the `checker`.
- `remote/challenge-sync-airbus` is an inotify reacting service that
allows us to synchronize scores and exercice validations with the
Airbus scoring platform.
- `remote/scores-sync-zqds` is an inotify reacting service that allows
us to synchronize scores with the ZQDS scoring platform.
- `repochecker` is a side project to check offline for synchronization
issues.
Here is how thoses services speak to each others:
![Overview of the micro-services](doc/micro-services.png)
In the production setup, each micro-service runs in a dedicated
container, isolated from each other. Moreover, two physical machines
should be used:
- `phobos` communicates with players: displaying the web interface,
authenticate teams and players, storing contest files and handling
submissions retrieval without understanding them. It can't access
`deimos` so its job stops after writing requests on the filesystem.
- `deimos` is hidden from players, isolated from the network. It can
only access `phobos` via a restricted ssh connection, to retrieve
requests from `phobos` filesystem and pushing to it newly generated
static files.
Concretely, the L2 looks like this:
![Layer 2 connections](doc/l2.png)
So, the general filesystem is organized this way:
- `DASHBOARD` contains files structuring the content of the dashboard
screen(s).
- `FILES` stores the contest file to be downloaded by players. To be
accessible without authentication and to avoid bruteforce, each file
is placed into a directory with a hashed name (the original file
name is preserved). It's rsynced as is to `deimos`.
- `GENERATOR` contains a socket to allow other services to communicate
with the `generator`.
- `PKI` takes care of the PKI used for the client certiciate
authorization process, and more generaly, all authentication related
files (htpasswd, dexidp config, ...). Only the `shared` subdirectory
is shared with `deimos`, private key and teams P12 don't go out.
- `SETTINGS` stores the challenge config as wanted by admins. It's not
always the config in use: it uses can be delayed waiting for a
trigger.
- `SETTINGSDIST` is the challenge configuration in use. It is the one
shared with players.
- `startingblock` keep the `started` state of the challenge. This
helps `nginx` to know when it can start distributing exercices
related files.
- `TEAMS` stores the static files generated by the `generator`, there is
one subdirectory by team (id of the team), plus some files at the
root, which are common to all teams. There is also symlink pointing
to team directory, each symlink represent an authentication
association (certificate ID, OpenID username, htpasswd user, ...).
- `submissions` is the directory where the `receiver` writes
requests. It creates subdirectories at the name of the
authentication association, as seen in `TEAMS`, `checker` then
resolve the association regarding `TEAMS` directory. There is also a
special directory to handle team registration.
Here is a diagram showing how each micro-service uses directories it has access to (blue for read access, red for write access):
![Usage of directories by each micro-service](doc/directories.png)
Local developer setup
---------------------
### Using Docker
Use `docker-compose build`, then `docker-compose up` to launch the infrastructure.
After booting, you'll be able to reach the main interface at:
<http://localhost:8042/> and the admin one at: <http://localhost:8081/> (or at <http://localhost:8042/admin/>).
The dashboard is available at <http://localhost:8042/dashboard/> and the QA service at <http://localhost:8042/qa/>.
In this setup, there is no authentication. You are identfied [as a team](./configs/nginx/get-team/team-1.conf). On first use you'll need to register.
#### Import folder
##### Local import folder
The following changes is only required if your are trying to change the local import folder `~/fic` location.
Make the following changes inside this file `docker-compose.yml`:
23 volumes:
24 - - ~/fic:/mnt/fic:ro
24 + - <custom-path-to-import-folder>/fic:/mnt/fic:ro
##### Git import
A git repository can be used:
29 - command: --baseurl /admin/ -localimport /mnt/fic -localimportsymlink
29 + command: --baseurl /admin/ -localimport /mnt/fic -localimportsymlink -git-import-remote git@gitlab.cri.epita.fr:ing/majeures/srs/fic/2042/challenges.git
##### Owncloud import folder
If your are trying to use the folder available with the Owncloud service, make the following changes inside this file `docker-compose.yml`:
29 - command: --baseurl /admin/ -localimport /mnt/fic -localimportsymlink
29 + command: --baseurl /admin/ -clouddav=https://owncloud.srs.epita.fr/remote.php/webdav/FIC%202019/ -clouduser <login_x> -cloudpass '<passwd>'
### Manual builds
Running this project requires a web server (configuration is given for nginx),
a database (currently supporting only MySQL/MariaDB), a Go compiler for the
revision 1.18 at least and a `inotify`-aware system. You'll also need NodeJS to
compile some user interfaces.
1. Above all, you need to build Node projects:
cd frontend/fic; npm install && npm run build
cd qa/ui; npm install && npm run build
2. First, you'll need to retrieve the dependencies:
go mod vendor
2. Then, build the three Go projects:
go build -o fic-admin ./admin
go build -o fic-checker ./checker
go build -o fic-dashboard ./dashboard
go build -o fic-generator ./generator
go build -o fic-qa ./qa
go build -o fic-receiver ./receiver
go build -o fic-repochecker ./repochecker
...
3. Before launching anything, you need to create a database:
mysql -u root -p <<EOF
CREATE DATABASE fic;
CREATE USER fic@localhost IDENTIFIED BY 'fic';
GRANT ALL ON fic.* TO fic@localhost;
EOF
By default, expected credentials for development purpose is `fic`,
for both username, password and database name. If you want to use
other credentials, define the corresponding environment variable:
`MYSQL_HOST`, `MYSQL_USER`, `MYSQL_PASSWORD` and
`MYSQL_DATABASE`. Those variables are the one used by the `mysql`
docker image, so just link them together if you use containers.
4. Launch it!
./fic-admin &
After initializing the database, the server will listen on
<http://localhost:8081/>: this is the administration part.
./fic-generator &
This daemon generates static and team related files and then waits
another process to tell it to regenerate some files.
./fic-receiver &
This one exposes an API that gives time synchronization to clients and
handle submission reception (but without treating them).
./fic-checker &
This service waits for new submissions (expected in `submissions`
directory). It only watchs modifications on the file system, it has no web
interface.
./fic-dashboard &
This last server runs the public dashboard. It serves all file, without the
need of a webserver. It listens on port 8082 by default.
./fic-qa &
If you need it, this will launch a web interface on the port 8083 by
default, to perform quality control.
For the moment, a web server is mandatory to serve static files, look
at the samples given in the `configs/nginx` directory. You need to
pick one base configation flavor in the `configs/nginx/base`
directory, and associated with an authentication mechanism in
`configs/nginx/auth` (named the file `fic-auth.conf` in `/etc/nginx`),
and also pick the corresponding `configs/nginx/get-team` file, you
named `fic-get-team.conf`.

1
admin/.gitignore vendored
View file

@ -2,3 +2,4 @@ admin
fic.db
PKI/
FILES/
static/full_import_report.json

View file

@ -1,102 +0,0 @@
package main
import (
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"strings"
)
type DispatchFunction func([]string, []byte) (interface{}, error)
var apiRoutes = map[string]*(map[string]DispatchFunction){
"version": &ApiVersionRouting,
"ca": &ApiCARouting,
"events": &ApiEventsRouting,
"exercices": &ApiExercicesRouting,
"themes": &ApiThemesRouting,
"teams": &ApiTeamsRouting,
}
type apiRouting struct{}
func ApiHandler() http.Handler {
return apiRouting{}
}
func (a apiRouting) ServeHTTP(w http.ResponseWriter, r *http.Request) {
log.Printf("Handling %s request from %s: %s [%s]\n", r.Method, r.RemoteAddr, r.URL.Path, r.UserAgent())
// Extract URL arguments
var sURL = strings.Split(r.URL.Path, "/")[1:]
if len(sURL) > 1 && sURL[len(sURL)-1] == "" {
// Remove trailing /
sURL = sURL[:len(sURL)-1]
}
w.Header().Set("Content-Type", "application/json")
var ret interface{}
var err error = nil
// Read the body
if r.ContentLength < 0 || r.ContentLength > 6553600 {
http.Error(w, fmt.Sprintf("{errmsg:\"Request too large or request size unknown\"}", err), http.StatusRequestEntityTooLarge)
return
}
var body []byte
if r.ContentLength > 0 {
tmp := make([]byte, 1024)
for {
n, err := r.Body.Read(tmp)
for j := 0; j < n; j++ {
body = append(body, tmp[j])
}
if err != nil || n <= 0 {
break
}
}
}
// Route request
if len(sURL) > 0 {
if h, ok := apiRoutes[sURL[0]]; ok {
if f, ok := (*h)[r.Method]; ok {
ret, err = f(sURL[1:], body)
} else {
err = errors.New(fmt.Sprintf("Invalid action (%s) provided for %s.", r.Method, sURL[0]))
}
}
} else {
err = errors.New("No action provided.")
}
// Format response
resStatus := http.StatusOK
if err != nil {
ret = map[string]string{"errmsg": err.Error()}
resStatus = http.StatusBadRequest
log.Println(r.RemoteAddr, resStatus, err.Error())
}
if ret == nil {
ret = map[string]string{"errmsg": "Page not found"}
resStatus = http.StatusNotFound
}
if str, found := ret.(string); found {
w.WriteHeader(resStatus)
io.WriteString(w, str)
} else if bts, found := ret.([]byte); found {
w.WriteHeader(resStatus)
w.Write(bts)
} else if j, err := json.Marshal(ret); err != nil {
http.Error(w, fmt.Sprintf("{\"errmsg\":\"%q\"}", err), http.StatusInternalServerError)
} else {
w.WriteHeader(resStatus)
w.Write(j)
}
}

479
admin/api/certificate.go Normal file
View file

@ -0,0 +1,479 @@
package api
import (
"crypto/rand"
"crypto/sha1"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base32"
"encoding/base64"
"errors"
"fmt"
"io/ioutil"
"log"
"math"
"math/big"
"net/http"
"os"
"path"
"strconv"
"strings"
"time"
"srs.epita.fr/fic-server/admin/pki"
"srs.epita.fr/fic-server/libfic"
"github.com/gin-gonic/gin"
)
var TeamsDir string
func declareCertificateRoutes(router *gin.RouterGroup) {
router.GET("/htpasswd", func(c *gin.Context) {
ret, err := genHtpasswd(true)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.String(http.StatusOK, ret)
})
router.POST("/htpasswd", func(c *gin.Context) {
if htpasswd, err := genHtpasswd(true); err != nil {
log.Println("Unable to generate htpasswd:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
} else if err := ioutil.WriteFile(path.Join(pki.PKIDir, "shared", "ficpasswd"), []byte(htpasswd), 0644); err != nil {
log.Println("Unable to write htpasswd:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.AbortWithStatus(http.StatusOK)
})
router.DELETE("/htpasswd", func(c *gin.Context) {
if err := os.Remove(path.Join(pki.PKIDir, "shared", "ficpasswd")); err != nil {
log.Println("Unable to remove htpasswd:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.AbortWithStatus(http.StatusOK)
})
router.GET("/htpasswd.apr1", func(c *gin.Context) {
ret, err := genHtpasswd(false)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.String(http.StatusOK, ret)
})
router.GET("/ca", infoCA)
router.GET("/ca.pem", getCAPEM)
router.POST("/ca/new", func(c *gin.Context) {
var upki PKISettings
err := c.ShouldBindJSON(&upki)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
if err := pki.GenerateCA(upki.NotBefore, upki.NotAfter); err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.JSON(http.StatusCreated, true)
})
router.GET("/certs", getCertificates)
router.POST("/certs", generateClientCert)
router.DELETE("/certs", func(c *gin.Context) {
v, err := fic.ClearCertificates()
if err != nil {
log.Println("Unable to ClearCertificates:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.JSON(http.StatusOK, v)
})
apiCertificatesRoutes := router.Group("/certs/:certid")
apiCertificatesRoutes.Use(CertificateHandler)
apiCertificatesRoutes.HEAD("", getTeamP12File)
apiCertificatesRoutes.GET("", getTeamP12File)
apiCertificatesRoutes.PUT("", updateCertificateAssociation)
apiCertificatesRoutes.DELETE("", func(c *gin.Context) {
cert := c.MustGet("cert").(*fic.Certificate)
v, err := cert.Revoke()
if err != nil {
log.Println("Unable to Revoke:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.JSON(http.StatusOK, v)
})
}
func declareTeamCertificateRoutes(router *gin.RouterGroup) {
router.GET("/certificates", func(c *gin.Context) {
team := c.MustGet("team").(*fic.Team)
if serials, err := pki.GetTeamSerials(TeamsDir, team.Id); err != nil {
log.Println("Unable to GetTeamSerials:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
} else {
var certs []CertExported
for _, serial := range serials {
if cert, err := fic.GetCertificate(serial); err == nil {
certs = append(certs, CertExported{fmt.Sprintf("%0[2]*[1]X", cert.Id, int(math.Ceil(math.Log2(float64(cert.Id))/8)*2)), cert.Creation, cert.Password, &team.Id, cert.Revoked})
} else {
log.Println("Unable to get back certificate, whereas an association exists on disk: ", err)
}
}
c.JSON(http.StatusOK, certs)
}
})
router.GET("/associations", func(c *gin.Context) {
team := c.MustGet("team").(*fic.Team)
assocs, err := pki.GetTeamAssociations(TeamsDir, team.Id)
if err != nil {
log.Println("Unable to GetTeamAssociations:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.JSON(http.StatusOK, assocs)
})
apiTeamAssociationsRoutes := router.Group("/associations/:assoc")
apiTeamAssociationsRoutes.POST("", func(c *gin.Context) {
team := c.MustGet("team").(*fic.Team)
if err := os.Symlink(fmt.Sprintf("%d", team.Id), path.Join(TeamsDir, c.Params.ByName("assoc"))); err != nil {
log.Println("Unable to create association symlink:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to create association symlink: %s", err.Error())})
return
}
c.JSON(http.StatusOK, c.Params.ByName("assoc"))
})
apiTeamAssociationsRoutes.DELETE("", func(c *gin.Context) {
err := pki.DeleteTeamAssociation(TeamsDir, c.Params.ByName("assoc"))
if err != nil {
log.Printf("Unable to DeleteTeamAssociation(%s): %s", c.Params.ByName("assoc"), err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to delete association symlink: %s", err.Error())})
return
}
c.JSON(http.StatusOK, nil)
})
}
func CertificateHandler(c *gin.Context) {
var certid uint64
var err error
cid := strings.TrimSuffix(string(c.Params.ByName("certid")), ".p12")
if certid, err = strconv.ParseUint(cid, 10, 64); err != nil {
if certid, err = strconv.ParseUint(cid, 16, 64); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid certficate identifier"})
return
}
}
cert, err := fic.GetCertificate(certid)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Certificate not found"})
return
}
c.Set("cert", cert)
c.Next()
}
func genHtpasswd(ssha bool) (ret string, err error) {
var teams []*fic.Team
teams, err = fic.GetTeams()
if err != nil {
return
}
for _, team := range teams {
var serials []uint64
serials, err = pki.GetTeamSerials(TeamsDir, team.Id)
if err != nil {
return
}
if len(serials) == 0 {
// Don't include teams that don't have associated certificates
continue
}
for _, serial := range serials {
var cert *fic.Certificate
cert, err = fic.GetCertificate(serial)
if err != nil {
// Ignore invalid/incorrect/non-existant certificates
continue
}
if cert.Revoked != nil {
continue
}
salt := make([]byte, 5)
if _, err = rand.Read(salt); err != nil {
return
}
if ssha {
hash := sha1.New()
hash.Write([]byte(cert.Password))
hash.Write([]byte(salt))
passwdline := fmt.Sprintf(":{SSHA}%s\n", base64.StdEncoding.EncodeToString(append(hash.Sum(nil), salt...)))
ret += strings.ToLower(team.Name) + passwdline
ret += fmt.Sprintf("%0[2]*[1]x", cert.Id, int(math.Ceil(math.Log2(float64(cert.Id))/8)*2)) + passwdline
ret += fmt.Sprintf("%0[2]*[1]X", cert.Id, int(math.Ceil(math.Log2(float64(cert.Id))/8)*2)) + passwdline
teamAssociations, _ := pki.GetTeamAssociations(TeamsDir, team.Id)
log.Println(path.Join(TeamsDir, fmt.Sprintf("%d", team.Id)), teamAssociations)
for _, ta := range teamAssociations {
ret += strings.Replace(ta, ":", "", -1) + passwdline
}
} else {
salt32 := base32.StdEncoding.EncodeToString(salt)
ret += fmt.Sprintf(
"%s:$apr1$%s$%s\n",
strings.ToLower(team.Name),
salt32,
fic.Apr1Md5(cert.Password, salt32),
)
}
}
}
return
}
type PKISettings struct {
Version int `json:"version"`
SerialNumber *big.Int `json:"serialnumber"`
Issuer pkix.Name `json:"issuer"`
Subject pkix.Name `json:"subject"`
NotBefore time.Time `json:"notbefore"`
NotAfter time.Time `json:"notafter"`
SignatureAlgorithm x509.SignatureAlgorithm `json:"signatureAlgorithm,"`
PublicKeyAlgorithm x509.PublicKeyAlgorithm `json:"publicKeyAlgorithm"`
}
func infoCA(c *gin.Context) {
_, cacert, err := pki.LoadCA()
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "CA not found"})
return
}
c.JSON(http.StatusOK, PKISettings{
Version: cacert.Version,
SerialNumber: cacert.SerialNumber,
Issuer: cacert.Issuer,
Subject: cacert.Subject,
NotBefore: cacert.NotBefore,
NotAfter: cacert.NotAfter,
SignatureAlgorithm: cacert.SignatureAlgorithm,
PublicKeyAlgorithm: cacert.PublicKeyAlgorithm,
})
}
func getCAPEM(c *gin.Context) {
if _, err := os.Stat(pki.CACertPath()); os.IsNotExist(err) {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Unable to locate the CA root certificate. Have you generated it?"})
return
} else if fd, err := os.Open(pki.CACertPath()); err != nil {
log.Println("Unable to open CA root certificate:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
} else {
defer fd.Close()
cnt, err := ioutil.ReadAll(fd)
if err != nil {
log.Println("Unable to read CA root certificate:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.String(http.StatusOK, string(cnt))
}
}
func getTeamP12File(c *gin.Context) {
cert := c.MustGet("cert").(*fic.Certificate)
// Create p12 if necessary
if _, err := os.Stat(pki.ClientP12Path(cert.Id)); os.IsNotExist(err) {
if err := pki.WriteP12(cert.Id, cert.Password); err != nil {
log.Println("Unable to WriteP12:", err.Error())
c.AbortWithError(http.StatusInternalServerError, err)
return
}
}
if _, err := os.Stat(pki.ClientP12Path(cert.Id)); os.IsNotExist(err) {
log.Println("Unable to compute ClientP12Path:", err.Error())
c.AbortWithError(http.StatusInternalServerError, errors.New("Unable to locate the p12. Have you generated it?"))
return
} else if fd, err := os.Open(pki.ClientP12Path(cert.Id)); err != nil {
log.Println("Unable to open ClientP12Path:", err.Error())
c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("Unable to open the p12: %w", err))
return
} else {
defer fd.Close()
data, err := ioutil.ReadAll(fd)
if err != nil {
log.Println("Unable to open ClientP12Path:", err.Error())
c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("Unable to open the p12: %w", err))
return
}
c.Data(http.StatusOK, "application/x-pkcs12", data)
}
}
func generateClientCert(c *gin.Context) {
// First, generate a new, unique, serial
var serial_gen [8]byte
if _, err := rand.Read(serial_gen[:]); err != nil {
log.Println("Unable to read enough entropy to generate client certificate:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to read enough entropy"})
return
}
for fic.ExistingCertSerial(serial_gen) {
if _, err := rand.Read(serial_gen[:]); err != nil {
log.Println("Unable to read enough entropy to generate client certificate:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to read enough entropy"})
return
}
}
var serial_b big.Int
serial_b.SetBytes(serial_gen[:])
serial := serial_b.Uint64()
// Let's pick a random password
password, err := fic.GeneratePassword()
if err != nil {
log.Println("Unable to generate password:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to generate password: " + err.Error()})
return
}
// Ok, now load CA
capriv, cacert, err := pki.LoadCA()
if err != nil {
log.Println("Unable to load the CA:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to load the CA"})
return
}
// Generate our privkey
if err := pki.GenerateClient(serial, cacert.NotBefore, cacert.NotAfter, &cacert, &capriv); err != nil {
log.Println("Unable to generate private key:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to generate private key: " + err.Error()})
return
}
// Save in DB
cert, err := fic.RegisterCertificate(serial, password)
if err != nil {
log.Println("Unable to register certificate:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to register certificate."})
return
}
c.JSON(http.StatusOK, CertExported{fmt.Sprintf("%0[2]*[1]X", cert.Id, int(math.Ceil(math.Log2(float64(cert.Id))/8)*2)), cert.Creation, cert.Password, nil, cert.Revoked})
}
type CertExported struct {
Id string `json:"id"`
Creation time.Time `json:"creation"`
Password string `json:"password,omitempty"`
IdTeam *int64 `json:"id_team"`
Revoked *time.Time `json:"revoked"`
}
func getCertificates(c *gin.Context) {
certificates, err := fic.GetCertificates()
if err != nil {
log.Println("Unable to retrieve certificates list:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during certificates retrieval."})
return
}
ret := make([]CertExported, 0)
for _, cert := range certificates {
dstLinkPath := path.Join(TeamsDir, pki.GetCertificateAssociation(cert.Id))
var idTeam *int64 = nil
if lnk, err := os.Readlink(dstLinkPath); err == nil {
if tid, err := strconv.ParseInt(lnk, 10, 64); err == nil {
idTeam = &tid
}
}
ret = append(ret, CertExported{fmt.Sprintf("%0[2]*[1]X", cert.Id, int(math.Ceil(math.Log2(float64(cert.Id))/8)*2)), cert.Creation, "", idTeam, cert.Revoked})
}
c.JSON(http.StatusOK, ret)
}
type CertUploaded struct {
Team *int64 `json:"id_team"`
}
func updateCertificateAssociation(c *gin.Context) {
cert := c.MustGet("cert").(*fic.Certificate)
var uc CertUploaded
err := c.ShouldBindJSON(&uc)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
dstLinkPath := path.Join(TeamsDir, pki.GetCertificateAssociation(cert.Id))
if uc.Team != nil {
srcLinkPath := fmt.Sprintf("%d", *uc.Team)
if err := os.Symlink(srcLinkPath, dstLinkPath); err != nil {
log.Println("Unable to create certificate symlink:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to create certificate symlink: %s", err.Error())})
return
}
// Mark team as active to ensure it'll be generated
if ut, err := fic.GetTeam(*uc.Team); err != nil {
log.Println("Unable to GetTeam:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during team retrieval."})
return
} else if !ut.Active {
ut.Active = true
_, err := ut.Update()
if err != nil {
log.Println("Unable to UpdateTeam after updateCertificateAssociation:", err.Error())
}
}
} else {
os.Remove(dstLinkPath)
}
c.JSON(http.StatusOK, cert)
}

499
admin/api/claim.go Normal file
View file

@ -0,0 +1,499 @@
package api
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"path"
"strconv"
"time"
"srs.epita.fr/fic-server/admin/generation"
"srs.epita.fr/fic-server/libfic"
"github.com/gin-gonic/gin"
)
func declareClaimsRoutes(router *gin.RouterGroup) {
// Tasks
router.GET("/claims", getClaims)
router.POST("/claims", newClaim)
router.DELETE("/claims", clearClaims)
apiClaimsRoutes := router.Group("/claims/:cid")
apiClaimsRoutes.Use(ClaimHandler)
apiClaimsRoutes.GET("", showClaim)
apiClaimsRoutes.PUT("", updateClaim)
apiClaimsRoutes.POST("", addClaimDescription)
apiClaimsRoutes.DELETE("", deleteClaim)
apiClaimsRoutes.GET("/last_update", getClaimLastUpdate)
apiClaimsRoutes.PUT("/descriptions", updateClaimDescription)
// Assignees
router.GET("/claims-assignees", getAssignees)
router.POST("/claims-assignees", newAssignee)
apiClaimAssigneesRoutes := router.Group("/claims-assignees/:aid")
apiClaimAssigneesRoutes.Use(ClaimAssigneeHandler)
router.GET("/claims-assignees/:aid", showClaimAssignee)
router.PUT("/claims-assignees/:aid", updateClaimAssignee)
router.DELETE("/claims-assignees/:aid", deleteClaimAssignee)
}
func declareExerciceClaimsRoutes(router *gin.RouterGroup) {
router.GET("/claims", getExerciceClaims)
}
func declareTeamClaimsRoutes(router *gin.RouterGroup) {
router.GET("/api/teams/:tid/issue.json", func(c *gin.Context) {
team := c.MustGet("team").(*fic.Team)
issues, err := team.MyIssueFile()
if err != nil {
log.Printf("Unable to MyIssueFile(tid=%d): %s", team.Id, err.Error())
c.JSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to generate issues.json."})
return
}
c.JSON(http.StatusOK, issues)
})
router.GET("/claims", getTeamClaims)
}
func ClaimHandler(c *gin.Context) {
cid, err := strconv.ParseInt(string(c.Params.ByName("cid")), 10, 64)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid claim identifier"})
return
}
claim, err := fic.GetClaim(cid)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Requested claim not found"})
return
}
c.Set("claim", claim)
c.Next()
}
func ClaimAssigneeHandler(c *gin.Context) {
aid, err := strconv.ParseInt(string(c.Params.ByName("aid")), 10, 64)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid claim assignee identifier"})
return
}
assignee, err := fic.GetAssignee(aid)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Requested claim-assignee not found"})
return
}
c.Set("claim-assignee", assignee)
c.Next()
}
func getClaims(c *gin.Context) {
claims, err := fic.GetClaims()
if err != nil {
log.Println("Unable to getClaims:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during claims retrieval."})
return
}
c.JSON(http.StatusOK, claims)
}
func getTeamClaims(c *gin.Context) {
team := c.MustGet("team").(*fic.Team)
claims, err := team.GetClaims()
if err != nil {
log.Printf("Unable to GetClaims(tid=%d): %s", team.Id, err.Error())
c.JSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieve claim list."})
return
}
c.JSON(http.StatusOK, claims)
}
func getExerciceClaims(c *gin.Context) {
exercice := c.MustGet("exercice").(*fic.Exercice)
claims, err := exercice.GetClaims()
if err != nil {
log.Printf("Unable to GetClaims(eid=%d): %s", exercice.Id, err.Error())
c.JSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieve claim list."})
return
}
c.JSON(http.StatusOK, claims)
}
func getClaimLastUpdate(c *gin.Context) {
claim := c.MustGet("claim").(*fic.Claim)
v, err := claim.GetLastUpdate()
if err != nil {
log.Printf("Unable to GetLastUpdate: %s", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during claim last update retrieval."})
return
}
c.JSON(http.StatusOK, v)
}
type ClaimExported struct {
Id int64 `json:"id"`
Subject string `json:"subject"`
IdTeam *int64 `json:"id_team"`
Team *fic.Team `json:"team"`
IdExercice *int64 `json:"id_exercice"`
Exercice *fic.Exercice `json:"exercice"`
IdAssignee *int64 `json:"id_assignee"`
Assignee *fic.ClaimAssignee `json:"assignee"`
Creation time.Time `json:"creation"`
LastUpdate time.Time `json:"last_update"`
State string `json:"state"`
Priority string `json:"priority"`
Descriptions []*fic.ClaimDescription `json:"descriptions"`
}
func showClaim(c *gin.Context) {
claim := c.MustGet("claim").(*fic.Claim)
var e ClaimExported
var err error
if e.Team, err = claim.GetTeam(); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Sprintf("Unable to find associated team: %s", err.Error())})
return
}
if e.Exercice, err = claim.GetExercice(); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Sprintf("Unable to find associated exercice: %s", err.Error())})
return
}
if e.Assignee, err = claim.GetAssignee(); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Sprintf("Unable to find associated assignee: %s", err.Error())})
return
}
if e.Descriptions, err = claim.GetDescriptions(); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Sprintf("Unable to find claim's descriptions: %s", err.Error())})
return
}
e.LastUpdate = e.Creation
for _, d := range e.Descriptions {
if d.Date.After(e.LastUpdate) {
e.LastUpdate = d.Date
}
}
e.Id = claim.Id
e.IdAssignee = claim.IdAssignee
e.IdTeam = claim.IdTeam
e.IdExercice = claim.IdExercice
e.Subject = claim.Subject
e.Creation = claim.Creation
e.State = claim.State
e.Priority = claim.Priority
c.JSON(http.StatusOK, e)
}
type ClaimUploaded struct {
fic.Claim
Whoami *int64 `json:"whoami"`
}
func newClaim(c *gin.Context) {
var uc ClaimUploaded
err := c.ShouldBindJSON(&uc)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
if uc.Subject == "" {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Claim's subject cannot be empty."})
return
}
var t *fic.Team
if uc.IdTeam != nil {
if team, err := fic.GetTeam(*uc.IdTeam); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Sprintf("Unable to get associated team: %s", err.Error())})
return
} else {
t = team
}
} else {
t = nil
}
var e *fic.Exercice
if uc.IdExercice != nil {
if exercice, err := fic.GetExercice(*uc.IdExercice); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Sprintf("Unable to get associated exercice: %s", err.Error())})
return
} else {
e = exercice
}
} else {
e = nil
}
var a *fic.ClaimAssignee
if uc.IdAssignee != nil {
if assignee, err := fic.GetAssignee(*uc.IdAssignee); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Sprintf("Unable to get associated assignee: %s", err.Error())})
return
} else {
a = assignee
}
} else {
a = nil
}
if uc.Priority == "" {
uc.Priority = "medium"
}
claim, err := fic.NewClaim(uc.Subject, t, e, a, uc.Priority)
if err != nil {
log.Println("Unable to newClaim:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to register new claim"})
return
}
c.JSON(http.StatusOK, claim)
}
func clearClaims(c *gin.Context) {
nb, err := fic.ClearClaims()
if err != nil {
log.Printf("Unable to clearClaims: %s", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during claims clearing."})
return
}
c.JSON(http.StatusOK, nb)
}
func generateTeamIssuesFile(team fic.Team) error {
if generation.GeneratorSocket == "" {
if my, err := team.MyIssueFile(); err != nil {
return fmt.Errorf("Unable to generate issue FILE (tid=%d): %w", team.Id, err)
} else if j, err := json.Marshal(my); err != nil {
return fmt.Errorf("Unable to encode issues' file JSON: %w", err)
} else if err = ioutil.WriteFile(path.Join(TeamsDir, fmt.Sprintf("%d", team.Id), "issues.json"), j, 0644); err != nil {
return fmt.Errorf("Unable to write issues' file: %w", err)
}
} else {
resp, err := generation.PerformGeneration(fic.GenStruct{Type: fic.GenTeamIssues, TeamId: team.Id})
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
v, _ := ioutil.ReadAll(resp.Body)
return fmt.Errorf("%s", string(v))
}
}
return nil
}
func addClaimDescription(c *gin.Context) {
claim := c.MustGet("claim").(*fic.Claim)
var ud fic.ClaimDescription
err := c.ShouldBindJSON(&ud)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
assignee, err := fic.GetAssignee(ud.IdAssignee)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Sprintf("Unable to get associated assignee: %s", err.Error())})
return
}
description, err := claim.AddDescription(ud.Content, assignee, ud.Publish)
if err != nil {
log.Println("Unable to addClaimDescription:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to add description"})
return
}
if team, _ := claim.GetTeam(); team != nil {
err = generateTeamIssuesFile(*team)
if err != nil {
log.Println("Unable to generateTeamIssuesFile after addClaimDescription:", err.Error())
}
}
c.JSON(http.StatusOK, description)
}
func updateClaimDescription(c *gin.Context) {
claim := c.MustGet("claim").(*fic.Claim)
var ud fic.ClaimDescription
err := c.ShouldBindJSON(&ud)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
if _, err := ud.Update(); err != nil {
log.Println("Unable to updateClaimDescription:", err.Error())
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "An error occurs during claim description updating."})
return
}
if team, _ := claim.GetTeam(); team != nil {
err = generateTeamIssuesFile(*team)
if err != nil {
log.Println("Unable to generateTeamIssuesFile:", err.Error())
}
}
c.JSON(http.StatusOK, ud)
}
func updateClaim(c *gin.Context) {
claim := c.MustGet("claim").(*fic.Claim)
var uc ClaimUploaded
err := c.ShouldBindJSON(&uc)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
uc.Id = claim.Id
_, err = uc.Update()
if err != nil {
log.Printf("Unable to updateClaim: %s", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during claim update."})
return
}
if claim.State != uc.State {
if uc.Whoami != nil {
if assignee, err := fic.GetAssignee(*uc.Whoami); err == nil {
claim.AddDescription(fmt.Sprintf("%s a changé l'état de la tâche vers %q (était %q).", assignee.Name, uc.State, claim.State), assignee, true)
}
}
}
if claim.IdAssignee != uc.IdAssignee {
if uc.Whoami != nil {
if whoami, err := fic.GetAssignee(*uc.Whoami); err == nil {
if uc.IdAssignee != nil {
if assignee, err := fic.GetAssignee(*uc.IdAssignee); err == nil {
if assignee.Id != whoami.Id {
claim.AddDescription(fmt.Sprintf("%s a assigné la tâche à %s.", whoami.Name, assignee.Name), whoami, false)
} else {
claim.AddDescription(fmt.Sprintf("%s s'est assigné la tâche.", assignee.Name), whoami, false)
}
}
} else {
claim.AddDescription(fmt.Sprintf("%s a retiré l'attribution de la tâche.", whoami.Name), whoami, false)
}
}
}
}
if team, _ := claim.GetTeam(); team != nil {
err = generateTeamIssuesFile(*team)
}
c.JSON(http.StatusOK, uc)
}
func deleteClaim(c *gin.Context) {
claim := c.MustGet("claim").(*fic.Claim)
if nb, err := claim.Delete(); err != nil {
log.Println("Unable to deleteClaim:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during claim deletion."})
return
} else {
c.JSON(http.StatusOK, nb)
}
}
func getAssignees(c *gin.Context) {
assignees, err := fic.GetAssignees()
if err != nil {
log.Println("Unable to getAssignees:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during assignees retrieval."})
return
}
c.JSON(http.StatusOK, assignees)
}
func showClaimAssignee(c *gin.Context) {
c.JSON(http.StatusOK, c.MustGet("claim-assignee").(*fic.ClaimAssignee))
}
func newAssignee(c *gin.Context) {
var ua fic.ClaimAssignee
err := c.ShouldBindJSON(&ua)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
assignee, err := fic.NewClaimAssignee(ua.Name)
if err != nil {
log.Println("Unable to newAssignee:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during assignee creation."})
return
}
c.JSON(http.StatusOK, assignee)
}
func updateClaimAssignee(c *gin.Context) {
assignee := c.MustGet("claim-assignee").(*fic.ClaimAssignee)
var ua fic.ClaimAssignee
err := c.ShouldBindJSON(&ua)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
ua.Id = assignee.Id
if _, err := ua.Update(); err != nil {
log.Println("Unable to updateClaimAssignee:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during claim assignee update."})
return
}
c.JSON(http.StatusOK, ua)
}
func deleteClaimAssignee(c *gin.Context) {
assignee := c.MustGet("claim-assignee").(*fic.ClaimAssignee)
if _, err := assignee.Delete(); err != nil {
log.Println("Unable to deleteClaimAssignee:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("An error occurs during claim assignee deletion: %s", err.Error())})
return
}
c.JSON(http.StatusOK, true)
}

154
admin/api/events.go Normal file
View file

@ -0,0 +1,154 @@
package api
import (
"encoding/json"
"io/ioutil"
"log"
"net/http"
"path"
"strconv"
"srs.epita.fr/fic-server/libfic"
"github.com/gin-gonic/gin"
)
func declareEventsRoutes(router *gin.RouterGroup) {
router.GET("/events", getEvents)
router.GET("/events.json", getLastEvents)
router.POST("/events", newEvent)
router.DELETE("/events", clearEvents)
apiEventsRoutes := router.Group("/events/:evid")
apiEventsRoutes.Use(EventHandler)
apiEventsRoutes.GET("", showEvent)
apiEventsRoutes.PUT("", updateEvent)
apiEventsRoutes.DELETE("", deleteEvent)
}
func EventHandler(c *gin.Context) {
evid, err := strconv.ParseInt(string(c.Params.ByName("evid")), 10, 64)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid event identifier"})
return
}
event, err := fic.GetEvent(evid)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Event not found"})
return
}
c.Set("event", event)
c.Next()
}
func genEventsFile() error {
if evts, err := fic.GetLastEvents(); err != nil {
return err
} else if j, err := json.Marshal(evts); err != nil {
return err
} else if err := ioutil.WriteFile(path.Join(TeamsDir, "events.json"), j, 0666); err != nil {
return err
}
return nil
}
func getEvents(c *gin.Context) {
evts, err := fic.GetEvents()
if err != nil {
log.Println("Unable to GetEvents:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieve events list"})
return
}
c.JSON(http.StatusOK, evts)
}
func getLastEvents(c *gin.Context) {
evts, err := fic.GetLastEvents()
if err != nil {
log.Println("Unable to GetLastEvents:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieve last events list"})
return
}
c.JSON(http.StatusOK, evts)
}
func newEvent(c *gin.Context) {
var ue fic.Event
err := c.ShouldBindJSON(&ue)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
event, err := fic.NewEvent(ue.Text, ue.Kind)
if err != nil {
log.Printf("Unable to newEvent: %s", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during event creation."})
return
}
genEventsFile()
c.JSON(http.StatusOK, event)
}
func clearEvents(c *gin.Context) {
nb, err := fic.ClearEvents()
if err != nil {
log.Printf("Unable to clearEvent: %s", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during event clearing."})
return
}
c.JSON(http.StatusOK, nb)
}
func showEvent(c *gin.Context) {
event := c.MustGet("event").(*fic.Event)
c.JSON(http.StatusOK, event)
}
func updateEvent(c *gin.Context) {
event := c.MustGet("event").(*fic.Event)
var ue fic.Event
err := c.ShouldBindJSON(&ue)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
ue.Id = event.Id
if _, err := ue.Update(); err != nil {
log.Printf("Unable to updateEvent: %s", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during event update."})
return
}
genEventsFile()
c.JSON(http.StatusOK, ue)
}
func deleteEvent(c *gin.Context) {
event := c.MustGet("event").(*fic.Event)
_, err := event.Delete()
if err != nil {
log.Printf("Unable to deleteEvent: %s", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during event deletion."})
return
}
genEventsFile()
c.JSON(http.StatusOK, true)
}

1749
admin/api/exercice.go Normal file

File diff suppressed because it is too large Load diff

126
admin/api/export.go Normal file
View file

@ -0,0 +1,126 @@
package api
import (
"archive/zip"
"encoding/json"
"io"
"log"
"net/http"
"path"
"srs.epita.fr/fic-server/admin/sync"
"srs.epita.fr/fic-server/libfic"
"srs.epita.fr/fic-server/settings"
"github.com/gin-gonic/gin"
)
func declareExportRoutes(router *gin.RouterGroup) {
router.GET("/archive.zip", func(c *gin.Context) {
challengeinfo, err := GetChallengeInfo()
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
my, err := fic.MyJSONTeam(nil, true)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
s, err := settings.ReadSettings(path.Join(settings.SettingsDir, settings.SettingsFile))
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
s.End = nil
s.NextChangeTime = nil
s.DelegatedQA = []string{}
teams, err := fic.ExportTeams(false)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
themes, err := fic.ExportThemes()
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.Writer.WriteHeader(http.StatusOK)
c.Header("Content-Disposition", "attachment; filename=archive.zip")
c.Header("Content-Type", "application/zip")
w := zip.NewWriter(c.Writer)
// challenge.json
f, err := w.Create("challenge.json")
if err == nil {
json.NewEncoder(f).Encode(challengeinfo)
}
// Include partners' logos from challenge.json
if sync.GlobalImporter != nil {
if len(challengeinfo.MainLogo) > 0 {
for _, logo := range challengeinfo.MainLogo {
fd, closer, err := sync.OpenOrGetFile(sync.GlobalImporter, logo)
if err != nil {
log.Printf("Unable to archive main logo %q: %s", logo, err.Error())
continue
}
f, err := w.Create(path.Join("logo", path.Base(logo)))
if err == nil {
io.Copy(f, fd)
}
closer()
}
}
if len(challengeinfo.Partners) > 0 {
for _, partner := range challengeinfo.Partners {
fd, closer, err := sync.OpenOrGetFile(sync.GlobalImporter, partner.Src)
if err != nil {
log.Printf("Unable to archive partner logo %q: %s", partner.Src, err.Error())
continue
}
f, err := w.Create(path.Join("partner", path.Base(partner.Src)))
if err == nil {
io.Copy(f, fd)
}
closer()
}
}
}
// my.json
f, err = w.Create("my.json")
if err == nil {
json.NewEncoder(f).Encode(my)
}
// settings.json
f, err = w.Create("settings.json")
if err == nil {
json.NewEncoder(f).Encode(s)
}
// teams.json
f, err = w.Create("teams.json")
if err == nil {
json.NewEncoder(f).Encode(teams)
}
// themes.json
f, err = w.Create("themes.json")
if err == nil {
json.NewEncoder(f).Encode(themes)
}
w.Close()
})
}

297
admin/api/file.go Normal file
View file

@ -0,0 +1,297 @@
package api
import (
"encoding/hex"
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"strconv"
"srs.epita.fr/fic-server/admin/sync"
"srs.epita.fr/fic-server/libfic"
"github.com/gin-gonic/gin"
)
func declareFilesGlobalRoutes(router *gin.RouterGroup) {
router.DELETE("/files/", clearFiles)
// Remote
router.GET("/remote/themes/:thid/exercices/:exid/files", sync.ApiGetRemoteExerciceFiles)
}
func declareFilesRoutes(router *gin.RouterGroup) {
router.GET("/files", listFiles)
router.POST("/files", createExerciceFile)
apiFilesRoutes := router.Group("/files/:fileid")
apiFilesRoutes.Use(FileHandler)
apiFilesRoutes.GET("", showFile)
apiFilesRoutes.PUT("", updateFile)
apiFilesRoutes.DELETE("", deleteFile)
apiFileDepsRoutes := apiFilesRoutes.Group("/dependancies/:depid")
apiFileDepsRoutes.Use(FileDepHandler)
apiFileDepsRoutes.DELETE("", deleteFileDep)
// Check
apiFilesRoutes.POST("/check", checkFile)
apiFilesRoutes.POST("/gunzip", gunzipFile)
}
func FileHandler(c *gin.Context) {
fileid, err := strconv.ParseInt(string(c.Params.ByName("fileid")), 10, 64)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid file identifier"})
return
}
var file *fic.EFile
if exercice, exists := c.Get("exercice"); exists {
file, err = exercice.(*fic.Exercice).GetFile(fileid)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "File not found"})
return
}
} else {
file, err = fic.GetFile(fileid)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "File not found"})
return
}
}
c.Set("file", file)
c.Next()
}
func FileDepHandler(c *gin.Context) {
depid, err := strconv.ParseInt(string(c.Params.ByName("depid")), 10, 64)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid dependency identifier"})
return
}
c.Set("file-depid", depid)
c.Next()
}
type APIFile struct {
*fic.EFile
Depends []fic.Flag `json:"depends,omitempty"`
}
func genFileList(in []*fic.EFile, e error) (out []APIFile, err error) {
if e != nil {
return nil, e
}
for _, f := range in {
g := APIFile{EFile: f}
var deps []fic.Flag
deps, err = f.GetDepends()
if err != nil {
return
}
for _, d := range deps {
if k, ok := d.(*fic.FlagKey); ok {
k, err = fic.GetFlagKey(k.Id)
if err != nil {
return
}
g.Depends = append(g.Depends, k)
} else if m, ok := d.(*fic.MCQ); ok {
m, err = fic.GetMCQ(m.Id)
if err != nil {
return
}
g.Depends = append(g.Depends, m)
} else {
err = fmt.Errorf("Unknown type %T to handle file dependancy", k)
return
}
}
out = append(out, g)
}
return
}
func listFiles(c *gin.Context) {
var files []APIFile
var err error
if exercice, exists := c.Get("exercice"); exists {
files, err = genFileList(exercice.(*fic.Exercice).GetFiles())
} else {
files, err = genFileList(fic.GetFiles())
}
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.JSON(http.StatusOK, files)
}
func clearFiles(c *gin.Context) {
err := os.RemoveAll(fic.FilesDir)
if err != nil {
log.Println("Unable to remove files:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
err = os.MkdirAll(fic.FilesDir, 0751)
if err != nil {
log.Println("Unable to create FILES:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
_, err = fic.ClearFiles()
if err != nil {
log.Println("Unable to clean DB files:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Les fichiers ont bien été effacés. Mais il n'a pas été possible d'effacer la base de données. Refaites une synchronisation maintenant. " + err.Error()})
return
}
c.JSON(http.StatusOK, true)
}
func showFile(c *gin.Context) {
c.JSON(http.StatusOK, c.MustGet("file").(*fic.EFile))
}
type uploadedFile struct {
URI string
Digest string
}
func createExerciceFile(c *gin.Context) {
exercice, exists := c.Get("exercice")
if !exists {
c.AbortWithStatusJSON(http.StatusMethodNotAllowed, gin.H{"errmsg": "File can only be added inside an exercice."})
return
}
paramsFiles, err := sync.GetExerciceFilesParams(sync.GlobalImporter, exercice.(*fic.Exercice))
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
var uf uploadedFile
err = c.ShouldBindJSON(&uf)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
ret, err := sync.ImportFile(sync.GlobalImporter, uf.URI,
func(filePath string, origin string) (interface{}, error) {
if digest, err := hex.DecodeString(uf.Digest); err != nil {
return nil, err
} else {
published := true
disclaimer := ""
if f, exists := paramsFiles[filepath.Base(filePath)]; exists {
published = !f.Hidden
if disclaimer, err = sync.ProcessMarkdown(sync.GlobalImporter, f.Disclaimer, exercice.(*fic.Exercice).Path); err != nil {
return nil, fmt.Errorf("error during markdown formating of disclaimer: %w", err)
}
}
return exercice.(*fic.Exercice).ImportFile(filePath, origin, digest, nil, disclaimer, published)
}
})
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.JSON(http.StatusOK, ret)
}
func updateFile(c *gin.Context) {
file := c.MustGet("file").(*fic.EFile)
var uf fic.EFile
err := c.ShouldBindJSON(&uf)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
uf.Id = file.Id
if _, err := uf.Update(); err != nil {
log.Println("Unable to updateFile:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to update file."})
return
}
c.JSON(http.StatusOK, uf)
}
func deleteFile(c *gin.Context) {
file := c.MustGet("file").(*fic.EFile)
_, err := file.Delete()
if err != nil {
log.Println("Unable to updateFile:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to update file."})
return
}
c.JSON(http.StatusOK, true)
}
func deleteFileDep(c *gin.Context) {
file := c.MustGet("file").(*fic.EFile)
depid := c.MustGet("file-depid").(int64)
err := file.DeleteDepend(&fic.FlagKey{Id: int(depid)})
if err != nil {
log.Println("Unable to deleteFileDep:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to delete file dependency."})
return
}
c.JSON(http.StatusOK, true)
}
func checkFile(c *gin.Context) {
file := c.MustGet("file").(*fic.EFile)
err := file.CheckFileOnDisk()
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.JSON(http.StatusOK, true)
}
func gunzipFile(c *gin.Context) {
file := c.MustGet("file").(*fic.EFile)
err := file.GunzipFileOnDisk()
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.JSON(http.StatusOK, true)
}

156
admin/api/health.go Normal file
View file

@ -0,0 +1,156 @@
package api
import (
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"path"
"strings"
"time"
"srs.epita.fr/fic-server/admin/pki"
"srs.epita.fr/fic-server/libfic"
"github.com/gin-gonic/gin"
)
var TimestampCheck = "submissions"
func declareHealthRoutes(router *gin.RouterGroup) {
router.GET("/timestamps.json", func(c *gin.Context) {
stat, err := os.Stat(TimestampCheck)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": fmt.Sprintf("timestamp.json: %s", err.Error())})
return
}
now := time.Now().UTC()
c.JSON(http.StatusOK, gin.H{
"frontend": stat.ModTime().UTC(),
"backend": now,
"diffFB": now.Sub(stat.ModTime()),
})
})
router.GET("/health.json", GetHealth)
router.GET("/submissions-stats.json", GetSubmissionsStats)
router.GET("/validations-stats.json", GetValidationsStats)
router.DELETE("/submissions/*path", func(c *gin.Context) {
err := os.Remove(path.Join(TimestampCheck, c.Params.ByName("path")))
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": err.Error()})
return
}
c.Status(http.StatusOK)
})
}
type healthFileReport struct {
IdTeam string `json:"id_team,omitempty"`
Path string `json:"path"`
Error string `json:"error"`
}
func getHealth(pathname string) (ret []healthFileReport) {
if ds, err := ioutil.ReadDir(pathname); err != nil {
ret = append(ret, healthFileReport{
Path: strings.TrimPrefix(pathname, TimestampCheck),
Error: fmt.Sprintf("unable to ReadDir: %s", err),
})
return
} else {
for _, d := range ds {
p := path.Join(pathname, d.Name())
if d.IsDir() && d.Name() != ".tmp" && d.Mode()&os.ModeSymlink == 0 {
ret = append(ret, getHealth(p)...)
} else if !d.IsDir() && d.Mode()&os.ModeSymlink == 0 && time.Since(d.ModTime()) > 2*time.Second {
if d.Name() == ".locked" {
continue
}
teamDir := strings.TrimPrefix(pathname, TimestampCheck)
idteam, _ := pki.GetAssociation(path.Join(TeamsDir, teamDir))
ret = append(ret, healthFileReport{
IdTeam: idteam,
Path: path.Join(teamDir, d.Name()),
Error: "existing untreated file",
})
}
}
return
}
}
func GetHealth(c *gin.Context) {
if _, err := os.Stat(TimestampCheck); err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": fmt.Sprintf("health.json: %s", err.Error())})
return
}
c.JSON(http.StatusOK, getHealth(TimestampCheck))
}
type SubmissionsStats struct {
NbSubmissionLastMinute uint `json:"nbsubminute"`
NbSubmissionLast5Minute uint `json:"nbsub5minute"`
NbSubmissionLastQuarter uint `json:"nbsubquarter"`
NbSubmissionLastHour uint `json:"nbsubhour"`
NbSubmissionLastDay uint `json:"nbsubday"`
}
func calcSubmissionsStats(tries []time.Time) (stats SubmissionsStats) {
lastMinute := time.Now().Add(-1 * time.Minute)
last5Minute := time.Now().Add(-5 * time.Minute)
lastQuarter := time.Now().Add(-15 * time.Minute)
lastHour := time.Now().Add(-1 * time.Hour)
lastDay := time.Now().Add(-24 * time.Hour)
for _, t := range tries {
if lastMinute.Before(t) {
stats.NbSubmissionLastMinute += 1
stats.NbSubmissionLast5Minute += 1
stats.NbSubmissionLastQuarter += 1
stats.NbSubmissionLastHour += 1
stats.NbSubmissionLastDay += 1
} else if last5Minute.Before(t) {
stats.NbSubmissionLast5Minute += 1
stats.NbSubmissionLastQuarter += 1
stats.NbSubmissionLastHour += 1
stats.NbSubmissionLastDay += 1
} else if lastQuarter.Before(t) {
stats.NbSubmissionLastQuarter += 1
stats.NbSubmissionLastHour += 1
stats.NbSubmissionLastDay += 1
} else if lastHour.Before(t) {
stats.NbSubmissionLastHour += 1
stats.NbSubmissionLastDay += 1
} else if lastDay.Before(t) {
stats.NbSubmissionLastDay += 1
}
}
return
}
func GetSubmissionsStats(c *gin.Context) {
tries, err := fic.GetTries(nil, nil)
if err != nil {
log.Println("Unable to GetTries:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieves tries."})
return
}
c.JSON(http.StatusOK, calcSubmissionsStats(tries))
}
func GetValidationsStats(c *gin.Context) {
tries, err := fic.GetValidations(nil, nil)
if err != nil {
log.Println("Unable to GetTries:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieves tries."})
return
}
c.JSON(http.StatusOK, calcSubmissionsStats(tries))
}

98
admin/api/monitor.go Normal file
View file

@ -0,0 +1,98 @@
package api
import (
"bufio"
"io/ioutil"
"net/http"
"os"
"strconv"
"strings"
"github.com/gin-gonic/gin"
)
func declareMonitorRoutes(router *gin.RouterGroup) {
router.GET("/monitor", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"localhost": genLocalConstants(),
})
})
}
func readLoadAvg(fd *os.File) (ret map[string]float64) {
if s, err := ioutil.ReadAll(fd); err == nil {
f := strings.Fields(strings.TrimSpace(string(s)))
if len(f) >= 3 {
ret = map[string]float64{}
ret["1m"], _ = strconv.ParseFloat(f[0], 64)
ret["5m"], _ = strconv.ParseFloat(f[1], 64)
ret["15m"], _ = strconv.ParseFloat(f[2], 64)
}
}
return
}
func readMeminfo(fd *os.File) (ret map[string]uint64) {
ret = map[string]uint64{}
scanner := bufio.NewScanner(fd)
for scanner.Scan() {
f := strings.Fields(strings.TrimSpace(scanner.Text()))
if len(f) >= 2 {
if v, err := strconv.ParseUint(f[1], 10, 64); err == nil {
ret[strings.ToLower(strings.TrimSuffix(f[0], ":"))] = v * 1024
}
}
}
return
}
func readCPUStats(fd *os.File) (ret map[string]map[string]uint64) {
ret = map[string]map[string]uint64{}
scanner := bufio.NewScanner(fd)
for scanner.Scan() {
f := strings.Fields(strings.TrimSpace(scanner.Text()))
if len(f[0]) >= 4 && f[0][0:3] == "cpu" && len(f) >= 8 {
ret[f[0]] = map[string]uint64{}
var total uint64 = 0
for i, k := range []string{"user", "nice", "system", "idle", "iowait", "irq", "softirq"} {
if v, err := strconv.ParseUint(f[i+1], 10, 64); err == nil {
ret[f[0]][k] = v
total += v
}
}
ret[f[0]]["total"] = total
}
}
return
}
func genLocalConstants() interface{} {
ret := map[string]interface{}{}
fi, err := os.Open("/proc/loadavg")
if err != nil {
return err
}
defer fi.Close()
ret["loadavg"] = readLoadAvg(fi)
fi, err = os.Open("/proc/meminfo")
if err != nil {
return err
}
defer fi.Close()
ret["meminfo"] = readMeminfo(fi)
fi, err = os.Open("/proc/stat")
if err != nil {
return err
}
defer fi.Close()
ret["cpustat"] = readCPUStats(fi)
return ret
}

360
admin/api/password.go Normal file
View file

@ -0,0 +1,360 @@
package api
import (
"bytes"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"path"
"text/template"
"unicode"
"srs.epita.fr/fic-server/admin/pki"
"srs.epita.fr/fic-server/libfic"
"github.com/gin-gonic/gin"
)
var (
OidcIssuer = "live.fic.srs.epita.fr"
OidcClientId = "epita-challenge"
OidcSecret = ""
)
func declarePasswordRoutes(router *gin.RouterGroup) {
router.POST("/password", func(c *gin.Context) {
passwd, err := fic.GeneratePassword()
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"password": passwd})
})
router.GET("/oauth-status", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"secret_defined": OidcSecret != "",
})
})
router.GET("/dex.yaml", func(c *gin.Context) {
cfg, err := genDexConfig()
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.String(http.StatusOK, string(cfg))
})
router.POST("/dex.yaml", func(c *gin.Context) {
if dexcfg, err := genDexConfig(); err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
} else if err := ioutil.WriteFile(path.Join(pki.PKIDir, "shared", "dex-config.yaml"), []byte(dexcfg), 0644); err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.JSON(http.StatusOK, true)
})
router.GET("/dex-password.tpl", func(c *gin.Context) {
passtpl, err := genDexPasswordTpl()
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.String(http.StatusOK, string(passtpl))
})
router.POST("/dex-password.tpl", func(c *gin.Context) {
if dexcfg, err := genDexPasswordTpl(); err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
} else if err := ioutil.WriteFile(path.Join(pki.PKIDir, "shared", "dex-password.tpl"), []byte(dexcfg), 0644); err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.JSON(http.StatusOK, true)
})
router.GET("/vouch-proxy.yaml", func(c *gin.Context) {
cfg, err := genVouchProxyConfig()
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.String(http.StatusOK, string(cfg))
})
router.POST("/vouch-proxy.yaml", func(c *gin.Context) {
if dexcfg, err := genVouchProxyConfig(); err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
} else if err := ioutil.WriteFile(path.Join(pki.PKIDir, "shared", "vouch-config.yaml"), []byte(dexcfg), 0644); err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.JSON(http.StatusOK, true)
})
}
func declareTeamsPasswordRoutes(router *gin.RouterGroup) {
router.GET("/password", func(c *gin.Context) {
team := c.MustGet("team").(*fic.Team)
if team.Password != nil {
c.String(http.StatusOK, *team.Password)
} else {
c.AbortWithStatusJSON(http.StatusNotFound, nil)
}
})
router.POST("/password", func(c *gin.Context) {
team := c.MustGet("team").(*fic.Team)
if passwd, err := fic.GeneratePassword(); err != nil {
log.Println("Unable to GeneratePassword:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Something went wrong when generating the new team password"})
return
} else {
team.Password = &passwd
_, err := team.Update()
if err != nil {
log.Println("Unable to Update Team:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Something went wrong when updating the new team password"})
return
}
c.JSON(http.StatusOK, team)
}
})
}
const dexcfgtpl = `issuer: {{ .Issuer }}
storage:
type: sqlite3
config:
file: /var/dex/dex.db
web:
http: 0.0.0.0:5556
frontend:
issuer: {{ .Name }}
logoURL: {{ .LogoPath }}
dir: /srv/dex/web/
oauth2:
skipApprovalScreen: true
staticClients:
{{ range $c := .Clients }}
- id: {{ $c.Id }}
name: {{ $c.Name }}
redirectURIs: [{{ range $u := $c.RedirectURIs }}'{{ $u }}'{{ end }}]
secret: {{ $c.Secret }}
{{ end }}
enablePasswordDB: true
staticPasswords:
{{ range $t := .Teams }}
- email: "team{{ printf "%02d" $t.Id }}"
hash: "{{with $t }}{{ .HashedPassword }}{{end}}"
{{ end }}
`
const dexpasswdtpl = `{{ "{{" }} template "header.html" . {{ "}}" }}
<div class="theme-panel">
<h2 class="theme-heading">
Bienvenue au {{ .Name }}&nbsp;!
</h2>
<form method="post" action="{{ "{{" }} .PostURL {{ "}}" }}">
<div class="theme-form-row">
<div class="theme-form-label">
<label for="userid">Votre équipe</label>
</div>
<select tabindex="1" required id="login" name="login" class="theme-form-input" autofocus>
{{ range $t := .Teams }} <option value="team{{ printf "%02d" $t.Id }}">{{ $t.Name }}</option>
{{ end }} </select>
</div>
<div class="theme-form-row">
<div class="theme-form-label">
<label for="password">Mot de passe</label>
</div>
<input tabindex="2" required id="password" name="password" type="password" class="theme-form-input" placeholder="mot de passe" {{ "{{" }} if .Invalid {{ "}}" }} autofocus {{ "{{" }} end {{ "}}" }}/>
</div>
{{ "{{" }} if .Invalid {{ "}}" }}
<div id="login-error" class="dex-error-box">
Identifiants incorrects.
</div>
{{ "{{" }} end {{ "}}" }}
<button tabindex="3" id="submit-login" type="submit" class="dex-btn theme-btn--primary">C'est parti&nbsp;!</button>
</form>
{{ "{{" }} if .BackLink {{ "}}" }}
<div class="theme-link-back">
<a class="dex-subtle-text" href="{{ "{{" }} .BackLink {{ "}}" }}">Sélectionner une autre méthode d'authentification.</a>
</div>
{{ "{{" }} end {{ "}}" }}
</div>
{{ "{{" }} template "footer.html" . {{ "}}" }}
`
type dexConfigClient struct {
Id string
Name string
RedirectURIs []string
Secret string
}
type dexConfig struct {
Name string
Issuer string
Clients []dexConfigClient
Teams []*fic.Team
LogoPath string
}
func genDexConfig() ([]byte, error) {
if OidcSecret == "" {
return nil, fmt.Errorf("Unable to generate dex configuration: OIDC Secret not defined. Please define FICOIDC_SECRET in your environment.")
}
teams, err := fic.GetTeams()
if err != nil {
return nil, err
}
b := bytes.NewBufferString("")
challengeInfo, err := GetChallengeInfo()
if err != nil {
return nil, fmt.Errorf("Cannot create template: %w", err)
}
// Lower the first letter to be included in a sentence.
name := []rune(challengeInfo.Title)
if len(name) > 0 {
name[0] = unicode.ToLower(name[0])
}
logoPath := ""
if len(challengeInfo.MainLogo) > 0 {
logoPath = path.Join("../../files", "logo", path.Base(challengeInfo.MainLogo[len(challengeInfo.MainLogo)-1]))
}
dexTmpl, err := template.New("dexcfg").Parse(dexcfgtpl)
if err != nil {
return nil, fmt.Errorf("Cannot create template: %w", err)
}
err = dexTmpl.Execute(b, dexConfig{
Name: string(name),
Issuer: "https://" + OidcIssuer,
Clients: []dexConfigClient{
dexConfigClient{
Id: OidcClientId,
Name: challengeInfo.Title,
RedirectURIs: []string{"https://" + OidcIssuer + "/challenge_access/auth"},
Secret: OidcSecret,
},
},
Teams: teams,
LogoPath: logoPath,
})
if err != nil {
return nil, fmt.Errorf("An error occurs during template execution: %w", err)
}
// Also generate team associations
for _, team := range teams {
if _, err := os.Stat(path.Join(TeamsDir, fmt.Sprintf("team%02d", team.Id))); err == nil {
if err = os.Remove(path.Join(TeamsDir, fmt.Sprintf("team%02d", team.Id))); err != nil {
log.Println("Unable to remove existing association symlink:", err.Error())
return nil, fmt.Errorf("Unable to remove existing association symlink: %s", err.Error())
}
}
if err := os.Symlink(fmt.Sprintf("%d", team.Id), path.Join(TeamsDir, fmt.Sprintf("team%02d", team.Id))); err != nil {
log.Println("Unable to create association symlink:", err.Error())
return nil, fmt.Errorf("Unable to create association symlink: %s", err.Error())
}
}
return b.Bytes(), nil
}
func genDexPasswordTpl() ([]byte, error) {
challengeInfo, err := GetChallengeInfo()
if err != nil {
return nil, fmt.Errorf("Cannot create template: %w", err)
}
if teams, err := fic.GetTeams(); err != nil {
return nil, err
} else {
b := bytes.NewBufferString("")
if dexTmpl, err := template.New("dexpasswd").Parse(dexpasswdtpl); err != nil {
return nil, fmt.Errorf("Cannot create template: %w", err)
} else if err = dexTmpl.Execute(b, dexConfig{
Teams: teams,
Name: challengeInfo.Title,
}); err != nil {
return nil, fmt.Errorf("An error occurs during template execution: %w", err)
} else {
return b.Bytes(), nil
}
}
}
const vouchcfgtpl = `# CONFIGURATION FILE HANDLED BY fic-admin
# DO NOT MODIFY IT BY HAND
vouch:
logLevel: debug
allowAllUsers: true
document_root: /challenge_access
cookie:
domain: {{ .Domain }}
oauth:
provider: oidc
client_id: {{ .ClientId }}
client_secret: {{ .ClientSecret }}
callback_urls:
- https://{{ .Domain }}/challenge_access/auth
auth_url: https://{{ .Domain }}/auth
token_url: http://127.0.0.1:5556/token
user_info_url: http://127.0.0.1:5556/userinfo
scopes:
- openid
- email
`
type vouchProxyConfig struct {
Domain string
ClientId string
ClientSecret string
}
func genVouchProxyConfig() ([]byte, error) {
if OidcSecret == "" {
return nil, fmt.Errorf("Unable to generate vouch proxy configuration: OIDC Secret not defined. Please define FICOIDC_SECRET in your environment.")
}
b := bytes.NewBufferString("")
if vouchTmpl, err := template.New("vouchcfg").Parse(vouchcfgtpl); err != nil {
return nil, fmt.Errorf("Cannot create template: %w", err)
} else if err = vouchTmpl.Execute(b, vouchProxyConfig{
Domain: OidcIssuer,
ClientId: OidcClientId,
ClientSecret: OidcSecret,
}); err != nil {
return nil, fmt.Errorf("An error occurs during template execution: %w", err)
} else {
return b.Bytes(), nil
}
}

206
admin/api/public.go Normal file
View file

@ -0,0 +1,206 @@
package api
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"path"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
)
var DashboardDir string
func declarePublicRoutes(router *gin.RouterGroup) {
router.GET("/public/", listPublic)
router.GET("/public/:sid", getPublic)
router.DELETE("/public/:sid", deletePublic)
router.PUT("/public/:sid", savePublic)
}
type FICPublicScene struct {
Type string `json:"type"`
Params map[string]interface{} `json:"params"`
}
type FICPublicDisplay struct {
Scenes []FICPublicScene `json:"scenes"`
Side []FICPublicScene `json:"side"`
CustomCountdown map[string]interface{} `json:"customCountdown"`
HideEvents bool `json:"hideEvents"`
HideCountdown bool `json:"hideCountdown"`
HideCarousel bool `json:"hideCarousel"`
PropagationTime *time.Time `json:"propagationTime,omitempty"`
}
func InitDashboardPresets(dir string) error {
return nil
}
func readPublic(path string) (FICPublicDisplay, error) {
var s FICPublicDisplay
if fd, err := os.Open(path); err != nil {
return s, err
} else {
defer fd.Close()
jdec := json.NewDecoder(fd)
if err := jdec.Decode(&s); err != nil {
return s, err
}
return s, nil
}
}
func savePublicTo(path string, s FICPublicDisplay) error {
if fd, err := os.Create(path); err != nil {
return err
} else {
defer fd.Close()
jenc := json.NewEncoder(fd)
if err := jenc.Encode(s); err != nil {
return err
}
return nil
}
}
type DashboardFiles struct {
Presets []string `json:"presets"`
Nexts []*NextDashboardFile `json:"nexts"`
}
type NextDashboardFile struct {
Name string `json:"name"`
Screen int `json:"screen"`
Date time.Time `json:"date"`
}
func listPublic(c *gin.Context) {
files, err := os.ReadDir(DashboardDir)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
var ret DashboardFiles
for _, file := range files {
if strings.HasPrefix(file.Name(), "preset-") {
ret.Presets = append(ret.Presets, strings.TrimSuffix(strings.TrimPrefix(file.Name(), "preset-"), ".json"))
continue
}
if !strings.HasPrefix(file.Name(), "public") || len(file.Name()) < 18 {
continue
}
ts, err := strconv.ParseInt(file.Name()[8:18], 10, 64)
if err == nil {
s, _ := strconv.Atoi(file.Name()[6:7])
ret.Nexts = append(ret.Nexts, &NextDashboardFile{
Name: file.Name()[6:18],
Screen: s,
Date: time.Unix(ts, 0),
})
}
}
c.JSON(http.StatusOK, ret)
}
func getPublic(c *gin.Context) {
if strings.Contains(c.Params.ByName("sid"), "/") {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "sid cannot contains /"})
return
}
filename := fmt.Sprintf("public%s.json", c.Params.ByName("sid"))
if strings.HasPrefix(c.Params.ByName("sid"), "preset-") {
filename = fmt.Sprintf("%s.json", c.Params.ByName("sid"))
}
if _, err := os.Stat(path.Join(DashboardDir, filename)); !os.IsNotExist(err) {
p, err := readPublic(path.Join(DashboardDir, filename))
if err != nil {
log.Println("Unable to readPublic in getPublic:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during scene retrieval."})
return
}
c.JSON(http.StatusOK, p)
return
}
c.JSON(http.StatusOK, FICPublicDisplay{Scenes: []FICPublicScene{}, Side: []FICPublicScene{}})
}
func deletePublic(c *gin.Context) {
if strings.Contains(c.Params.ByName("sid"), "/") {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "sid cannot contains /"})
return
}
filename := fmt.Sprintf("public%s.json", c.Params.ByName("sid"))
if strings.HasPrefix(c.Params.ByName("sid"), "preset-") {
filename = fmt.Sprintf("%s.json", c.Params.ByName("sid"))
}
if len(filename) == 12 {
if err := savePublicTo(path.Join(DashboardDir, filename), FICPublicDisplay{}); err != nil {
log.Println("Unable to deletePublic:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during scene deletion."})
return
}
} else {
if err := os.Remove(path.Join(DashboardDir, filename)); err != nil {
log.Println("Unable to deletePublic:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during scene deletion."})
return
}
}
c.JSON(http.StatusOK, FICPublicDisplay{Scenes: []FICPublicScene{}, Side: []FICPublicScene{}})
}
func savePublic(c *gin.Context) {
if strings.Contains(c.Params.ByName("sid"), "/") {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "sid cannot contains /"})
return
}
var scenes FICPublicDisplay
err := c.ShouldBindJSON(&scenes)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
filename := fmt.Sprintf("public%s.json", c.Params.ByName("sid"))
if c.Request.URL.Query().Has("t") {
t, err := time.Parse(time.RFC3339, c.Request.URL.Query().Get("t"))
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
filename = fmt.Sprintf("public%s-%d.json", c.Params.ByName("sid"), t.Unix())
} else if c.Request.URL.Query().Has("p") {
filename = fmt.Sprintf("preset-%s.json", c.Request.URL.Query().Get("p"))
}
if err := savePublicTo(path.Join(DashboardDir, filename), scenes); err != nil {
log.Println("Unable to savePublicTo:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during scene saving."})
return
}
c.JSON(http.StatusOK, scenes)
}

119
admin/api/qa.go Normal file
View file

@ -0,0 +1,119 @@
package api
import (
"log"
"net/http"
"strconv"
"srs.epita.fr/fic-server/libfic"
"github.com/gin-gonic/gin"
)
func declareQARoutes(router *gin.RouterGroup) {
router.POST("/qa/", importExerciceQA)
apiQARoutes := router.Group("/qa/:qid")
apiQARoutes.Use(QAHandler)
apiQARoutes.POST("/comments", importQAComment)
}
func QAHandler(c *gin.Context) {
qid, err := strconv.ParseInt(string(c.Params.ByName("qid")), 10, 64)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid QA identifier"})
return
}
qa, err := fic.GetQAQuery(qid)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "QA query not found"})
return
}
c.Set("qa-query", qa)
c.Next()
}
func importExerciceQA(c *gin.Context) {
// Create a new query
var uq fic.QAQuery
err := c.ShouldBindJSON(&uq)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
var exercice *fic.Exercice
if uq.IdExercice == 0 {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "id_exercice not filled"})
return
} else if exercice, err = fic.GetExercice(uq.IdExercice); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Unable to find requested exercice"})
return
}
if len(uq.State) == 0 {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "State not filled"})
return
}
if len(uq.Subject) == 0 {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Subject not filled"})
return
}
if qa, err := exercice.NewQAQuery(uq.Subject, uq.IdTeam, uq.User, uq.State, nil); err != nil {
log.Println("Unable to importExerciceQA:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during query creation."})
return
} else {
qa.Creation = uq.Creation
qa.Solved = uq.Solved
qa.Closed = uq.Closed
_, err = qa.Update()
if err != nil {
log.Println("Unable to update in importExerciceQA:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during query updating."})
return
}
c.JSON(http.StatusOK, qa)
}
}
func importQAComment(c *gin.Context) {
query := c.MustGet("qa-query").(*fic.QAQuery)
// Create a new query
var uc fic.QAComment
err := c.ShouldBindJSON(&uc)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
if len(uc.Content) == 0 {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Empty comment"})
return
}
if qac, err := query.AddComment(uc.Content, uc.IdTeam, uc.User); err != nil {
log.Println("Unable to AddComment in importQAComment:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during comment creation."})
return
} else {
qac.Date = uc.Date
_, err = qac.Update()
if err != nil {
log.Println("Unable to Update comment in importQAComment")
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during comment creation."})
return
}
c.JSON(http.StatusOK, qac)
}
}

67
admin/api/repositories.go Normal file
View file

@ -0,0 +1,67 @@
package api
import (
"net/http"
"strings"
"srs.epita.fr/fic-server/admin/sync"
"github.com/gin-gonic/gin"
)
func declareRepositoriesRoutes(router *gin.RouterGroup) {
if gi, ok := sync.GlobalImporter.(sync.GitImporter); ok {
router.GET("/repositories", func(c *gin.Context) {
mod, err := gi.GetSubmodules()
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"repositories": mod})
})
router.GET("/repositories/*repopath", func(c *gin.Context) {
repopath := strings.TrimPrefix(c.Param("repopath"), "/")
mod, err := gi.GetSubmodule(repopath)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.JSON(http.StatusOK, mod)
})
router.POST("/repositories/*repopath", func(c *gin.Context) {
repopath := strings.TrimPrefix(c.Param("repopath"), "/")
mod, err := gi.IsRepositoryUptodate(repopath)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.JSON(http.StatusOK, mod)
})
router.DELETE("/repositories/*repopath", func(c *gin.Context) {
di, ok := sync.GlobalImporter.(sync.DeletableImporter)
if !ok {
c.AbortWithStatusJSON(http.StatusNotImplemented, gin.H{"errmsg": "Not implemented"})
return
}
if strings.Contains(c.Param("repopath"), "..") {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Repopath contains invalid characters"})
return
}
repopath := strings.TrimPrefix(c.Param("repopath"), "/")
err := di.DeleteDir(repopath)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.JSON(http.StatusOK, true)
})
}
}

29
admin/api/router.go Normal file
View file

@ -0,0 +1,29 @@
package api
import (
"github.com/gin-gonic/gin"
)
func DeclareRoutes(router *gin.RouterGroup) {
apiRoutes := router.Group("/api")
declareCertificateRoutes(apiRoutes)
declareClaimsRoutes(apiRoutes)
declareEventsRoutes(apiRoutes)
declareExercicesRoutes(apiRoutes)
declareExportRoutes(apiRoutes)
declareFilesGlobalRoutes(apiRoutes)
declareFilesRoutes(apiRoutes)
declareGlobalExercicesRoutes(apiRoutes)
declareHealthRoutes(apiRoutes)
declareMonitorRoutes(apiRoutes)
declarePasswordRoutes(apiRoutes)
declarePublicRoutes(apiRoutes)
declareQARoutes(apiRoutes)
declareRepositoriesRoutes(apiRoutes)
declareTeamsRoutes(apiRoutes)
declareThemesRoutes(apiRoutes)
declareSettingsRoutes(apiRoutes)
declareSyncRoutes(apiRoutes)
DeclareVersionRoutes(apiRoutes)
}

425
admin/api/settings.go Normal file
View file

@ -0,0 +1,425 @@
package api
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"path"
"strconv"
"time"
"srs.epita.fr/fic-server/admin/generation"
"srs.epita.fr/fic-server/admin/sync"
"srs.epita.fr/fic-server/libfic"
"srs.epita.fr/fic-server/settings"
"github.com/gin-gonic/gin"
)
var IsProductionEnv = false
func declareSettingsRoutes(router *gin.RouterGroup) {
router.GET("/challenge.json", getChallengeInfo)
router.PUT("/challenge.json", saveChallengeInfo)
router.GET("/settings.json", getSettings)
router.PUT("/settings.json", saveSettings)
router.DELETE("/settings.json", func(c *gin.Context) {
err := ResetSettings()
if err != nil {
log.Println("Unable to ResetSettings:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during setting reset."})
return
}
c.JSON(http.StatusOK, true)
})
router.GET("/settings-next", listNextSettings)
apiNextSettingsRoutes := router.Group("/settings-next/:ts")
apiNextSettingsRoutes.Use(NextSettingsHandler)
apiNextSettingsRoutes.GET("", getNextSettings)
apiNextSettingsRoutes.DELETE("", deleteNextSettings)
router.POST("/reset", reset)
router.POST("/full-generation", fullGeneration)
router.GET("/prod", func(c *gin.Context) {
c.JSON(http.StatusOK, IsProductionEnv)
})
router.PUT("/prod", func(c *gin.Context) {
err := c.ShouldBindJSON(&IsProductionEnv)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
c.JSON(http.StatusOK, IsProductionEnv)
})
}
func NextSettingsHandler(c *gin.Context) {
ts, err := strconv.ParseInt(string(c.Params.ByName("ts")), 10, 64)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid next settings identifier"})
return
}
nsf, err := settings.ReadNextSettingsFile(path.Join(settings.SettingsDir, fmt.Sprintf("%d.json", ts)), ts)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Next settings not found"})
return
}
c.Set("next-settings", nsf)
c.Next()
}
func fullGeneration(c *gin.Context) {
resp, err := generation.FullGeneration()
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"errmsg": err.Error(),
})
return
}
defer resp.Body.Close()
v, _ := io.ReadAll(resp.Body)
c.JSON(resp.StatusCode, gin.H{
"errmsg": string(v),
})
}
func GetChallengeInfo() (*settings.ChallengeInfo, error) {
var challengeinfo string
var err error
if sync.GlobalImporter == nil {
if fd, err := os.Open(path.Join(settings.SettingsDir, settings.ChallengeFile)); err == nil {
defer fd.Close()
var buf []byte
buf, err = io.ReadAll(fd)
if err == nil {
challengeinfo = string(buf)
}
}
} else {
challengeinfo, err = sync.GetFileContent(sync.GlobalImporter, settings.ChallengeFile)
}
if err != nil {
log.Println("Unable to retrieve challenge.json:", err.Error())
return nil, fmt.Errorf("Unable to retrive challenge.json: %w", err)
}
s, err := settings.ReadChallengeInfo(challengeinfo)
if err != nil {
log.Println("Unable to ReadChallengeInfo:", err.Error())
return nil, fmt.Errorf("Unable to read challenge info: %w", err)
}
return s, nil
}
func getChallengeInfo(c *gin.Context) {
if s, err := GetChallengeInfo(); err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
} else {
c.JSON(http.StatusOK, s)
}
}
func saveChallengeInfo(c *gin.Context) {
var info *settings.ChallengeInfo
err := c.ShouldBindJSON(&info)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
if sync.GlobalImporter != nil {
jenc, err := json.Marshal(info)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
err = sync.WriteFileContent(sync.GlobalImporter, "challenge.json", jenc)
if err != nil {
log.Println("Unable to SaveChallengeInfo:", err.Error())
// Ignore the error, try to continue
}
err = sync.ImportChallengeInfo(info, DashboardDir)
if err != nil {
log.Println("Unable to ImportChallengeInfo:", err.Error())
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Sprintf("Something goes wrong when trying to import related files: %s", err.Error())})
return
}
}
if err := settings.SaveChallengeInfo(path.Join(settings.SettingsDir, settings.ChallengeFile), info); err != nil {
log.Println("Unable to SaveChallengeInfo:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to save distributed challenge info: %s", err.Error())})
return
}
c.JSON(http.StatusOK, info)
}
func getSettings(c *gin.Context) {
s, err := settings.ReadSettings(path.Join(settings.SettingsDir, settings.SettingsFile))
if err != nil {
log.Println("Unable to ReadSettings:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to read settings: %s", err.Error())})
return
}
s.WorkInProgress = !IsProductionEnv
c.Writer.Header().Add("X-FIC-Time", fmt.Sprintf("%d", time.Now().Unix()))
c.JSON(http.StatusOK, s)
}
func saveSettings(c *gin.Context) {
var config *settings.Settings
err := c.ShouldBindJSON(&config)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
// Is this a future setting?
if c.Request.URL.Query().Has("t") {
t, err := time.Parse(time.RFC3339, c.Request.URL.Query().Get("t"))
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
// Load current settings to perform diff later
init_settings, err := settings.ReadSettings(path.Join(settings.SettingsDir, settings.SettingsFile))
if err != nil {
log.Println("Unable to ReadSettings:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to read settings: %s", err.Error())})
return
}
current_settings := init_settings
// Apply already registered settings
nsu, err := settings.MergeNextSettingsUntil(&t)
if err == nil {
current_settings = settings.MergeSettings(*init_settings, nsu)
} else {
log.Println("Unable to MergeNextSettingsUntil:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to merge next settings: %s", err.Error())})
return
}
// Keep only diff
diff := settings.DiffSettings(current_settings, config)
hasItems := false
for _, _ = range diff {
hasItems = true
break
}
if !hasItems {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "No difference to apply."})
return
}
if !c.Request.URL.Query().Has("erase") {
// Check if there is already diff to apply at the given time
if nsf, err := settings.ReadNextSettingsFile(path.Join(settings.SettingsDir, fmt.Sprintf("%d.json", t.Unix())), t.Unix()); err == nil {
for k, v := range nsf.Values {
if _, ok := diff[k]; !ok {
diff[k] = v
}
}
}
}
// Save the diff
settings.SaveSettings(path.Join(settings.SettingsDir, fmt.Sprintf("%d.json", t.Unix())), diff)
// Return current settings
c.JSON(http.StatusOK, current_settings)
} else {
// Just apply settings right now!
if err := settings.SaveSettings(path.Join(settings.SettingsDir, settings.SettingsFile), config); err != nil {
log.Println("Unable to SaveSettings:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to save settings: %s", err.Error())})
return
}
ApplySettings(config)
c.JSON(http.StatusOK, config)
}
}
func listNextSettings(c *gin.Context) {
nsf, err := settings.ListNextSettingsFiles()
if err != nil {
log.Println("Unable to ListNextSettingsFiles:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to list next settings files: %s", err.Error())})
return
}
c.JSON(http.StatusOK, nsf)
}
func getNextSettings(c *gin.Context) {
c.JSON(http.StatusOK, c.MustGet("next-settings").(*settings.NextSettingsFile))
}
func deleteNextSettings(c *gin.Context) {
nsf := c.MustGet("next-settings").(*settings.NextSettingsFile)
err := os.Remove(path.Join(settings.SettingsDir, fmt.Sprintf("%d.json", nsf.Id)))
if err != nil {
log.Println("Unable to remove the file:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to remove the file: %s", err.Error())})
return
}
c.JSON(http.StatusOK, true)
}
func ApplySettings(config *settings.Settings) {
fic.PartialValidation = config.PartialValidation
fic.UnlockedChallengeDepth = config.UnlockedChallengeDepth
fic.UnlockedChallengeUpTo = config.UnlockedChallengeUpTo
fic.DisplayAllFlags = config.DisplayAllFlags
fic.HideCaseSensitivity = config.HideCaseSensitivity
fic.UnlockedStandaloneExercices = config.UnlockedStandaloneExercices
fic.UnlockedStandaloneExercicesByThemeStepValidation = config.UnlockedStandaloneExercicesByThemeStepValidation
fic.UnlockedStandaloneExercicesByStandaloneExerciceValidation = config.UnlockedStandaloneExercicesByStandaloneExerciceValidation
fic.DisplayMCQBadCount = config.DisplayMCQBadCount
fic.FirstBlood = config.FirstBlood
fic.SubmissionCostBase = config.SubmissionCostBase
fic.HintCoefficient = config.HintCurCoefficient
fic.WChoiceCoefficient = config.WChoiceCurCoefficient
fic.ExerciceCurrentCoefficient = config.ExerciceCurCoefficient
fic.GlobalScoreCoefficient = config.GlobalScoreCoefficient
fic.SubmissionCostBase = config.SubmissionCostBase
fic.SubmissionUniqueness = config.SubmissionUniqueness
fic.CountOnlyNotGoodTries = config.CountOnlyNotGoodTries
fic.QuestionGainRatio = config.QuestionGainRatio
if config.DiscountedFactor != fic.DiscountedFactor {
fic.DiscountedFactor = config.DiscountedFactor
if err := fic.DBRecreateDiscountedView(); err != nil {
log.Println("Unable to recreate exercices_discounted view:", err.Error())
}
}
}
func ResetSettings() error {
return settings.SaveSettings(path.Join(settings.SettingsDir, settings.SettingsFile), &settings.Settings{
WorkInProgress: IsProductionEnv,
FirstBlood: fic.FirstBlood,
SubmissionCostBase: fic.SubmissionCostBase,
ExerciceCurCoefficient: 1,
HintCurCoefficient: 1,
WChoiceCurCoefficient: 1,
GlobalScoreCoefficient: 1,
DiscountedFactor: 0,
QuestionGainRatio: 0,
UnlockedStandaloneExercices: 10,
UnlockedStandaloneExercicesByThemeStepValidation: 1,
UnlockedStandaloneExercicesByStandaloneExerciceValidation: 0,
AllowRegistration: false,
CanJoinTeam: false,
DenyTeamCreation: false,
DenyNameChange: false,
AcceptNewIssue: true,
QAenabled: false,
EnableResolutionRoute: false,
PartialValidation: true,
UnlockedChallengeDepth: 0,
SubmissionUniqueness: true,
CountOnlyNotGoodTries: true,
DisplayAllFlags: false,
DisplayMCQBadCount: false,
EventKindness: false,
})
}
func ResetChallengeInfo() error {
return settings.SaveChallengeInfo(path.Join(settings.SettingsDir, settings.ChallengeFile), &settings.ChallengeInfo{
Title: "Challenge forensic",
SubTitle: "sous le patronage du commandement de la cyberdéfense",
Authors: "Laboratoire SRS, ÉPITA",
VideosLink: "",
Description: `<p>Le challenge <em>forensic</em> vous place dans la peau de <strong>spécialistes en investigation numérique</strong>. Nous mettons à votre disposition une <strong>vingtaine de scénarios différents</strong>, dans lesquels vous devrez faire les différentes étapes <strong>de la caractérisation dune réponse à incident</strong> proposées.</p>
<p>Chaque scénario met en scène un contexte d<strong>entreprise</strong>, ayant découvert récemment quelle a été <strong>victime dune cyberattaque</strong>. Elle vous demande alors de laider à <strong>caractériser</strong>, afin de mieux comprendre <strong>la situation</strong>, notamment le <strong>mode opératoire de ladversaire</strong>, les <strong>impacts</strong> de la cyberattaque, le <strong>périmètre technique compromis</strong>, etc. Il faudra parfois aussi léclairer sur les premières étapes de la réaction.</p>`,
Rules: `<h3>Déroulement</h3>
<p>Pendant toute la durée du challenge, vous aurez <strong>accès à tous les scénarios</strong>, mais seulement à la première des 5 étapes. <strong>Chaque étape</strong> supplémentaire <strong>est débloquée lorsque vous validez lintégralité de létape précédente</strong>. Toutefois, pour dynamiser le challenge toutes les étapes et tous les scénarios seront débloquées pour la dernière heure du challenge.</p>
<p>Nous mettons à votre disposition une <strong>plateforme</strong> sur laquelle vous pourrez <strong>obtenir les informations sur le contexte</strong> de lentreprise et, généralement, une <strong>série de fichiers</strong> qui semblent appropriés pour avancer dans linvestigation.</p>
<p>La <strong>validation dune étape</strong> se fait sur la plateforme, après avoir analysé les informations fournies, en <strong>répondant à des questions</strong> plus ou moins précises. Il sagit le plus souvent des <strong>mots-clefs</strong> que lon placerait dans un <strong>rapport</strong>.</p>
<p>Pour vous débloquer ou accélérer votre investigation, vous pouvez accéder à quelques <strong><em>indices</em></strong>, en échange dune décote sur votre score dun certain nombre de points préalablement affichés.</p>
<h3>Calcul des points, bonus, malus et classement</h3>
<p>Chaque équipe dispose dun <strong>compteur de points</strong> dans lintervalle ]-;+[ (aux détails techniques près), à partir duquel <strong>le classement est établi</strong>.</p>
<p>Vous <strong>perdez des points</strong> en <strong>dévoilant des indices</strong>, en <strong>demandant des propositions de réponses</strong> en remplacement de certains champs de texte, ou en <strong>essayant un trop grand nombre de fois une réponse</strong>.</p>
<p>Le nombre de points que vous fait perdre un indice dépend habituellement de laide quil vous apportera et est indiqué avant de le dévoiler, car il peut fluctuer en fonction de lavancement du challenge.</p>
<p>Pour chaque champ de texte, vous disposez de 10 tentatives avant de perdre des points (vous perdez les points même si vous ne validez pas létape) pour chaque tentative supplémentaire : -0,25&nbsp;point entre 11 et 20, -0,5 entre 21 et 30, -0,75 entre 31 et 40,&nbsp;</p>
<p>La seule manière de <strong>gagner des points</strong> est de <strong>valider une étape dun scénario dans son intégralité</strong>. Le nombre de points gagnés <strong>dépend de la difficulté théorique</strong> de létape ainsi que <strong>déventuels bonus</strong>. Un bonus de <strong>10&nbsp;%</strong> est accordé à la première équipe qui valide une étape. D<strong>autres bonus</strong> peuvent ponctuer le challenge, détaillé dans la partie suivante.</p>
<p>Le classement est établi par équipe, selon le nombre de points récoltés et perdus par tous les membres. En cas dégalité au score, les équipes sont départagées en fonction de leur ordre darrivée à ce score.</p>
<h3>Temps forts</h3>
<p>Le challenge <em>forensic</em> est jalonné de plusieurs temps forts durant lesquels <strong>certains calculs</strong> détaillés dans la partie précédente <strong>peuvent être altérés</strong>. Léquipe danimation du challenge vous <strong>avertira</strong> environ <strong>15 minutes avant</strong> le début de la modification.</p>
<p>Chaque modification se répercute instantanément dans votre interface, attendez simplement quelle apparaisse afin dêtre certain den bénéficier. Un compte à rebours est généralement affiché sur les écrans pour indiquer la fin dun temps fort. La fin dapplication dun bonus est déterminé par lheure darrivée de votre demande sur nos serveurs.</p>
<p>Sans y être limité ou assuré, sachez que durant les précédentes éditions du challenge <em>forensic</em>, nous avons par exemple : <strong>doublé les points</strong> de défis peu tentés, <strong>doublé les points de tous les défis</strong> pendant 30 minutes, <strong>réduit le coût des indices</strong> pendant 15 minutes, etc.</p>
<p></p>
<p>Tous les étudiants de la majeure Système, Réseaux et Sécurité de lÉPITA, son équipe enseignante ainsi que le commandement de la cyberdéfense vous souhaitent bon courage pour cette nouvelle éditions du challenge !</p>`,
YourMission: `<h4>Bienvenue au challenge forensic&nbsp;!</h4>
<p>Vous voici aujourd'hui dans la peau de <strong>spécialistes en investigation numérique</strong>. Vous avez à votre disposition une vingtaine de scénarios différents dans lesquels vous devrez faire les différentes étapes <strong>de la caractérisation dune réponse à incident</strong>.</p>
<p>Chaque scénario est découpé en 5 grandes <strong>étapes de difficulté croissante</strong>. Un certain nombre de points est attribué à chaque étape, avec un processus de validation automatique.</p>
<p>Un classement est établi en temps réel, tenant compte des différents bonus, en fonction du nombre de points de chaque équipe.</p>`,
})
}
func reset(c *gin.Context) {
var m map[string]string
err := c.ShouldBindJSON(&m)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
t, ok := m["type"]
if !ok {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Field type not found"})
}
switch t {
case "teams":
err = fic.ResetTeams()
case "challenges":
err = fic.ResetExercices()
case "game":
err = fic.ResetGame()
case "annexes":
err = fic.ResetAnnexes()
case "settings":
err = ResetSettings()
case "challengeInfo":
err = ResetChallengeInfo()
default:
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Unknown reset type"})
return
}
if err != nil {
log.Printf("Unable to reset (type=%q): %s", t, err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to performe the reset: %s", err.Error())})
return
}
c.JSON(http.StatusOK, true)
}

411
admin/api/sync.go Normal file
View file

@ -0,0 +1,411 @@
package api
import (
"fmt"
"log"
"net/http"
"net/url"
"os"
"path"
"reflect"
"strings"
"srs.epita.fr/fic-server/admin/generation"
"srs.epita.fr/fic-server/admin/sync"
"srs.epita.fr/fic-server/libfic"
"github.com/gin-gonic/gin"
"go.uber.org/multierr"
)
var lastSyncError = ""
func flatifySyncErrors(errs error) (ret []string) {
for _, err := range multierr.Errors(errs) {
ret = append(ret, err.Error())
}
return
}
func declareSyncRoutes(router *gin.RouterGroup) {
apiSyncRoutes := router.Group("/sync")
// Return the global sync status
apiSyncRoutes.GET("/status", func(c *gin.Context) {
syncMtd := "Disabled"
if sync.GlobalImporter != nil {
syncMtd = sync.GlobalImporter.Kind()
}
var syncId *string
if sync.GlobalImporter != nil {
syncId = sync.GlobalImporter.Id()
}
c.JSON(http.StatusOK, gin.H{
"sync-type": reflect.TypeOf(sync.GlobalImporter).Name(),
"sync-id": syncId,
"sync": syncMtd,
"pullMutex": !sync.OneGitPullStatus(),
"syncMutex": !sync.OneDeepSyncStatus() && !sync.OneThemeDeepSyncStatus(),
"progress": sync.DeepSyncProgress,
"lastError": lastSyncError,
})
})
// Base sync checks if the local directory is in sync with remote one.
apiSyncRoutes.POST("/base", func(c *gin.Context) {
err := sync.GlobalImporter.Sync()
if err != nil {
lastSyncError = err.Error()
c.JSON(http.StatusExpectationFailed, gin.H{"errmsg": err.Error()})
} else {
lastSyncError = ""
c.JSON(http.StatusOK, true)
}
})
// Speedy sync performs a recursive synchronization without importing files.
apiSyncRoutes.POST("/speed", func(c *gin.Context) {
st := sync.SpeedySyncDeep(sync.GlobalImporter)
sync.EditDeepReport(&st, false)
c.JSON(http.StatusOK, st)
})
// Deep sync: a fully recursive synchronization (can be limited by theme).
apiSyncRoutes.POST("/deep", func(c *gin.Context) {
r := sync.SyncDeep(sync.GlobalImporter)
lastSyncError = ""
c.JSON(http.StatusOK, r)
})
apiSyncRoutes.POST("/local-diff", APIDiffDBWithRemote)
apiSyncDeepRoutes := apiSyncRoutes.Group("/deep/:thid")
apiSyncDeepRoutes.Use(ThemeHandler)
// Special route to handle standalone exercices
apiSyncRoutes.POST("/deep/0", func(c *gin.Context) {
var st []string
for _, se := range multierr.Errors(sync.SyncThemeDeep(sync.GlobalImporter, &fic.StandaloneExercicesTheme, 0, 250, nil)) {
st = append(st, se.Error())
}
sync.EditDeepReport(&sync.SyncReport{Exercices: st}, false)
sync.DeepSyncProgress = 255
lastSyncError = ""
c.JSON(http.StatusOK, st)
})
apiSyncDeepRoutes.POST("", func(c *gin.Context) {
theme := c.MustGet("theme").(*fic.Theme)
exceptions := sync.LoadThemeException(sync.GlobalImporter, theme)
var st []string
for _, se := range multierr.Errors(sync.SyncThemeDeep(sync.GlobalImporter, theme, 0, 250, exceptions)) {
st = append(st, se.Error())
}
sync.EditDeepReport(&sync.SyncReport{Themes: map[string][]string{theme.Name: st}}, false)
sync.DeepSyncProgress = 255
lastSyncError = ""
c.JSON(http.StatusOK, st)
})
// Auto sync: to use with continuous deployment, in a development env
apiSyncRoutes.POST("/auto/*p", autoSync)
// Themes
apiSyncRoutes.POST("/fixurlids", fixAllURLIds)
apiSyncRoutes.POST("/themes", func(c *gin.Context) {
_, errs := sync.SyncThemes(sync.GlobalImporter)
lastSyncError = ""
c.JSON(http.StatusOK, flatifySyncErrors(errs))
})
apiSyncThemesRoutes := apiSyncRoutes.Group("/themes/:thid")
apiSyncThemesRoutes.Use(ThemeHandler)
apiSyncThemesRoutes.POST("/fixurlid", func(c *gin.Context) {
theme := c.MustGet("theme").(*fic.Theme)
if theme.FixURLId() {
v, err := theme.Update()
if err != nil {
log.Println("Unable to UpdateTheme after fixurlid:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when saving the theme."})
return
}
c.JSON(http.StatusOK, v)
} else {
c.AbortWithStatusJSON(http.StatusOK, 0)
}
})
// Exercices
declareSyncExercicesRoutes(apiSyncRoutes)
declareSyncExercicesRoutes(apiSyncThemesRoutes)
// Videos sync imports resolution.mp4 from path stored in database.
apiSyncRoutes.POST("/videos", func(c *gin.Context) {
exercices, err := fic.GetExercices()
if err != nil {
log.Println("Unable to GetExercices:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieve exercices list."})
return
}
for _, e := range exercices {
if len(e.VideoURI) == 0 || !strings.HasPrefix(e.VideoURI, "$RFILES$/") {
continue
}
vpath, err := url.PathUnescape(strings.TrimPrefix(e.VideoURI, "$RFILES$/"))
if err != nil {
c.JSON(http.StatusExpectationFailed, gin.H{"errmsg": fmt.Sprintf("Unable to perform URL unescape: %s", err.Error())})
return
}
_, err = sync.ImportFile(sync.GlobalImporter, vpath, func(filePath, URI string) (interface{}, error) {
e.VideoURI = path.Join("$FILES$", strings.TrimPrefix(filePath, fic.FilesDir))
return e.Update()
})
if err != nil {
c.JSON(http.StatusExpectationFailed, gin.H{"errmsg": err.Error()})
return
}
}
c.JSON(http.StatusOK, true)
})
// Remove soluces from the database.
apiSyncRoutes.POST("/drop_soluces", func(c *gin.Context) {
exercices, err := fic.GetExercices()
if err != nil {
log.Println("Unable to GetExercices:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieve exercices list."})
return
}
var errs error
for _, e := range exercices {
// Remove any published video
if len(e.VideoURI) > 0 && strings.HasPrefix(e.VideoURI, "$FILES$") {
vpath := path.Join(fic.FilesDir, strings.TrimPrefix(e.VideoURI, "$FILES$/"))
err = os.Remove(vpath)
if err != nil {
errs = multierr.Append(errs, fmt.Errorf("unable to delete published video (%q): %w", e.VideoURI, err))
}
}
// Clean the database
if len(e.VideoURI) > 0 || len(e.Resolution) > 0 {
e.VideoURI = ""
e.Resolution = ""
_, err = e.Update()
if err != nil {
errs = multierr.Append(errs, fmt.Errorf("unable to update exercice (%d: %s): %w", e.Id, e.Title, err))
}
}
}
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"errmsg": flatifySyncErrors(err)})
} else {
c.JSON(http.StatusOK, true)
}
})
}
func declareSyncExercicesRoutes(router *gin.RouterGroup) {
router.POST("/exercices", func(c *gin.Context) {
theme := c.MustGet("theme").(*fic.Theme)
exceptions := sync.LoadThemeException(sync.GlobalImporter, theme)
_, errs := sync.SyncExercices(sync.GlobalImporter, theme, exceptions)
c.JSON(http.StatusOK, flatifySyncErrors(errs))
})
apiSyncExercicesRoutes := router.Group("/exercices/:eid")
apiSyncExercicesRoutes.Use(ExerciceHandler)
apiSyncExercicesRoutes.POST("", func(c *gin.Context) {
theme := c.MustGet("theme").(*fic.Theme)
exercice := c.MustGet("exercice").(*fic.Exercice)
exceptions := sync.LoadExerciceException(sync.GlobalImporter, theme, exercice, nil)
_, _, _, errs := sync.SyncExercice(sync.GlobalImporter, theme, exercice.Path, nil, exceptions)
c.JSON(http.StatusOK, flatifySyncErrors(errs))
})
apiSyncExercicesRoutes.POST("/files", func(c *gin.Context) {
exercice := c.MustGet("exercice").(*fic.Exercice)
theme := c.MustGet("theme").(*fic.Theme)
exceptions := sync.LoadExerciceException(sync.GlobalImporter, theme, exercice, nil)
c.JSON(http.StatusOK, flatifySyncErrors(sync.ImportExerciceFiles(sync.GlobalImporter, exercice, exceptions)))
})
apiSyncExercicesRoutes.POST("/fixurlid", func(c *gin.Context) {
exercice := c.MustGet("exercice").(*fic.Exercice)
if exercice.FixURLId() {
v, err := exercice.Update()
if err != nil {
log.Println("Unable to UpdateExercice after fixurlid:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when saving the exercice."})
return
}
c.JSON(http.StatusOK, v)
} else {
c.AbortWithStatusJSON(http.StatusOK, 0)
}
})
apiSyncExercicesRoutes.POST("/hints", func(c *gin.Context) {
exercice := c.MustGet("exercice").(*fic.Exercice)
theme := c.MustGet("theme").(*fic.Theme)
exceptions := sync.LoadExerciceException(sync.GlobalImporter, theme, exercice, nil)
_, errs := sync.SyncExerciceHints(sync.GlobalImporter, exercice, sync.ExerciceFlagsMap(sync.GlobalImporter, exercice), exceptions)
c.JSON(http.StatusOK, flatifySyncErrors(errs))
})
apiSyncExercicesRoutes.POST("/flags", func(c *gin.Context) {
exercice := c.MustGet("exercice").(*fic.Exercice)
theme := c.MustGet("theme").(*fic.Theme)
exceptions := sync.LoadExerciceException(sync.GlobalImporter, theme, exercice, nil)
_, errs := sync.SyncExerciceFlags(sync.GlobalImporter, exercice, exceptions)
_, herrs := sync.SyncExerciceHints(sync.GlobalImporter, exercice, sync.ExerciceFlagsMap(sync.GlobalImporter, exercice), exceptions)
c.JSON(http.StatusOK, flatifySyncErrors(multierr.Append(errs, herrs)))
})
}
// autoSync tries to performs a smart synchronization, when in development environment.
// It'll sync most of modified things, and will delete out of sync data.
// Avoid using it in a production environment.
func autoSync(c *gin.Context) {
p := strings.Split(strings.TrimPrefix(c.Params.ByName("p"), "/"), "/")
if !IsProductionEnv {
if err := sync.GlobalImporter.Sync(); err != nil {
lastSyncError = err.Error()
log.Println("Unable to sync.GI.Sync:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to perform the pull."})
return
}
lastSyncError = ""
}
themes, err := fic.GetThemes()
if err != nil {
log.Println("Unable to GetThemes:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieve theme list."})
return
}
// No argument, do a deep sync
if len(p) == 0 {
if !IsProductionEnv {
for _, theme := range themes {
theme.DeleteDeep()
}
}
st := sync.SyncDeep(sync.GlobalImporter)
c.JSON(http.StatusOK, st)
return
}
var theTheme *fic.Theme
// Find the given theme
for _, theme := range themes {
if theme.Path == p[0] {
theTheme = theme
break
}
}
if theTheme == nil {
// The theme doesn't exists locally, perhaps it has not been imported already?
rThemes, err := sync.GetThemes(sync.GlobalImporter)
if err == nil {
for _, theme := range rThemes {
if theme == p[0] {
sync.SyncThemes(sync.GlobalImporter)
themes, err := fic.GetThemes()
if err == nil {
for _, theme := range themes {
if theme.Path == p[0] {
theTheme = theme
break
}
}
}
break
}
}
}
if theTheme == nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": fmt.Sprintf("Theme not found %q", p[0])})
return
}
}
if !IsProductionEnv {
exercices, err := theTheme.GetExercices()
if err == nil {
for _, exercice := range exercices {
if len(p) <= 1 || exercice.Path == path.Join(p[0], p[1]) {
exercice.DeleteDeep()
}
}
}
}
exceptions := sync.LoadThemeException(sync.GlobalImporter, theTheme)
var st []string
for _, se := range multierr.Errors(sync.SyncThemeDeep(sync.GlobalImporter, theTheme, 0, 250, exceptions)) {
st = append(st, se.Error())
}
sync.EditDeepReport(&sync.SyncReport{Themes: map[string][]string{theTheme.Name: st}}, false)
sync.DeepSyncProgress = 255
resp, err := generation.FullGeneration()
if err == nil {
defer resp.Body.Close()
}
c.JSON(http.StatusOK, st)
}
func diffDBWithRemote() (map[string][]syncDiff, error) {
diffs := map[string][]syncDiff{}
themes, err := fic.GetThemesExtended()
if err != nil {
return nil, err
}
// Compare inner themes
for _, theme := range themes {
diffs[theme.Name], err = diffThemeWithRemote(theme)
if err != nil {
return nil, fmt.Errorf("Unable to diffThemeWithRemote: %w", err)
}
}
return diffs, err
}
func APIDiffDBWithRemote(c *gin.Context) {
diffs, err := diffDBWithRemote()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.JSON(http.StatusOK, diffs)
}

641
admin/api/team.go Normal file
View file

@ -0,0 +1,641 @@
package api
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
"strings"
"time"
"srs.epita.fr/fic-server/admin/pki"
"srs.epita.fr/fic-server/libfic"
"github.com/gin-gonic/gin"
)
func declareTeamsRoutes(router *gin.RouterGroup) {
router.GET("/teams.json", func(c *gin.Context) {
teams, err := fic.ExportTeams(false)
if err != nil {
log.Println("Unable to ExportTeams:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during teams export."})
return
}
c.JSON(http.StatusOK, teams)
})
router.GET("/teams-members.json", func(c *gin.Context) {
teams, err := fic.ExportTeams(true)
if err != nil {
log.Println("Unable to ExportTeams:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during teams export."})
return
}
c.JSON(http.StatusOK, teams)
})
router.GET("/teams-associations.json", allAssociations)
router.GET("/teams-binding", bindingTeams)
router.GET("/teams-nginx", nginxGenTeams)
router.POST("/refine_colors", refineTeamsColors)
router.POST("/disableinactiveteams", disableInactiveTeams)
router.POST("/enableallteams", enableAllTeams)
router.GET("/teams-members-nginx", nginxGenMember)
router.GET("/teams-tries.json", func(c *gin.Context) {
tries, err := fic.GetTries(nil, nil)
if err != nil {
log.Println("Unable to GetTries:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieves tries."})
return
}
c.JSON(http.StatusOK, tries)
})
router.GET("/teams", func(c *gin.Context) {
teams, err := fic.GetTeams()
if err != nil {
log.Println("Unable to GetTeams:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during teams listing."})
return
}
c.JSON(http.StatusOK, teams)
})
router.POST("/teams", createTeam)
apiTeamsRoutes := router.Group("/teams/:tid")
apiTeamsRoutes.Use(TeamHandler)
apiTeamsRoutes.GET("/", func(c *gin.Context) {
c.JSON(http.StatusOK, c.MustGet("team").(*fic.Team))
})
apiTeamsRoutes.PUT("/", updateTeam)
apiTeamsRoutes.POST("/", addTeamMember)
apiTeamsRoutes.DELETE("/", deleteTeam)
apiTeamsRoutes.GET("/score-grid.json", func(c *gin.Context) {
team := c.MustGet("team").(*fic.Team)
sg, err := team.ScoreGrid()
if err != nil {
log.Printf("Unable to get ScoreGrid(tid=%d): %s", team.Id, err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during score grid calculation."})
return
}
c.JSON(http.StatusOK, sg)
})
apiTeamsPublicRoutes := router.Group("/teams/:tid")
apiTeamsPublicRoutes.Use(TeamPublicHandler)
apiTeamsPublicRoutes.GET("/my.json", func(c *gin.Context) {
var team *fic.Team
if t, ok := c.Get("team"); ok && t != nil {
team = t.(*fic.Team)
}
tfile, err := fic.MyJSONTeam(team, true)
if err != nil {
log.Println("Unable to get MyJSONTeam:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during team JSON generation."})
return
}
c.JSON(http.StatusOK, tfile)
})
apiTeamsPublicRoutes.GET("/wait.json", func(c *gin.Context) {
var team *fic.Team
if t, ok := c.Get("team"); ok && t != nil {
team = t.(*fic.Team)
}
tfile, err := fic.MyJSONTeam(team, false)
if err != nil {
log.Println("Unable to get MyJSONTeam:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during team JSON generation."})
return
}
c.JSON(http.StatusOK, tfile)
})
apiTeamsPublicRoutes.GET("/stats.json", func(c *gin.Context) {
var team *fic.Team
if t, ok := c.Get("team"); ok && t != nil {
team = t.(*fic.Team)
}
if team != nil {
stats, err := team.GetStats()
if err != nil {
log.Println("Unable to get GetStats:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during stats calculation."})
return
}
c.JSON(http.StatusOK, stats)
} else {
stats, err := fic.GetTeamsStats(nil)
if err != nil {
log.Println("Unable to get GetTeamsStats:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during global stats calculation."})
return
}
c.JSON(http.StatusOK, stats)
}
})
apiTeamsRoutes.GET("/history.json", func(c *gin.Context) {
team := c.MustGet("team").(*fic.Team)
history, err := team.GetHistory()
if err != nil {
log.Println("Unable to get GetHistory:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during history calculation."})
return
}
c.JSON(http.StatusOK, history)
})
apiTeamsRoutes.PATCH("/history.json", updateHistory)
apiTeamsRoutes.DELETE("/history.json", delHistory)
apiTeamsPublicRoutes.GET("/tries", func(c *gin.Context) {
team := c.MustGet("team").(*fic.Team)
tries, err := fic.GetTries(team, nil)
if err != nil {
log.Println("Unable to GetTries:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during tries calculation."})
return
}
c.JSON(http.StatusOK, tries)
})
apiTeamsRoutes.GET("/members", func(c *gin.Context) {
team := c.MustGet("team").(*fic.Team)
members, err := team.GetMembers()
if err != nil {
log.Println("Unable to GetMembers:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during members retrieval."})
return
}
c.JSON(http.StatusOK, members)
})
apiTeamsRoutes.POST("/members", addTeamMember)
apiTeamsRoutes.PUT("/members", setTeamMember)
declareTeamsPasswordRoutes(apiTeamsRoutes)
declareTeamClaimsRoutes(apiTeamsRoutes)
declareTeamCertificateRoutes(apiTeamsRoutes)
// Import teams from cyberrange
router.POST("/cyberrange-teams.json", importTeamsFromCyberrange)
}
func TeamHandler(c *gin.Context) {
tid, err := strconv.ParseInt(string(c.Params.ByName("tid")), 10, 64)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid team identifier"})
return
}
team, err := fic.GetTeam(tid)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Team not found"})
return
}
c.Set("team", team)
c.Next()
}
func TeamPublicHandler(c *gin.Context) {
tid, err := strconv.ParseInt(string(c.Params.ByName("tid")), 10, 64)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid team identifier"})
return
}
if tid != 0 {
team, err := fic.GetTeam(tid)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Team not found"})
return
}
c.Set("team", team)
} else {
c.Set("team", nil)
}
c.Next()
}
func nginxGenTeams(c *gin.Context) {
teams, err := fic.GetTeams()
if err != nil {
log.Println("Unable to GetTeams:", err.Error())
c.AbortWithError(http.StatusInternalServerError, err)
return
}
ret := ""
for _, team := range teams {
ret += fmt.Sprintf(" if ($remote_user = \"%s\") { set $team \"%d\"; }\n", strings.ToLower(team.Name), team.Id)
}
c.String(http.StatusOK, ret)
}
func nginxGenMember(c *gin.Context) {
teams, err := fic.GetTeams()
if err != nil {
log.Println("Unable to GetTeams:", err.Error())
c.AbortWithError(http.StatusInternalServerError, err)
return
}
ret := ""
for _, team := range teams {
if members, err := team.GetMembers(); err == nil {
for _, member := range members {
ret += fmt.Sprintf(" if ($remote_user = \"%s\") { set $team \"%d\"; }\n", member.Nickname, team.Id)
}
} else {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
}
c.String(http.StatusOK, ret)
}
func bindingTeams(c *gin.Context) {
teams, err := fic.GetTeams()
if err != nil {
log.Println("Unable to GetTeams:", err.Error())
c.AbortWithError(http.StatusInternalServerError, err)
return
}
ret := ""
for _, team := range teams {
if members, err := team.GetMembers(); err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
} else {
var mbs []string
for _, member := range members {
mbs = append(mbs, fmt.Sprintf("%s %s", member.Firstname, member.Lastname))
}
ret += fmt.Sprintf("%d;%s;%s\n", team.Id, team.Name, strings.Join(mbs, ";"))
}
}
c.String(http.StatusOK, ret)
}
type teamAssociation struct {
Association string `json:"association"`
TeamId int64 `json:"team_id"`
}
func allAssociations(c *gin.Context) {
teams, err := fic.GetTeams()
if err != nil {
log.Println("Unable to GetTeams:", err.Error())
c.AbortWithError(http.StatusInternalServerError, err)
return
}
var ret []teamAssociation
for _, team := range teams {
assocs, err := pki.GetTeamAssociations(TeamsDir, team.Id)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
for _, a := range assocs {
ret = append(ret, teamAssociation{a, team.Id})
}
}
c.JSON(http.StatusOK, ret)
}
func importTeamsFromCyberrange(c *gin.Context) {
file, err := c.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"errmsg": "Failed to get file: " + err.Error()})
return
}
src, err := file.Open()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"errmsg": "Failed to open file: " + err.Error()})
return
}
defer src.Close()
var ut []fic.CyberrangeTeamBase
err = json.NewDecoder(src).Decode(&fic.CyberrangeAPIResponse{Data: &ut})
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
teams, err := fic.GetTeams()
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Impossible de récupérer la liste des équipes actuelles: %s", err.Error())})
return
}
for _, crteam := range ut {
var exist_team *fic.Team
for _, team := range teams {
if team.Name == crteam.Name || team.ExternalId == crteam.UUID {
exist_team = team
break
}
}
if exist_team != nil {
exist_team.Name = crteam.Name
exist_team.ExternalId = crteam.UUID
_, err = exist_team.Update()
} else {
exist_team, err = fic.CreateTeam(crteam.Name, fic.RandomColor().ToRGB(), crteam.UUID)
}
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Impossible d'ajouter/de modifier l'équipe %v: %s", crteam, err.Error())})
return
}
// Import members
if c.DefaultQuery("nomembers", "0") != "" && len(crteam.Members) > 0 {
exist_team.ClearMembers()
for _, member := range crteam.Members {
_, err = exist_team.AddMember(member.Name, "", member.Nickname, exist_team.Name)
if err != nil {
log.Printf("Unable to add member %q to team %s (tid=%d): %s", member.UUID, exist_team.Name, exist_team.Id, err.Error())
}
}
}
}
teams, err = fic.GetTeams()
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Impossible de récupérer la liste des équipes après import: %s", err.Error())})
return
}
c.JSON(http.StatusOK, teams)
}
func createTeam(c *gin.Context) {
var ut fic.Team
err := c.ShouldBindJSON(&ut)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
if ut.Color == 0 {
ut.Color = fic.RandomColor().ToRGB()
}
team, err := fic.CreateTeam(strings.TrimSpace(ut.Name), ut.Color, ut.ExternalId)
if err != nil {
log.Println("Unable to CreateTeam:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during team creation."})
return
}
c.JSON(http.StatusOK, team)
}
func updateTeam(c *gin.Context) {
team := c.MustGet("team").(*fic.Team)
var ut fic.Team
err := c.ShouldBindJSON(&ut)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
ut.Id = team.Id
if ut.Password != nil && *ut.Password == "" {
ut.Password = nil
}
_, err = ut.Update()
if err != nil {
log.Println("Unable to updateTeam:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during team updating."})
return
}
c.JSON(http.StatusOK, ut)
}
func refineTeamsColors(c *gin.Context) {
teams, err := fic.GetTeams()
if err != nil {
log.Println("Unable to GetTeams:", err.Error())
c.AbortWithError(http.StatusInternalServerError, err)
return
}
for i, team := range teams {
team.Color = fic.HSL{
H: float64(i)/float64(len(teams)) - 0.2,
S: float64(1) / float64(1+i%2),
L: 0.25 + float64(0.5)/float64(1+i%3),
}.ToRGB()
_, err = team.Update()
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
}
c.JSON(http.StatusOK, teams)
}
func disableInactiveTeams(c *gin.Context) {
teams, err := fic.GetTeams()
if err != nil {
log.Println("Unable to GetTeams:", err.Error())
c.AbortWithError(http.StatusInternalServerError, err)
return
}
for _, team := range teams {
var serials []uint64
serials, err = pki.GetTeamSerials(TeamsDir, team.Id)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
var assocs []string
assocs, err = pki.GetTeamAssociations(TeamsDir, team.Id)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
if len(serials) == 0 && len(assocs) == 0 {
if team.Active {
team.Active = false
team.Update()
}
} else if !team.Active {
team.Active = true
team.Update()
}
}
c.JSON(http.StatusOK, true)
}
func enableAllTeams(c *gin.Context) {
teams, err := fic.GetTeams()
if err != nil {
log.Println("Unable to GetTeams:", err.Error())
c.AbortWithError(http.StatusInternalServerError, err)
return
}
for _, team := range teams {
if !team.Active {
team.Active = true
team.Update()
}
}
c.JSON(http.StatusOK, true)
}
func deleteTeam(c *gin.Context) {
team := c.MustGet("team").(*fic.Team)
assocs, err := pki.GetTeamAssociations(TeamsDir, team.Id)
if err != nil {
log.Printf("Unable to GetTeamAssociations(tid=%d): %s", team.Id, err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to retrieve team association."})
return
}
for _, assoc := range assocs {
err = pki.DeleteTeamAssociation(TeamsDir, assoc)
if err != nil {
log.Printf("Unable to DeleteTeamAssociation(assoc=%s): %s", assoc, err.Error())
return
}
}
_, err = team.Delete()
if err != nil {
log.Println("Unable to deleteTeam:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during team deletion."})
return
}
c.JSON(http.StatusOK, true)
}
func addTeamMember(c *gin.Context) {
team := c.MustGet("team").(*fic.Team)
var members []fic.Member
err := c.ShouldBindJSON(&members)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
for _, member := range members {
_, err := team.AddMember(strings.TrimSpace(member.Firstname), strings.TrimSpace(member.Lastname), strings.TrimSpace(member.Nickname), strings.TrimSpace(member.Company))
if err != nil {
log.Println("Unable to AddMember:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during member creation."})
return
}
}
mmbrs, err := team.GetMembers()
if err != nil {
log.Println("Unable to retrieve members list:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieve members list."})
return
}
c.JSON(http.StatusOK, mmbrs)
}
func setTeamMember(c *gin.Context) {
team := c.MustGet("team").(*fic.Team)
team.ClearMembers()
addTeamMember(c)
}
type uploadedHistory struct {
Kind string
Time time.Time
Primary *int64
Secondary *int64
Coefficient float32
}
func updateHistory(c *gin.Context) {
team := c.MustGet("team").(*fic.Team)
var uh uploadedHistory
err := c.ShouldBindJSON(&uh)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
var givenId int64
if uh.Secondary != nil {
givenId = *uh.Secondary
} else if uh.Primary != nil {
givenId = *uh.Primary
}
_, err = team.UpdateHistoryCoeff(uh.Kind, uh.Time, givenId, uh.Coefficient)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to update this history line: %s", err.Error())})
return
}
c.JSON(http.StatusOK, true)
}
func delHistory(c *gin.Context) {
team := c.MustGet("team").(*fic.Team)
var uh uploadedHistory
err := c.ShouldBindJSON(&uh)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
_, err = team.DelHistoryItem(uh.Kind, uh.Time, uh.Primary, uh.Secondary)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to delete this history line: %s", err.Error())})
return
}
c.JSON(http.StatusOK, true)
}

376
admin/api/theme.go Normal file
View file

@ -0,0 +1,376 @@
package api
import (
"fmt"
"log"
"net/http"
"path"
"reflect"
"strconv"
"strings"
"srs.epita.fr/fic-server/admin/sync"
"srs.epita.fr/fic-server/libfic"
"srs.epita.fr/fic-server/settings"
"github.com/gin-gonic/gin"
)
func declareThemesRoutes(router *gin.RouterGroup) {
router.GET("/themes", listThemes)
router.POST("/themes", createTheme)
router.GET("/themes.json", exportThemes)
router.GET("/session-forensic.yaml", func(c *gin.Context) {
if s, err := settings.ReadSettings(path.Join(settings.SettingsDir, settings.SettingsFile)); err != nil {
log.Printf("Unable to ReadSettings: %s", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during settings reading."})
return
} else if challengeinfo, err := sync.GetFileContent(sync.GlobalImporter, "challenge.json"); err != nil {
log.Println("Unable to retrieve challenge.json:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to retrive challenge.json: %s", err.Error())})
return
} else if ch, err := settings.ReadChallengeInfo(challengeinfo); err != nil {
log.Printf("Unable to ReadChallengeInfo: %s", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during challenge info reading."})
return
} else if sf, err := fic.GenZQDSSessionFile(ch, s); err != nil {
log.Printf("Unable to GenZQDSSessionFile: %s", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during session file generation."})
return
} else {
c.JSON(http.StatusOK, sf)
}
})
router.GET("/files-bindings", bindingFiles)
apiThemesRoutes := router.Group("/themes/:thid")
apiThemesRoutes.Use(ThemeHandler)
apiThemesRoutes.GET("", showTheme)
apiThemesRoutes.PUT("", updateTheme)
apiThemesRoutes.DELETE("", deleteTheme)
apiThemesRoutes.POST("/diff-sync", APIDiffThemeWithRemote)
apiThemesRoutes.GET("/exercices_stats.json", getThemedExercicesStats)
declareExercicesRoutes(apiThemesRoutes)
// Remote
router.GET("/remote/themes", sync.ApiListRemoteThemes)
router.GET("/remote/themes/:thid", sync.ApiGetRemoteTheme)
router.GET("/remote/themes/:thid/exercices", sync.ApiListRemoteExercices)
}
type Theme struct {
*fic.Theme
ForgeLink string `json:"forge_link,omitempty"`
}
func ThemeHandler(c *gin.Context) {
thid, err := strconv.ParseInt(string(c.Params.ByName("thid")), 10, 64)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid theme identifier"})
return
}
if thid == 0 {
c.Set("theme", &fic.StandaloneExercicesTheme)
} else {
theme, err := fic.GetTheme(thid)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Theme not found"})
return
}
c.Set("theme", theme)
}
c.Next()
}
func fixAllURLIds(c *gin.Context) {
nbFix := 0
if themes, err := fic.GetThemes(); err == nil {
for _, theme := range themes {
if theme.FixURLId() {
theme.Update()
nbFix += 1
}
if exercices, err := theme.GetExercices(); err == nil {
for _, exercice := range exercices {
if exercice.FixURLId() {
exercice.Update()
nbFix += 1
}
}
}
}
}
c.JSON(http.StatusOK, nbFix)
}
func bindingFiles(c *gin.Context) {
files, err := fic.GetFiles()
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
ret := ""
for _, file := range files {
ret += fmt.Sprintf("%s;%s\n", file.GetOrigin(), file.Path)
}
c.String(http.StatusOK, ret)
}
func listThemes(c *gin.Context) {
themes, err := fic.GetThemes()
if err != nil {
log.Println("Unable to listThemes:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to list themes."})
return
}
if has, _ := fic.HasStandaloneExercice(); has {
themes = append([]*fic.Theme{&fic.StandaloneExercicesTheme}, themes...)
}
c.JSON(http.StatusOK, themes)
}
func exportThemes(c *gin.Context) {
themes, err := fic.ExportThemes()
if err != nil {
log.Println("Unable to exportthemes:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to export themes."})
return
}
c.JSON(http.StatusOK, themes)
}
func showTheme(c *gin.Context) {
theme := c.MustGet("theme").(*fic.Theme)
var forgelink string
if fli, ok := sync.GlobalImporter.(sync.ForgeLinkedImporter); ok {
if u, _ := fli.GetThemeLink(theme); u != nil {
forgelink = u.String()
}
}
c.JSON(http.StatusOK, Theme{theme, forgelink})
}
func createTheme(c *gin.Context) {
var ut fic.Theme
err := c.ShouldBindJSON(&ut)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
if len(ut.Name) == 0 {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Theme's name not filled"})
return
}
th, err := fic.CreateTheme(&ut)
if err != nil {
log.Println("Unable to createTheme:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during theme creation."})
return
}
c.JSON(http.StatusOK, th)
}
func updateTheme(c *gin.Context) {
theme := c.MustGet("theme").(*fic.Theme)
var ut fic.Theme
err := c.ShouldBindJSON(&ut)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
ut.Id = theme.Id
if len(ut.Name) == 0 {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Theme's name not filled"})
return
}
if _, err := ut.Update(); err != nil {
log.Println("Unable to updateTheme:", err.Error())
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "An error occurs during theme update."})
return
}
if theme.Locked != ut.Locked {
exercices, err := theme.GetExercices()
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
for _, exercice := range exercices {
if exercice.Disabled != ut.Locked {
exercice.Disabled = ut.Locked
_, err = exercice.Update()
if err != nil {
log.Println("Unable to enable/disable exercice: ", exercice.Id, err.Error())
}
}
}
}
c.JSON(http.StatusOK, ut)
}
func deleteTheme(c *gin.Context) {
theme := c.MustGet("theme").(*fic.Theme)
_, err := theme.Delete()
if err != nil {
log.Println("Unable to deleteTheme:", err.Error())
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "An error occurs during theme deletion."})
return
}
c.JSON(http.StatusOK, true)
}
func getThemedExercicesStats(c *gin.Context) {
theme := c.MustGet("theme").(*fic.Theme)
exercices, err := theme.GetExercices()
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to fetch exercices: %s", err.Error())})
return
}
ret := []exerciceStats{}
for _, e := range exercices {
ret = append(ret, exerciceStats{
IdExercice: e.Id,
TeamTries: e.TriedTeamCount(),
TotalTries: e.TriedCount(),
SolvedCount: e.SolvedCount(),
FlagSolved: e.FlagSolved(),
MCQSolved: e.MCQSolved(),
})
}
c.JSON(http.StatusOK, ret)
}
func diffThemeWithRemote(theme *fic.Theme) ([]syncDiff, error) {
var diffs []syncDiff
// Compare theme attributes
theme_remote, err := sync.GetRemoteTheme(theme.Path)
if err != nil {
return nil, err
}
for _, field := range reflect.VisibleFields(reflect.TypeOf(*theme)) {
if ((field.Name == "Image") && path.Base(reflect.ValueOf(*theme_remote).FieldByName(field.Name).String()) != path.Base(reflect.ValueOf(*theme).FieldByName(field.Name).String())) || (field.Name != "Image" && !reflect.ValueOf(*theme_remote).FieldByName(field.Name).Equal(reflect.ValueOf(*theme).FieldByName(field.Name))) {
if !field.IsExported() || field.Name == "Id" || field.Name == "IdTheme" || field.Name == "IssueKind" || field.Name == "BackgroundColor" {
continue
}
diffs = append(diffs, syncDiff{
Field: field.Name,
Link: fmt.Sprintf("themes/%d", theme.Id),
Before: reflect.ValueOf(*theme).FieldByName(field.Name).Interface(),
After: reflect.ValueOf(*theme_remote).FieldByName(field.Name).Interface(),
})
}
}
// Compare exercices list
exercices, err := theme.GetExercices()
if err != nil {
return nil, fmt.Errorf("Unable to GetExercices: %w", err)
}
exercices_remote, err := sync.ListRemoteExercices(theme.Path)
if err != nil {
return nil, fmt.Errorf("Unable to ListRemoteExercices: %w", err)
}
var not_found []string
var extra_found []string
for _, exercice_remote := range exercices_remote {
found := false
for _, exercice := range exercices {
if exercice.Path[strings.Index(exercice.Path, "/")+1:] == exercice_remote {
found = true
break
}
}
if !found {
not_found = append(not_found, exercice_remote)
}
}
for _, exercice := range exercices {
found := false
for _, exercice_remote := range exercices_remote {
if exercice.Path[strings.Index(exercice.Path, "/")+1:] == exercice_remote {
found = true
break
}
}
if !found {
extra_found = append(extra_found, exercice.Path[strings.Index(exercice.Path, "/")+1:])
}
}
if len(not_found) > 0 || len(extra_found) > 0 {
diffs = append(diffs, syncDiff{
Field: "theme.Exercices",
Link: fmt.Sprintf("themes/%d", theme.Id),
Before: strings.Join(extra_found, ", "),
After: strings.Join(not_found, ", "),
})
}
// Compare inner exercices
for i, exercice := range exercices {
exdiffs, err := diffExerciceWithRemote(exercice, theme)
if err != nil {
return nil, fmt.Errorf("Unable to diffExerciceWithRemote: %w", err)
}
for _, exdiff := range exdiffs {
if theme.Id == 0 {
exdiff.Field = fmt.Sprintf("exercices[%d].%s", exercice.Id, exdiff.Field)
} else {
exdiff.Field = fmt.Sprintf("exercices[%d].%s", i, exdiff.Field)
}
diffs = append(diffs, exdiff)
}
}
return diffs, err
}
func APIDiffThemeWithRemote(c *gin.Context) {
theme := c.MustGet("theme").(*fic.Theme)
diffs, err := diffThemeWithRemote(theme)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.JSON(http.StatusOK, diffs)
}

15
admin/api/version.go Normal file
View file

@ -0,0 +1,15 @@
package api
import (
"net/http"
"github.com/gin-gonic/gin"
)
func DeclareVersionRoutes(router *gin.RouterGroup) {
router.GET("/version", showVersion)
}
func showVersion(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"version": 1.0})
}

View file

@ -1,32 +0,0 @@
package main
import (
"io/ioutil"
"os"
"srs.epita.fr/fic-server/libfic"
)
func CertificateAPI(team fic.Team, args []string) (interface{}, error) {
if len(args) == 1 {
if args[0] == "generate" {
return team.GenerateCert(), nil
} else if args[0] == "revoke" {
return team.RevokeCert(), nil
} else {
return nil, nil
}
} else if fd, err := os.Open("../PKI/pkcs/" + team.Name + ".p12"); err == nil {
return ioutil.ReadAll(fd)
} else {
return nil, err
}
}
var ApiCARouting = map[string]DispatchFunction{
"GET": genCA,
}
func genCA(args []string, body []byte) (interface{}, error) {
return fic.GenerateCA(), nil
}

View file

@ -1,17 +0,0 @@
package main
import (
"srs.epita.fr/fic-server/libfic"
)
var ApiEventsRouting = map[string]DispatchFunction{
"GET": getEvents,
}
func getEvents(args []string, body []byte) (interface{}, error) {
if evts, err := fic.GetEvents(); err != nil {
return nil, err
} else {
return evts, nil
}
}

View file

@ -1,144 +0,0 @@
package main
import (
"encoding/json"
"errors"
"strconv"
"srs.epita.fr/fic-server/libfic"
)
var ApiExercicesRouting = map[string]DispatchFunction{
"GET": listExercice,
"PATCH": updateExercice,
"DELETE": deletionExercice,
}
func listExercice(args []string, body []byte) (interface{}, error) {
if len(args) == 1 {
if eid, err := strconv.Atoi(string(args[0])); err != nil {
return nil, err
} else {
return fic.GetExercice(int64(eid))
}
} else {
// List all exercices
return fic.GetExercices()
}
}
func deletionExercice(args []string, body []byte) (interface{}, error) {
if len(args) == 1 {
if eid, err := strconv.Atoi(string(args[0])); err != nil {
return nil, err
} else if exercice, err := fic.GetExercice(int64(eid)); err != nil {
return nil, err
} else {
return exercice.Delete()
}
} else {
return nil, nil
}
}
type uploadedExercice struct {
Title string
Statement string
Hint string
Depend *int64
Gain int
VideoURI string
}
func updateExercice(args []string, body []byte) (interface{}, error) {
if len(args) == 1 {
if eid, err := strconv.Atoi(string(args[0])); err != nil {
return nil, err
} else if exercice, err := fic.GetExercice(int64(eid)); err != nil {
return nil, err
} else {
// Update an exercice
var ue uploadedExercice
if err := json.Unmarshal(body, &ue); err != nil {
return nil, err
}
if len(ue.Title) == 0 {
return nil, errors.New("Exercice's title not filled")
}
if ue.Depend != nil {
if _, err := fic.GetExercice(*ue.Depend); err != nil {
return nil, err
}
}
exercice.Title = ue.Title
exercice.Statement = ue.Statement
exercice.Hint = ue.Hint
exercice.Depend = ue.Depend
exercice.Gain = int64(ue.Gain)
exercice.VideoURI = ue.VideoURI
return exercice.Update()
}
} else {
return nil, nil
}
}
func createExercice(theme fic.Theme, args []string, body []byte) (interface{}, error) {
if len(args) >= 1 {
if eid, err := strconv.Atoi(args[0]); err != nil {
return nil, err
} else if exercice, err := theme.GetExercice(eid); err != nil {
return nil, err
} else {
if args[1] == "files" {
return createExerciceFile(theme, exercice, args[2:], body)
} else if args[1] == "keys" {
return createExerciceKey(theme, exercice, args[2:], body)
}
}
return nil, nil
} else {
// Create a new exercice
var ue uploadedExercice
if err := json.Unmarshal(body, &ue); err != nil {
return nil, err
}
if len(ue.Title) == 0 {
return nil, errors.New("Title not filled")
}
var depend *fic.Exercice = nil
if ue.Depend != nil {
if d, err := fic.GetExercice(*ue.Depend); err != nil {
return nil, err
} else {
depend = &d
}
}
return theme.AddExercice(ue.Title, ue.Statement, ue.Hint, depend, ue.Gain, ue.VideoURI)
}
}
type uploadedKey struct {
Name string
Key string
}
func createExerciceKey(theme fic.Theme, exercice fic.Exercice, args []string, body []byte) (interface{}, error) {
var uk uploadedKey
if err := json.Unmarshal(body, &uk); err != nil {
return nil, err
}
if len(uk.Key) == 0 {
return nil, errors.New("Key not filled")
}
return exercice.AddRawKey(uk.Name, uk.Key)
}

View file

@ -1,85 +0,0 @@
package main
import (
"bufio"
"crypto/sha512"
"encoding/base32"
"encoding/json"
"errors"
"log"
"net/http"
"os"
"path"
"strings"
"srs.epita.fr/fic-server/libfic"
)
func createExerciceFile(theme fic.Theme, exercice fic.Exercice, args []string, body []byte) (interface{}, error) {
var uf map[string]string
if err := json.Unmarshal(body, &uf); err != nil {
return nil, err
}
var hash [sha512.Size]byte
var logStr string
var fromURI string
var getFile func(string) error
if URI, ok := uf["URI"]; ok {
hash = sha512.Sum512([]byte(URI))
logStr = "Import file from Cloud: " + URI + " =>"
fromURI = URI
getFile = func(dest string) error { return getCloudFile(URI, dest) }
} else if path, ok := uf["path"]; ok {
hash = sha512.Sum512([]byte(path))
logStr = "Import file from local FS: " + path + " =>"
fromURI = path
getFile = func(dest string) error { return os.Symlink(path, dest) }
} else {
return nil, errors.New("URI or path not filled")
}
pathname := path.Join(fic.FilesDir, strings.ToLower(base32.StdEncoding.EncodeToString(hash[:])), path.Base(fromURI))
if _, err := os.Stat(pathname); os.IsNotExist(err) {
log.Println(logStr, pathname)
if err := os.MkdirAll(path.Dir(pathname), 0777); err != nil {
return nil, err
} else if err := getFile(pathname); err != nil {
return nil, err
}
}
return exercice.ImportFile(pathname, fromURI)
}
func getCloudFile(pathname string, dest string) error {
client := http.Client{}
if req, err := http.NewRequest("GET", CloudDAVBase+pathname, nil); err != nil {
return err
} else {
req.SetBasicAuth(CloudUsername, CloudPassword)
if resp, err := client.Do(req); err != nil {
return err
} else {
defer resp.Body.Close()
if fd, err := os.Create(dest); err != nil {
return err
} else {
defer fd.Close()
if resp.StatusCode != http.StatusOK {
return errors.New(resp.Status)
} else {
writer := bufio.NewWriter(fd)
reader := bufio.NewReader(resp.Body)
reader.WriteTo(writer)
writer.Flush()
}
}
}
}
return nil
}

View file

@ -1,43 +0,0 @@
package main
import (
"fmt"
"srs.epita.fr/fic-server/libfic"
)
type statsTheme struct {
SolvedByLevel []int `json:"solvedByLevel"`
}
type stats struct {
Themes map[string]statsTheme `json:"themes"`
TryRank []int64 `json:"tryRank"`
}
func genStats() (interface{}, error) {
ret := map[string]statsTheme{}
if themes, err := fic.GetThemes(); err != nil {
return nil, err
} else {
for _, theme := range themes {
if exercices, err := theme.GetExercices(); err != nil {
return nil, err
} else {
exos := map[string]fic.ExportedExercice{}
for _, exercice := range exercices {
exos[fmt.Sprintf("%d", exercice.Id)] = fic.ExportedExercice{
exercice.Title,
exercice.Gain,
exercice.SolvedCount(),
exercice.TriedTeamCount(),
}
}
ret[fmt.Sprintf("%d", theme.Id)] = statsTheme{}
}
}
return ret, nil
}
}

View file

@ -1,231 +0,0 @@
package main
import (
"encoding/json"
"fmt"
"strconv"
"strings"
"srs.epita.fr/fic-server/libfic"
)
var ApiTeamsRouting = map[string]DispatchFunction{
"GET": listTeam,
"PUT": creationTeamMembers,
"POST": creationTeam,
"DELETE": deletionTeam,
}
func nginxGenMember() (string, error) {
if teams, err := fic.GetTeams(); err != nil {
return "", err
} else {
ret := ""
for _, team := range teams {
if members, err := team.GetMembers(); err == nil {
for _, member := range members {
ret += fmt.Sprintf(" if ($remote_user = \"%s\") { set $team \"%s\"; }\n", member.Nickname, team.InitialName)
}
} else {
return "", err
}
}
return ret, nil
}
}
func nginxGenTeam() (string, error) {
if teams, err := fic.GetTeams(); err != nil {
return "", err
} else {
ret := ""
for _, team := range teams {
ret += fmt.Sprintf(" if ($ssl_client_s_dn ~ \"/C=FR/ST=France/O=Epita/OU=SRS/CN=%s\") { set $team \"%s\"; }\n", team.InitialName, team.InitialName)
}
return ret, nil
}
}
func bindingTeams() (string, error) {
if teams, err := fic.GetTeams(); err != nil {
return "", err
} else {
ret := ""
for _, team := range teams {
if members, err := team.GetMembers(); err != nil {
return "", err
} else {
var mbs []string
for _, member := range members {
mbs = append(mbs, fmt.Sprintf("%s %s", member.Firstname, member.Lastname))
}
ret += fmt.Sprintf("%d;%s;%s\n", team.Id, team.Name, strings.Join(mbs, ";"))
}
}
return ret, nil
}
}
type uploadedTeam struct {
Name string
Color uint32
}
type uploadedMember struct {
Firstname string
Lastname string
Nickname string
Company string
}
func listTeam(args []string, body []byte) (interface{}, error) {
if len(args) >= 2 {
var team *fic.Team
if tid, err := strconv.Atoi(args[0]); err != nil {
if t, err := fic.GetTeamByInitialName(args[0]); err != nil {
return nil, err
} else {
team = &t
}
} else {
if tid == 0 {
team = nil
} else if t, err := fic.GetTeam(tid); err != nil {
return nil, err
} else {
team = &t
}
}
if args[1] == "my.json" {
return fic.MyJSONTeam(team, true)
} else if args[1] == "wait.json" {
return fic.MyJSONTeam(team, false)
} else if args[1] == "stats.json" {
if team != nil {
return team.GetStats()
} else {
return fic.GetTeamsStats(nil)
}
} else if args[1] == "tries" {
return fic.GetTries(team, nil)
} else if team != nil && args[1] == "members" {
return team.GetMembers()
} else if args[1] == "certificate" && team != nil {
return CertificateAPI(*team, args[2:])
} else if team != nil && args[1] == "name" {
return team.Name, nil
}
} else if len(args) == 1 {
if args[0] == "teams.json" {
return fic.ExportTeams()
} else if args[0] == "tries" {
return fic.GetTries(nil, nil)
} else if args[0] == "nginx" {
return nginxGenTeam()
} else if args[0] == "nginx-members" {
return nginxGenMember()
} else if args[0] == "binding" {
return bindingTeams()
} else if tid, err := strconv.Atoi(string(args[0])); err != nil {
return fic.GetTeamByInitialName(args[0])
} else if team, err := fic.GetTeam(tid); err != nil {
return nil, err
} else {
return team, nil
}
} else if len(args) == 0 {
// List all teams
return fic.GetTeams()
}
return nil, nil
}
func creationTeam(args []string, body []byte) (interface{}, error) {
if len(args) == 1 {
// List given team
if tid, err := strconv.Atoi(string(args[0])); err != nil {
return nil, err
} else if team, err := fic.GetTeam(tid); err != nil {
return nil, err
} else {
var members []uploadedMember
if err := json.Unmarshal(body, &members); err != nil {
return nil, err
}
for _, member := range members {
team.AddMember(member.Firstname, member.Lastname, member.Nickname, member.Company)
}
return team.GetMembers()
}
} else if len(args) == 0 {
// Create a new team
var ut uploadedTeam
if err := json.Unmarshal(body, &ut); err != nil {
return nil, err
}
return fic.CreateTeam(ut.Name, ut.Color)
} else {
return nil, nil
}
}
func creationTeamMembers(args []string, body []byte) (interface{}, error) {
if len(args) == 1 {
// List given team
if tid, err := strconv.Atoi(string(args[0])); err != nil {
return nil, err
} else if team, err := fic.GetTeam(tid); err != nil {
return nil, err
} else {
var member uploadedMember
if err := json.Unmarshal(body, &member); err != nil {
return nil, err
}
team.AddMember(member.Firstname, member.Lastname, member.Nickname, member.Company)
return team.GetMembers()
}
} else if len(args) == 0 {
// Create a new team
var members []uploadedMember
if err := json.Unmarshal(body, &members); err != nil {
return nil, err
}
if team, err := fic.CreateTeam("", 0); err != nil {
return nil, err
} else {
for _, member := range members {
if _, err := team.AddMember(member.Firstname, member.Lastname, member.Nickname, member.Company); err != nil {
return nil, err
}
}
return team, nil
}
} else {
return nil, nil
}
}
func deletionTeam(args []string, body []byte) (interface{}, error) {
if len(args) == 1 {
if tid, err := strconv.Atoi(string(args[0])); err != nil {
return nil, err
} else if team, err := fic.GetTeam(tid); err != nil {
return nil, err
} else {
return team.Delete()
}
} else {
return nil, nil
}
}

View file

@ -1,164 +0,0 @@
package main
import (
"encoding/json"
"errors"
"fmt"
"strconv"
"srs.epita.fr/fic-server/libfic"
)
var ApiThemesRouting = map[string]DispatchFunction{
"GET": listTheme,
"PATCH": updateTheme,
"POST": creationTheme,
"DELETE": deletionTheme,
}
func bindingFiles() (string, error) {
if files, err := fic.GetFiles(); err != nil {
return "", err
} else {
ret := ""
for _, file := range files {
ret += fmt.Sprintf("%s;%s\n", file.GetOrigin(), file.Path)
}
return ret, nil
}
}
func getTheme(args []string) (fic.Theme, error) {
if tid, err := strconv.Atoi(string(args[0])); err != nil {
return fic.Theme{}, err
} else {
return fic.GetTheme(tid)
}
}
func getExercice(args []string) (fic.Exercice, error) {
if theme, err := getTheme(args); err != nil {
return fic.Exercice{}, err
} else if eid, err := strconv.Atoi(string(args[1])); err != nil {
return fic.Exercice{}, err
} else {
return theme.GetExercice(eid)
}
}
func listTheme(args []string, body []byte) (interface{}, error) {
if len(args) == 3 {
if e, err := getExercice(args); err != nil {
return nil, err
} else {
if args[2] == "files" {
return e.GetFiles()
} else if args[2] == "keys" {
return e.GetKeys()
}
}
} else if len(args) == 2 {
if args[1] == "exercices" {
if theme, err := getTheme(args); err != nil {
return nil, err
} else {
return theme.GetExercices()
}
} else {
return getExercice(args)
}
} else if len(args) == 1 {
if args[0] == "files-bindings" {
return bindingFiles()
} else if args[0] == "themes.json" {
return fic.ExportThemes()
} else {
return getTheme(args)
}
} else if len(args) == 0 {
// List all themes
return fic.GetThemes()
}
return nil, nil
}
type uploadedTheme struct {
Name string
Authors string
}
func creationTheme(args []string, body []byte) (interface{}, error) {
if len(args) >= 1 {
if theme, err := getTheme(args); err != nil {
return nil, err
} else {
return createExercice(theme, args[1:], body)
}
} else if len(args) == 0 {
// Create a new theme
var ut uploadedTheme
if err := json.Unmarshal(body, &ut); err != nil {
return nil, err
}
if len(ut.Name) == 0 {
return nil, errors.New("Theme's name not filled")
}
return fic.CreateTheme(ut.Name, ut.Authors)
} else {
return nil, nil
}
}
func updateTheme(args []string, body []byte) (interface{}, error) {
if len(args) == 2 {
// Update an exercice
var ue fic.Exercice
if err := json.Unmarshal(body, &ue); err != nil {
return nil, err
}
if len(ue.Title) == 0 {
return nil, errors.New("Exercice's title not filled")
}
if _, err := ue.Update(); err != nil {
return nil, err
}
return ue, nil
} else if len(args) == 1 {
// Update a theme
var ut fic.Theme
if err := json.Unmarshal(body, &ut); err != nil {
return nil, err
}
if len(ut.Name) == 0 {
return nil, errors.New("Theme's name not filled")
}
return ut.Update()
} else {
return nil, nil
}
}
func deletionTheme(args []string, body []byte) (interface{}, error) {
if len(args) == 2 {
if exercice, err := getExercice(args); err != nil {
return nil, err
} else {
return exercice.Delete()
}
} else if len(args) == 1 {
if theme, err := getTheme(args); err != nil {
return nil, err
} else {
return theme.Delete()
}
} else {
return nil, nil
}
}

View file

@ -1,11 +0,0 @@
package main
import ()
var ApiVersionRouting = map[string]DispatchFunction{
"GET": showVersion,
}
func showVersion(args []string, body []byte) (interface{}, error) {
return map[string]interface{}{"version": 0.1}, nil
}

81
admin/app.go Normal file
View file

@ -0,0 +1,81 @@
package main
import (
"context"
"log"
"net/http"
"path/filepath"
"strings"
"time"
"github.com/gin-gonic/gin"
"srs.epita.fr/fic-server/admin/api"
"srs.epita.fr/fic-server/settings"
)
type App struct {
router *gin.Engine
srv *http.Server
cfg *settings.Settings
bind string
}
func NewApp(cfg *settings.Settings, baseURL string, bind string) App {
if !cfg.WorkInProgress {
gin.SetMode(gin.ReleaseMode)
}
gin.ForceConsoleColor()
router := gin.Default()
api.DeclareRoutes(router.Group(""))
var baserouter *gin.RouterGroup
if len(baseURL) > 0 {
router.GET("/", func(c *gin.Context) {
c.Redirect(http.StatusFound, baseURL)
})
router.GET(filepath.Dir(baseURL)+"/files/*_", func(c *gin.Context) {
path := c.Request.URL.Path
c.Redirect(http.StatusFound, filepath.Join(baseURL, strings.TrimPrefix(path, filepath.Dir(baseURL))))
})
baserouter = router.Group(baseURL)
api.DeclareRoutes(baserouter)
declareStaticRoutes(baserouter, cfg, baseURL)
} else {
declareStaticRoutes(router.Group(""), cfg, "")
}
app := App{
router: router,
bind: bind,
}
return app
}
func (app *App) Start() {
app.srv = &http.Server{
Addr: app.bind,
Handler: app.router,
ReadHeaderTimeout: 15 * time.Second,
ReadTimeout: 15 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 30 * time.Second,
}
log.Printf("Ready, listening on %s\n", app.bind)
if err := app.srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s\n", err)
}
}
func (app *App) Stop() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := app.srv.Shutdown(ctx); err != nil {
log.Fatal("Server Shutdown:", err)
}
}

View file

@ -1,159 +0,0 @@
#!/bin/bash
BASEURL="http://localhost:8081/admin"
BASEURI="https://owncloud.srs.epita.fr/remote.php/webdav/FIC 2017"
BASEFILE="/mnt/fic/"
CLOUDPASS=fic:'f>t\nV33R|(+?$i*'
new_theme() {
NAME=`echo $1 | sed 's/"/\\\\"/g'`
AUTHORS=`echo $2 | sed 's/"/\\\\"/g'`
curl -f -s -d "{\"name\": \"$NAME\", \"authors\": \"$AUTHORS\"}" "${BASEURL}/api/themes/" |
grep -Eo '"id":[0-9]+,' | grep -Eo "[0-9]+"
}
new_exercice() {
THEME="$1"
TITLE=`echo "$2" | sed 's/"/\\\\"/g'`
STATEMENT=`echo "$3" | sed 's/"/\\\\"/g' | sed ':a;N;$!ba;s/\n/<br>/g'`
HINT=`echo "$4" | sed 's/"/\\\\"/g' | sed ':a;N;$!ba;s/\n/<br>/g'`
DEPEND="$5"
GAIN="$6"
VIDEO="$7"
curl -f -s -d "{\"title\": \"$TITLE\", \"statement\": \"$STATEMENT\", \"hint\": \"$HINT\", \"depend\": $DEPEND, \"gain\": $GAIN, \"videoURI\": \"$VIDEO\"}" "${BASEURL}/api/themes/$THEME" |
grep -Eo '"id":[0-9]+,' | grep -Eo "[0-9]+"
}
new_file() {
THEME="$1"
EXERCICE="$2"
URI="$3"
# curl -f -s -d "{\"URI\": \"${BASEFILE}${URI}\"}" "${BASEURL}/api/themes/$THEME/$EXERCICE/files" |
curl -f -s -d "{\"path\": \"${BASEFILE}${URI}\"}" "${BASEURL}/api/themes/$THEME/$EXERCICE/files" |
grep -Eo '"id":[0-9]+,' | grep -Eo "[0-9]+"
}
new_key() {
THEME="$1"
EXERCICE="$2"
NAME="$3"
KEY=`echo $4 | sed 's/"/\\\\"/g' | sed 's#\\\\#\\\\\\\\#g'`
curl -f -s -d "{\"name\": \"$NAME\", \"key\": \"$KEY\"}" "${BASEURL}/api/themes/$THEME/$EXERCICE/keys" |
grep -Eo '"id":[0-9]+,' | grep -Eo "[0-9]+"
}
get_dir_from_cloud() {
curl -f -s -X PROPFIND -u "${CLOUDPASS}" "${BASEURI}$1" | xmllint --format - | grep 'd:href' | sed -E 's/^.*>(.*)<.*$/\1/'
}
get_dir() {
ls "${BASEFILE}$1"
}
#alias get_dir=get_dir_from_cloud
get_file_from_cloud() {
curl -f -s -u "${CLOUDPASS}" "${BASEURI}$1" | tr -d '\r'
}
get_file() {
cat "${BASEFILE}$1" | tr -d '\r'
}
#alias get_file=get_file_from_cloud
unhtmlentities() {
cat | sed -E 's/%20/ /g' | sed -E "s/%27/'/g" | sed -E 's/%c3%a9/é/g' | sed -E 's/%c3%a8/è/g'
}
# Theme
get_dir "" | while read f; do basename "$f"; done | while read THEME_URI
do
THM_BASEURI="/${THEME_URI}/"
THEME_NAME=$(echo "${THEME_URI#*-}" | unhtmlentities)
THEME_AUTHORS=$(get_file "${THM_BASEURI}/AUTHORS.txt" | sed 's/$/, /' | tr -d '\n' | sed 's/, $//')
THEME_ID=`new_theme "$THEME_NAME" "$THEME_AUTHORS"`
if [ -z "$THEME_ID" ]; then
echo -e "\e[31;01m!!! An error occured during theme add\e[00m"
continue
else
echo -e "\e[33m>>> New theme created:\e[00m $THEME_ID - $THEME_NAME"
fi
LAST=null
EXO_NUM=0
get_dir "${THM_BASEURI}" | sed 1d | while read f; do basename "$f"; done | while read EXO_URI
do
case ${EXO_URI} in
[0-9]-*)
;;
*)
continue;;
esac
#EXO_NUM=$((EXO_NUM + 1))
EXO_NUM=${EXO_URI%-*}
EXO_NAME=$(echo "${EXO_URI#*-}" | unhtmlentities)
echo
echo -e "\e[36m--- Filling exercice ${EXO_NUM} in theme ${THEME_NAME}\e[00m"
EXO_BASEURI="${EXO_URI}/"
EXO_VIDEO=$(get_dir "${THM_BASEURI}${EXO_BASEURI}/resolution/" | grep -E "\.(mov|mkv|mp4|avi|flv|ogv|webm)$" | while read f; do basename $f; done | tail -1)
if [ "${LAST}" = "null" ]; then
echo ">>> Assuming this exercice has no dependency"
else
echo ">>> Assuming this exercice depends on the last entry (id=${LAST})"
fi
EXO_GAIN=$((3 * (2 ** $EXO_NUM) - 1))
echo ">>> Using default gain: ${EXO_GAIN} points"
EXO_SCENARIO=$(get_file "${THM_BASEURI}${EXO_BASEURI}/scenario.txt")
EXO_HINT=$(get_file "${THM_BASEURI}${EXO_BASEURI}/hint.txt")
EXO_ID=`new_exercice "${THEME_ID}" "${EXO_NAME}" "${EXO_SCENARIO}" "${EXO_HINT}" "${LAST}" "${EXO_GAIN}" "/resolution${THM_BASEURI}${EXO_BASEURI}resolution/${EXO_VIDEO}"`
if [ -z "$EXO_ID" ]; then
echo -e "\e[31;01m!!! An error occured during exercice add.\e[00m"
continue
else
echo -e "\e[32m>>> New exercice created:\e[00m $EXO_ID - $EXO_NAME"
fi
# Keys
get_file "${THM_BASEURI}${EXO_BASEURI}/flags.txt" | while read KEYLINE
do
KEY_NAME=$(echo "$KEYLINE" | cut -d : -f 1)
KEY_RAW=$(echo "$KEYLINE" | cut -d : -f 2-)
if [ -z "${KEY_NAME}" ]; then
KEY_NAME="Flag"
fi
KEY_ID=`new_key "${THEME_ID}" "${EXO_ID}" "${KEY_NAME}" "${KEY_RAW}"`
if [ -z "$KEY_ID" ]; then
echo -e "\e[31;01m!!! An error occured during key import!\e[00m (name=${KEYNAME};raw=${KEY_RAW})"
else
echo -e "\e[32m>>> New key added:\e[00m $KEY_ID - $KEY_NAME"
fi
done
# Files
get_dir "${THM_BASEURI}${EXO_BASEURI}files/" | grep -v DIGESTS.txt | while read f; do basename "$f"; done | while read FILE_URI
do
echo "Import file ${THM_BASEURI}${EXO_BASEURI}files/${FILE_URI}"
FILE_ID=`new_file "${THEME_ID}" "${EXO_ID}" "${THM_BASEURI}${EXO_BASEURI}files/${FILE_URI}"`
if [ -z "$FILE_ID" ]; then
echo -e "\e[31;01m!!! An error occured during file import! Please check path.\e[00m"
else
echo -e "\e[32m>>> New file added:\e[00m $FILE_ID - $FILE_URI"
fi
done
LAST=$EXO_ID
done
echo
done

View file

@ -2,13 +2,14 @@
BASEURL="http://127.0.0.1:8081/admin"
GEN_CERTS=0
GEN_PASSWD=0
EXTRA_TEAMS=0
CSV_SPLITER=","
CSV_COL_LASTNAME=1
CSV_COL_FIRSTNAME=2
CSV_COL_NICKNAME=3
CSV_COL_COMPANY=7
CSV_COL_TEAM=7
CSV_COL_LASTNAME=2
CSV_COL_FIRSTNAME=3
CSV_COL_NICKNAME=5
CSV_COL_COMPANY=6
CSV_COL_TEAM=1
usage() {
echo "$0 [options] csv_file"
@ -16,6 +17,7 @@ usage() {
echo " -S -csv-spliter SEP CSV separator (default: $CSV_SPLITER)"
echo " -e -extra-teams NBS Number of extra teams to generate (default: ${EXTRA_TEAMS})"
echo " -c -generate-certificate Should team certificates be generated? (default: no)"
echo " -p -generate-password Should generate team password to teams.pass? (default: no)"
}
# Parse options
@ -33,6 +35,8 @@ do
shift;;
-c|-generate-certificates)
GEN_CERTS=1;;
-p|-generate-password)
GEN_PASSWD=1;;
*)
echo "Unknown option '$1'"
usage
@ -41,8 +45,7 @@ do
shift
done
[ "$#" -lt 1 ] && { usage; exit 1; }
PART_FILE="$1"
[ "$#" -lt 1 ] && [ "${EXTRA_TEAMS}" -eq 0 ] && { usage; exit 1; }
new_team() {
head -n "$1" team-names.txt | tail -1 | sed -E 's/^.*\|\[\[([^|]+\|)?([^|]+)\]\][^|]*\|([A-Fa-f0-9]{1,2})\|([A-Fa-f0-9]{1,2})\|([A-Fa-f0-9]{1,2})\|([0-9]{1,3})\|([0-9]{1,3})\|([0-9]{1,3})\|.*$/\6 \7 \8 \2/' |
@ -59,7 +62,7 @@ new_team() {
COLOR=$((($R*256 + $G) * 256 + $B))
curl -s -d "{\"name\": \"$N\",\"color\": $COLOR}" "${BASEURL}/api/teams/"
curl -s -d "{\"name\": \"$N\",\"color\": $COLOR}" "${BASEURL}/api/teams"
done | grep -Eo '"id":[0-9]+,' | grep -Eo "[0-9]+"
}
@ -76,10 +79,30 @@ do
if [ "${GEN_CERTS}" -eq 1 ] && ! curl -s -f "${BASEURL}/api/teams/${TID}/certificate" > /dev/null
then
curl -s -f "${BASEURL}/api/teams/${TID}/certificate/generate"
elif [ "${GEN_PASSWD}" -eq 1 ]
then
TEAMID=$(curl -s -f "${BASEURL}/api/teams/${TID}/" | jq -r .name)
PASSWD=$(curl -X POST -s -f "${BASEURL}/api/teams/${TID}/password" | jq -r .password)
NP=$(echo "${TEAMID}" | cut -d : -f 1 | sed 's/[[:upper:]]/\l&/g;s/[âáàä]/a/g;s/[êéèë]/e/g')
cat >> teams.pass <<EOF
${TEAMID}:${PASSWD}
EOF
SALT="$(openssl rand -base64 3)"
HASHED="{SSHA}$(echo -n $PASSWD$SALT | openssl dgst -binary -sha1 | sed 's#$#'"$SALT"'#' | base64)"
cat >> htpasswd.ssha <<EOF
${NP}:${HASHED}
EOF
HASHED="$(echo -n $PASSWD | openssl passwd -apr1 -in -)"
cat >> htpasswd.apr1 <<EOF
${NP}:${HASHED}
EOF
fi
echo
done
[ "$#" -lt 1 ] && exit 0
PART_FILE="$1"
TMAX=`cat "$PART_FILE" | cut -d "${CSV_SPLITER}" -f $CSV_COL_TEAM | sort | uniq | wc -l`
TMAX=$(($TMAX + $TNUM))
cat "$PART_FILE" | cut -d "${CSV_SPLITER}" -f $CSV_COL_TEAM | sort | uniq | while read TEAMID
@ -93,7 +116,7 @@ do
if ! (
echo -n "["
HAS_MEMBER=1
grep "${CSV_SPLITER}${TEAMID}\$" "$PART_FILE" | while read MEMBER
grep "${TEAMID}${CSV_SPLITER}" "$PART_FILE" | while read MEMBER
do
LASTNAME=`echo $MEMBER | cut -d "${CSV_SPLITER}" -f $CSV_COL_LASTNAME | tr -d "\r\n"`
FIRSTNAME=`echo $MEMBER | cut -d "${CSV_SPLITER}" -f $CSV_COL_FIRSTNAME | tr -d "\r\n"`
@ -117,12 +140,28 @@ do
EOF
done
echo "]"
) | curl -f -s -d @- "${BASEURL}/api/teams/${TID}"
) | curl -f -s -d @- "${BASEURL}/api/teams/${TID}/members"
then
echo "An error occured"
elif [ "${GEN_CERTS}" -eq 1 ] && ! curl -s -f "${BASEURL}/api/teams/${TID}/certificate" > /dev/null
then
curl -s -f "${BASEURL}/api/teams/${TID}/certificate/generate"
elif [ "${GEN_PASSWD}" -eq 1 ]
then
PASSWD=$(curl -X POST -s -f "${BASEURL}/api/teams/${TID}/password" | jq -r .password)
NP=$(echo "${TEAMID}" | cut -d : -f 1 | sed 's/[[:upper:]]/\l&/g;s/[âáàä]/a/g;s/[êéèë]/e/g')
cat >> teams.pass <<EOF
${TEAMID}:${PASSWD}
EOF
SALT="$(openssl rand -base64 3)"
HASHED="{SSHA}$(echo -n $PASSWD$SALT | openssl dgst -binary -sha1 | sed 's#$#'"$SALT"'#' | base64)"
cat >> htpasswd.ssha <<EOF
${NP}:${HASHED}
EOF
HASHED="$(echo -n $PASSWD | openssl passwd -apr1 -in -)"
cat >> htpasswd.apr1 <<EOF
${NP}:${HASHED}
EOF
fi
echo
done

15
admin/fill_teams_zqds.sh Executable file
View file

@ -0,0 +1,15 @@
#!/bin/sh
BASEURL="http://127.0.0.1:8081/admin"
EVENTID=6109ae5acbb7b36b789c9330
BASEURL_ZQDS="https://api.well-played.gg"
curl -s -H 'accept: */*' "${BASEURL_ZQDS}/teams?event_id=${EVENTID}&size=100" | jq --compact-output .content[] | while read TEAMOBJ; do
curl -s -d @- "${BASEURL}/api/teams/" <<EOF
{
"name": $(echo "${TEAMOBJ}" | jq .name),
"external_id": $(echo "${TEAMOBJ}" | jq .id)
}
EOF
done

View file

@ -0,0 +1,60 @@
package generation
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"strings"
"srs.epita.fr/fic-server/libfic"
)
var GeneratorSocket string
func doGeneration(uri string, contenttype string, buf io.Reader) (*http.Response, error) {
sockType := "unix"
if strings.Contains(GeneratorSocket, ":") {
sockType = "tcp"
}
socket, err := net.Dial(sockType, GeneratorSocket)
if err != nil {
return nil, err
}
defer socket.Close()
httpClient := &http.Client{
Transport: &http.Transport{
Dial: func(network, addr string) (net.Conn, error) {
return socket, nil
},
},
}
return httpClient.Post("http://localhost"+uri, contenttype, buf)
}
func EnqueueGeneration(gs fic.GenStruct) (*http.Response, error) {
buf, err := json.Marshal(gs)
if err != nil {
return nil, fmt.Errorf("Something is wrong with JSON encoder: %w", err)
}
return doGeneration("/enqueue", "application/json", bytes.NewReader(buf))
}
func PerformGeneration(gs fic.GenStruct) (*http.Response, error) {
buf, err := json.Marshal(gs)
if err != nil {
return nil, fmt.Errorf("Something is wrong with JSON encoder: %w", err)
}
return doGeneration("/perform", "application/json", bytes.NewReader(buf))
}
func FullGeneration() (*http.Response, error) {
return doGeneration("/full", "application/json", nil)
}

View file

@ -0,0 +1,133 @@
package main
import (
"flag"
"log"
"os"
"path"
"path/filepath"
"srs.epita.fr/fic-server/admin/sync"
"srs.epita.fr/fic-server/libfic"
)
func main() {
cloudDAVBase := ""
cloudUsername := "fic"
cloudPassword := ""
localImporterDirectory := ""
// Read paremeters from environment
if v, exists := os.LookupEnv("FICCLOUD_URL"); exists {
cloudDAVBase = v
}
if v, exists := os.LookupEnv("FICCLOUD_USER"); exists {
cloudUsername = v
}
if v, exists := os.LookupEnv("FICCLOUD_PASS"); exists {
cloudPassword = v
}
// Read parameters from command line
flag.StringVar(&localImporterDirectory, "localimport", localImporterDirectory,
"Base directory where to find challenges files to import, local part")
flag.StringVar(&cloudDAVBase, "clouddav", cloudDAVBase,
"Base directory where to find challenges files to import, cloud part")
flag.StringVar(&cloudUsername, "clouduser", cloudUsername, "Username used to sync")
flag.StringVar(&cloudPassword, "cloudpass", cloudPassword, "Password used to sync")
flag.Var(&sync.RemoteFileDomainWhitelist, "remote-file-domain-whitelist", "List of domains which are allowed to store remote files")
flag.Parse()
// Do not display timestamp
log.SetFlags(0)
// Instantiate importer
regenImporter := false
if localImporterDirectory != "" {
sync.GlobalImporter = sync.LocalImporter{Base: localImporterDirectory, Symlink: true}
} else if cloudDAVBase != "" {
sync.GlobalImporter, _ = sync.NewCloudImporter(cloudDAVBase, cloudUsername, cloudPassword)
} else {
// In this case, we want to treat the entier path given
regenImporter = true
}
for _, p := range flag.Args() {
if regenImporter {
var err error
p, err = filepath.Abs(p)
if err != nil {
p = path.Clean(p)
}
sync.GlobalImporter = sync.LocalImporter{
Base: p,
Symlink: true,
}
}
// Find all challenge.toml or challenge.txt
treatDir("")
}
}
func treatDir(p string) {
var expath string
for _, f := range []string{"challenge.toml", "challenge.txt"} {
if sync.GlobalImporter.Exists(path.Join(p, f)) {
expath = p
break
}
}
if expath != "" {
treatExercice(expath)
} else {
files, err := sync.GlobalImporter.ListDir(p)
if err != nil {
log.Printf("Unable to readdir at %s: %s", p, err.Error())
return
}
for _, f := range files {
st, err := sync.GlobalImporter.Stat(path.Join(p, f))
if err == nil && st.IsDir() {
treatDir(path.Join(p, f))
}
}
}
}
func treatExercice(expath string) {
// Load exercice
exercice, _, _, _, _, err := sync.BuildExercice(sync.GlobalImporter, &fic.Theme{}, expath, nil, nil)
if exercice == nil {
log.Printf("Unable to treat exercice %q: %s", expath, err.Error())
return
}
paramsFiles, err := sync.GetExerciceFilesParams(sync.GlobalImporter, exercice)
if err != nil {
log.Printf("Unable to read challenge.toml %q: %s", expath, err.Error())
return
}
for fname, pf := range paramsFiles {
if pf.URL == "" {
continue
}
dest := path.Join(exercice.Path, "files", fname)
log.Printf("Downloading %s...", fname)
if li, ok := sync.GlobalImporter.(sync.LocalImporter); ok {
err = sync.DownloadExerciceFile(paramsFiles[fname], li.GetLocalPath(dest), exercice, false)
} else {
err = sync.DownloadExerciceFile(paramsFiles[fname], dest, exercice, false)
}
if err != nil {
log.Println("DownloadExerciceFile error:", err.Error())
}
}
}

View file

@ -1,23 +0,0 @@
#!/bin/sh
BASEURL="http://localhost:8081"
BASEURI="https://srs.epita.fr/owncloud/remote.php/webdav/FIC 2016"
CLOUDUSER='fic'
CLOUDPASS='f>t\nV33R|(+?$i*'
if [ $# -gt 0 ]
then
WHERE=$1
else
WHERE="files"
fi
curl -q -f ${BASEURL}/api/themes/files-bindings | while read l
do
FROM=$(echo "$l" | cut -d ";" -f 1)
DEST=$(echo "$l" | cut -d ";" -f 2)
mkdir -p $(dirname "${WHERE}${DEST}")
wget -O "${WHERE}${DEST}" --user "${CLOUDUSER}" --password "${CLOUDPASS}" "${BASEURI}${FROM}"
done

View file

@ -4,51 +4,171 @@ const indextpl = `<!DOCTYPE html>
<html ng-app="FICApp">
<head>
<meta charset="utf-8">
<title>Challenge Forensic - Administration</title>
<link href="/css/bootstrap.min.css" rel="stylesheet">
<title>{{ .title }} - Administration</title>
<link href="{{.urlbase}}css/bootstrap.min.css" type="text/css" rel="stylesheet">
<link href="{{.urlbase}}css/glyphicon.css" type="text/css" rel="stylesheet" media="screen">
<style>
.cksum {
overflow-x: hidden;
text-overflow: ellipsis;
max-width: 100%;
display: inline-block;
vertical-align: middle;
word-wrap: normal;
white-space: nowrap;
}
.bg-mfound {
background-color: #7bcfd0 !important;
}
.bg-ffound {
background-color: #7bdfc0 !important;
}
.bg-wchoices {
background-color: #c07bdf !important;
}
.table th.frotated {
border: 0;
}
.table th.rotated {
height: 100px;
width: 40px;
min-width: 40px;
max-width: 40px;
position: relative;
vertical-align: bottom;
padding: 0;
font-size: 12px;
line-height: 0.9;
border: 0;
}
th.rotated > div {
position: relative;
top: 0px;
left: -50px;
height: 100%;
transform: skew(45deg,0deg);
overflow: hidden;
border: 1px solid #000;
}
th.rotated div a {
transform: skew(-45deg,0deg) rotate(45deg);
position: absolute;
bottom: 40px;
left: -35px;
display: inline-block;
width: 110px;
text-align: left;
text-overflow: ellipsis;
}
.col img {
max-width: 100%;
}
.circle-anim {
z-index:1;
border: black 1px solid;
border-radius: .5em;
margin-top: .4em;
margin-left: .5em;
height: 1em;
width: 1em;
transition: transform ease-in .7s;
transform: scale(1);
}
.circle-anim.play {
transform: scale(250);
opacity:0;
}
</style>
<base href="{{.urlbase}}">
<script src="/js/d3.v3.min.js"></script>
<script src="js/d3.v3.min.js"></script>
</head>
<body>
<nav class="navbar navbar-inverse navbar-static-top">
<div class="container">
<body class="bg-light text-dark">
<nav class="navbar sticky-top navbar-expand-lg navbar-dark text-light" ng-class="{'bg-dark': settings.wip, 'bg-danger': !settings.wip}">
<a class="navbar-brand" href=".">
<img alt="{{ .title }}" src="{{ .logo }}" style="height: 30px">
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#adminMenu" aria-controls="adminMenu" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-header">
<a class="navbar-brand" href="{{.urlbase}}">
<img alt="FIC" src="{{.urlbase}}img/fic.png" style="height: 100%">
</a>
</div>
<ul class="nav navbar-nav">
<li><a href="{{.urlbase}}teams">&Eacute;quipes</a></li>
<li><a href="{{.urlbase}}themes">Thèmes</a></li>
<li><a href="{{.urlbase}}exercices">Exercices</a></li>
<li><a href="{{.urlbase}}events">&Eacute;vénements</a></li>
<div class="collapse navbar-collapse" id="adminMenu">
<ul class="navbar-nav mr-auto">
<li class="nav-item" ng-class="{'active': $location.path().startsWith('/teams')}"><a class="nav-link" href="teams">&Eacute;quipes</a></li>
<li class="nav-item" ng-class="{'active': $location.path().startsWith('/auth')}"><a class="nav-link" href="auth">Authentification</a></li>
<li class="nav-item" ng-class="{'active': $location.path().startsWith('/themes')}"><a class="nav-link" href="themes">Thèmes</a></li>
<li class="nav-item" ng-class="{'active': $location.path().startsWith('/exercices')}"><a class="nav-link" href="exercices">Exercices</a></li>
<li class="nav-item" ng-class="{'active': $location.path().startsWith('/public')}"><a class="nav-link" href="public/0">Public</a></li>
<li class="nav-item" ng-class="{'active': $location.path().startsWith('/events')}"><a class="nav-link" href="events">&Eacute;vénements</a></li>
<li class="nav-item" ng-class="{'active': $location.path().startsWith('/claims')}"><a class="nav-link" href="claims" ng-controller="ClaimsTinyListController">
Tâches
<span class="badge badge-{{ "{{ priorities[myClaimsMaxLevel] }}" }}" ng-show="myClaims" title="Tâches qui me sont assignées">{{ "{{ myClaims }}" }}</span>
<span class="badge badge-{{ "{{ priorities[newClaimsMaxLevel] }}" }}" ng-show="newClaims" title="Nouvelles tâches en attente d'attribution">{{ "{{ newClaims }}" }}</span>
</a></li>
<li class="nav-item" ng-class="{'active': $location.path().startsWith('/sync') || $location.path().startsWith('/repositories')}">
<a class="nav-link" href="sync" ng-show="settings.wip">
Synchronisation
</a>
</li>
<li class="nav-item" ng-class="{'active': $location.path().startsWith('/settings')}">
<a class="nav-link" href="settings">
Paramètres
</a>
</li>
<span class="d-flex flex-column justify-content-center" ng-show="!settings.wip" ng-cloak>
<span class="badge badge-light p-1">
prod
</span>
</span>
</ul>
</div>
<p id="clock" class="navbar-text navbar-right" ng-controller="CountdownController">
<span id="clock" class="navbar-text" ng-controller="CountdownController" ng-cloak>
<div style="pointer-events: none; position: absolute;">
<div style="position: absolute;" id="circle1" class="circle-anim border-danger"></div>
<div style="position: absolute;" id="circle2" class="circle-anim border-info"></div>
</div>
<button type="button" class="mr-2 btn btn-sm" ng-class="{'btn-info':staticFilesNeedUpdate,'btn-secondary':!staticFilesNeedUpdate}" ng-click="regenerateStaticFiles()" ng-disabled="staticRegenerationInProgress">
<span class="glyphicon glyphicon-refresh" aria-hidden="true" title="Regénérer les fichiers statiques" ng-show="!staticRegenerationInProgress"></span>
<div class="spinner-border spinner-border-sm" role="status" ng-show="staticRegenerationInProgress">
<span class="sr-only">Loading...</span>
</div>
<span ng-if="staticFilesNeedUpdate"> {{ "{{ staticFilesNeedUpdate }}" }}</span>
</button>
<span ng-show="startIn > 0">
Démarrage dans :
<span>{{"{{ startIn }}"}}</span>"
<span class="point">|</span>
</span>
<span ng-show="settings && settings.end > 0">
<span id="hours">{{"{{ time.hours | time }}"}}</span>
<span class="point">:</span>
<span id="min">{{"{{ time.minutes | time }}"}}</span>
<span class="point">:</span>
<span id="sec">{{"{{ time.seconds | time }}"}}</span>
</p>
</div>
</span>
</span>
</nav>
<div class="container">
<div class="row">
<div class="col-sm-12" ng-view></div>
</div>
<div class="progress" style="background-color: #4eaee6; height: 3px; border-radius: 0;">
<div class="progress-bar bg-secondary" role="progressbar" style="width: {{ "{{timeProgression * 100}}" }}%"></div>
</div>
<script src="/js/jquery.min.js"></script>
<script src="/js/bootstrap.min.js"></script>
<script src="/js/angular.min.js"></script>
<script src="{{.urlbase}}js/angular-resource.min.js"></script>
<script src="/js/angular-route.min.js"></script>
<script src="{{.urlbase}}js/app.js"></script>
<div class="container mt-1" ng-view></div>
<div style="position: fixed; top: 60px; right: 0; z-index: 10; min-width: 30vw;">
<toast ng-repeat="toast in toasts" yes-no="toast.yesFunc || toast.noFunc" onyes="toast.yesFunc" onno="toast.noFunc" date="toast.date" msg="toast.msg" timeout="toast.timeout" title="toast.title" variant="toast.variant"></toast>
</div>
<script src="js/jquery.min.js"></script>
<script src="js/popper.min.js"></script>
<script src="js/bootstrap.min.js"></script>
<script src="js/angular.min.js"></script>
<script src="js/angular-resource.min.js"></script>
<script src="js/angular-route.min.js"></script>
<script src="js/angular-sanitize.min.js"></script>
<script src="js/app.js"></script>
<script src="js/common.js"></script>
</body>
</html>
`

View file

@ -2,91 +2,332 @@ package main
import (
"flag"
"fmt"
"io/fs"
"io/ioutil"
"log"
"net/http"
"os"
"os/signal"
"path"
"path/filepath"
"text/template"
"strconv"
"strings"
"syscall"
"srs.epita.fr/fic-server/admin/api"
"srs.epita.fr/fic-server/admin/generation"
"srs.epita.fr/fic-server/admin/pki"
"srs.epita.fr/fic-server/admin/sync"
"srs.epita.fr/fic-server/libfic"
"srs.epita.fr/fic-server/settings"
)
var PKIDir string
var SubmissionDir string
var BaseURL string
var CloudDAVBase string
var CloudUsername string
var CloudPassword string
var StaticDir string
func main() {
var bind = flag.String("bind", "127.0.0.1:8081", "Bind port/socket")
var dsn = flag.String("dsn", "fic:fic@/fic", "DSN to connect to the MySQL server")
flag.StringVar(&BaseURL, "baseurl", "/", "URL prepended to each URL")
flag.StringVar(&SubmissionDir, "submission", "./submissions/", "Base directory where save submissions")
flag.StringVar(&PKIDir, "pki", "./pki/", "Base directory where found PKI scripts")
flag.StringVar(&StaticDir, "static", "./htdocs-admin/", "Directory containing static files")
flag.StringVar(&fic.FilesDir, "files", "./FILES/", "Base directory where found challenges files, local part")
flag.StringVar(&CloudDAVBase, "clouddav", "https://srs.epita.fr/owncloud/remote.php/webdav/FIC 2016",
"Base directory where found challenges files, cloud part")
flag.StringVar(&CloudUsername, "clouduser", "fic", "Username used to sync")
flag.StringVar(&CloudPassword, "cloudpass", "", "Password used to sync")
var err error
bind := "127.0.0.1:8081"
cloudDAVBase := ""
cloudUsername := "fic"
cloudPassword := ""
localImporterDirectory := ""
gitImporterRemote := ""
gitImporterBranch := ""
localImporterSymlink := false
baseURL := "/"
checkplugins := sync.CheckPluginList{}
// Read paremeters from environment
if v, exists := os.LookupEnv("FICOIDC_ISSUER"); exists {
api.OidcIssuer = v
} else if v, exists := os.LookupEnv("FICOIDC_ISSUER_FILE"); exists {
fd, err := os.Open(v)
if err != nil {
log.Fatal("Unable to open FICOIDC_ISSUER_FILE:", err)
}
b, _ := ioutil.ReadAll(fd)
api.OidcIssuer = strings.TrimSpace(string(b))
fd.Close()
}
if v, exists := os.LookupEnv("FICOIDC_SECRET"); exists {
api.OidcSecret = v
} else if v, exists := os.LookupEnv("FICOIDC_SECRET_FILE"); exists {
fd, err := os.Open(v)
if err != nil {
log.Fatal("Unable to open FICOIDC_SECRET_FILE:", err)
}
b, _ := ioutil.ReadAll(fd)
api.OidcSecret = strings.TrimSpace(string(b))
fd.Close()
}
if v, exists := os.LookupEnv("FICCA_PASS"); exists {
pki.SetCAPassword(v)
} else if v, exists := os.LookupEnv("FICCA_PASS_FILE"); exists {
fd, err := os.Open(v)
if err != nil {
log.Fatal("Unable to open FICCA_PASS_FILE:", err)
}
b, _ := ioutil.ReadAll(fd)
pki.SetCAPassword(strings.TrimSpace(string(b)))
fd.Close()
} else {
log.Println("WARNING: no password defined for the CA, will use empty password to secure CA private key")
log.Println("WARNING: PLEASE DEFINE ENVIRONMENT VARIABLE: FICCA_PASS")
}
if v, exists := os.LookupEnv("FICCLOUD_URL"); exists {
cloudDAVBase = v
}
if v, exists := os.LookupEnv("FICCLOUD_USER"); exists {
cloudUsername = v
}
if v, exists := os.LookupEnv("FICCLOUD_PASS"); exists {
cloudPassword = v
} else if v, exists := os.LookupEnv("FICCLOUD_PASS_FILE"); exists {
fd, err := os.Open(v)
if err != nil {
log.Fatal("Unable to open FICCLOUD_PASS_FILE:", err)
}
b, _ := ioutil.ReadAll(fd)
cloudPassword = strings.TrimSpace(string(b))
fd.Close()
}
if v, exists := os.LookupEnv("FIC_BASEURL"); exists {
baseURL = v
}
if v, exists := os.LookupEnv("FIC_4REAL"); exists {
api.IsProductionEnv, err = strconv.ParseBool(v)
if err != nil {
log.Fatal("Unable to parse FIC_4REAL variable:", err)
}
}
if v, exists := os.LookupEnv("FIC_ADMIN_BIND"); exists {
bind = v
}
if v, exists := os.LookupEnv("FIC_TIMESTAMPCHECK"); exists {
api.TimestampCheck = v
}
if v, exists := os.LookupEnv("FIC_SETTINGS"); exists {
settings.SettingsDir = v
}
if v, exists := os.LookupEnv("FIC_FILES"); exists {
fic.FilesDir = v
}
if v, exists := os.LookupEnv("FIC_SYNC_LOCALIMPORT"); exists {
localImporterDirectory = v
}
if v, exists := os.LookupEnv("FIC_SYNC_LOCALIMPORTSYMLINK"); exists {
localImporterSymlink, err = strconv.ParseBool(v)
if err != nil {
log.Fatal("Unable to parse FIC_SYNC_LOCALIMPORTSYMLINK variable:", err)
}
}
if v, exists := os.LookupEnv("FIC_SYNC_GIT_IMPORT_REMOTE"); exists {
gitImporterRemote = v
}
if v, exists := os.LookupEnv("FIC_SYNC_GIT_BRANCH"); exists {
gitImporterBranch = v
}
if v, exists := os.LookupEnv("FIC_OPTIONALDIGEST"); exists {
fic.OptionalDigest, err = strconv.ParseBool(v)
if err != nil {
log.Fatal("Unable to parse FIC_OPTIONALDIGEST variable:", err)
}
}
if v, exists := os.LookupEnv("FIC_STRONGDIGEST"); exists {
fic.StrongDigest, err = strconv.ParseBool(v)
if err != nil {
log.Fatal("Unable to parse FIC_STRONGDIGEST variable:", err)
}
}
// Read parameters from command line
flag.StringVar(&bind, "bind", bind, "Bind port/socket")
var dsn = flag.String("dsn", fic.DSNGenerator(), "DSN to connect to the MySQL server")
flag.StringVar(&baseURL, "baseurl", baseURL, "URL prepended to each URL")
flag.StringVar(&api.TimestampCheck, "timestampCheck", api.TimestampCheck, "Path regularly touched by frontend to check time synchronisation")
flag.StringVar(&pki.PKIDir, "pki", "./PKI", "Base directory where found PKI scripts")
var staticDir = flag.String("static", "", "Directory containing static files (default if not provided: use embedded files)")
flag.StringVar(&api.TeamsDir, "teams", "./TEAMS", "Base directory where save teams JSON files")
flag.StringVar(&api.DashboardDir, "dashbord", "./DASHBOARD", "Base directory where save public JSON files")
flag.StringVar(&settings.SettingsDir, "settings", settings.SettingsDir, "Base directory where load and save settings")
flag.StringVar(&fic.FilesDir, "files", fic.FilesDir, "Base directory where found challenges files, local part")
flag.StringVar(&generation.GeneratorSocket, "generator", "./GENERATOR/generator.socket", "Path to the generator socket (used to trigger issues.json generations, use an empty string to generate locally)")
flag.StringVar(&localImporterDirectory, "localimport", localImporterDirectory,
"Base directory where found challenges files to import, local part")
flag.BoolVar(&localImporterSymlink, "localimportsymlink", localImporterSymlink,
"Copy files or just create symlink?")
flag.StringVar(&gitImporterRemote, "git-import-remote", gitImporterRemote,
"Remote URL of the git repository to use as synchronization source")
flag.StringVar(&gitImporterBranch, "git-branch", gitImporterBranch,
"Branch to use in the git repository")
flag.StringVar(&cloudDAVBase, "clouddav", cloudDAVBase,
"Base directory where found challenges files to import, cloud part")
flag.StringVar(&cloudUsername, "clouduser", cloudUsername, "Username used to sync")
flag.StringVar(&cloudPassword, "cloudpass", cloudPassword, "Password used to sync")
flag.BoolVar(&fic.OptionalDigest, "optionaldigest", fic.OptionalDigest, "Is the digest required when importing files?")
flag.BoolVar(&fic.StrongDigest, "strongdigest", fic.StrongDigest, "Are BLAKE2b digests required or is SHA-1 good enough?")
flag.BoolVar(&api.IsProductionEnv, "4real", api.IsProductionEnv, "Set this flag when running for a real challenge (it disallows or avoid most of mass user progression deletion)")
flag.Var(&checkplugins, "rules-plugins", "List of libraries containing others rules to checks")
flag.Var(&sync.RemoteFileDomainWhitelist, "remote-file-domain-whitelist", "List of domains which are allowed to store remote files")
flag.Parse()
log.SetPrefix("[admin] ")
var err error
log.Println("Checking paths...")
if StaticDir, err = filepath.Abs(StaticDir); err != nil {
log.Fatal(err)
// Instantiate importer
if localImporterDirectory != "" && cloudDAVBase != "" {
log.Fatal("Cannot have both --clouddav and --localimport defined.")
return
} else if gitImporterRemote != "" && cloudDAVBase != "" {
log.Fatal("Cannot have both --clouddav and --git-import-remote defined.")
return
} else if gitImporterRemote != "" {
sync.GlobalImporter = sync.NewGitImporter(sync.LocalImporter{Base: localImporterDirectory, Symlink: localImporterSymlink}, gitImporterRemote, gitImporterBranch)
} else if localImporterDirectory != "" {
sync.GlobalImporter = sync.LocalImporter{Base: localImporterDirectory, Symlink: localImporterSymlink}
} else if cloudDAVBase != "" {
sync.GlobalImporter, _ = sync.NewCloudImporter(cloudDAVBase, cloudUsername, cloudPassword)
}
if fic.FilesDir, err = filepath.Abs(fic.FilesDir); err != nil {
log.Fatal(err)
}
if PKIDir, err = filepath.Abs(PKIDir); err != nil {
log.Fatal(err)
}
if SubmissionDir, err = filepath.Abs(SubmissionDir); err != nil {
log.Fatal(err)
}
if fic.FilesDir, err = filepath.Abs(fic.FilesDir); err != nil {
log.Fatal(err)
if sync.GlobalImporter != nil {
if err := sync.GlobalImporter.Init(); err != nil {
log.Fatal("Unable to initialize the importer: ", err.Error())
}
log.Println("Using", sync.GlobalImporter.Kind())
challengeinfo, err := sync.GetFileContent(sync.GlobalImporter, settings.ChallengeFile)
if err == nil {
// Initial distribution of challenge.json
if _, err := os.Stat(path.Join(settings.SettingsDir, settings.ChallengeFile)); os.IsNotExist(err) {
if fd, err := os.Create(path.Join(settings.SettingsDir, settings.ChallengeFile)); err != nil {
log.Fatal("Unable to open SETTINGS/challenge.json:", err)
} else {
fd.Write([]byte(challengeinfo))
err = fd.Close()
if err != nil {
log.Fatal("Something went wrong during SETTINGS/challenge.json writing:", err)
}
}
}
if ci, err := settings.ReadChallengeInfo(challengeinfo); err == nil {
fic.StandaloneExercicesTheme.Authors = ci.Authors
}
}
}
// Sanitize options
log.Println("Checking paths...")
if staticDir != nil && *staticDir != "" {
if sDir, err := filepath.Abs(*staticDir); err != nil {
log.Fatal(err)
} else {
log.Println("Serving pages from", sDir)
staticFS = http.Dir(sDir)
sync.DeepReportPath = path.Join(sDir, sync.DeepReportPath)
}
} else {
sub, err := fs.Sub(assets, "static")
if err != nil {
log.Fatal("Unable to cd to static/ directory:", err)
}
log.Println("Serving pages from memory.")
staticFS = http.FS(sub)
sync.DeepReportPath = path.Join("SYNC", sync.DeepReportPath)
if _, err := os.Stat("SYNC"); os.IsNotExist(err) {
os.MkdirAll("SYNC", 0751)
}
}
if fic.FilesDir, err = filepath.Abs(fic.FilesDir); err != nil {
log.Fatal(err)
}
if pki.PKIDir, err = filepath.Abs(pki.PKIDir); err != nil {
log.Fatal(err)
}
if api.DashboardDir, err = filepath.Abs(api.DashboardDir); err != nil {
log.Fatal(err)
}
if api.TeamsDir, err = filepath.Abs(api.TeamsDir); err != nil {
log.Fatal(err)
}
if api.TimestampCheck, err = filepath.Abs(api.TimestampCheck); err != nil {
log.Fatal(err)
}
if settings.SettingsDir, err = filepath.Abs(settings.SettingsDir); err != nil {
log.Fatal(err)
}
if baseURL != "/" {
baseURL = path.Clean(baseURL)
} else {
baseURL = ""
}
// Creating minimal directories structure
os.MkdirAll(fic.FilesDir, 0751)
os.MkdirAll(pki.PKIDir, 0711)
os.MkdirAll(api.TeamsDir, 0751)
os.MkdirAll(api.DashboardDir, 0751)
os.MkdirAll(settings.SettingsDir, 0751)
// Load rules plugins
for _, p := range checkplugins {
if err := sync.LoadChecksPlugin(p); err != nil {
log.Fatalf("Unable to load rule plugin %q: %s", p, err.Error())
} else {
log.Printf("Rules plugin %q successfully loaded", p)
}
}
// Initialize settings and load them
if !settings.ExistsSettings(path.Join(settings.SettingsDir, settings.SettingsFile)) {
if err = api.ResetSettings(); err != nil {
log.Fatal("Unable to initialize settings.json:", err)
}
}
var config *settings.Settings
if config, err = settings.ReadSettings(path.Join(settings.SettingsDir, settings.SettingsFile)); err != nil {
log.Fatal("Unable to read settings.json:", err)
} else {
api.ApplySettings(config)
}
// Initialize dashboard presets
if err = api.InitDashboardPresets(api.DashboardDir); err != nil {
log.Println("Unable to initialize dashboards presets:", err)
}
// Database connection
log.Println("Opening database...")
if err := fic.DBInit(fmt.Sprintf("%s?parseTime=true", *dsn)); err != nil {
if err = fic.DBInit(*dsn); err != nil {
log.Fatal("Cannot open the database: ", err)
}
defer fic.DBClose()
log.Println("Creating database...")
if err := fic.DBCreate(); err != nil {
if err = fic.DBCreate(); err != nil {
log.Fatal("Cannot create database: ", err)
}
log.Println("Changing base url...")
if file, err := os.OpenFile(path.Join(StaticDir, "index.html"), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(0644)); err != nil {
log.Println("Unable to open index.html: ", err)
} else if indexTmpl, err := template.New("index").Parse(indextpl); err != nil {
log.Println("Cannot create template: ", err)
} else if err := indexTmpl.Execute(file, map[string]string{"urlbase": path.Clean(path.Join(baseURL + "/", "nuke"))[:len(path.Clean(path.Join(baseURL + "/", "nuke"))) - 4]}); err != nil {
log.Println("An error occurs during template execution: ", err)
}
// Update base URL on main page
log.Println("Changing base URL to", baseURL+"/", "...")
genIndex(baseURL)
// Prepare graceful shutdown
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM)
os.Chdir(PKIDir)
app := NewApp(config, baseURL, bind)
go app.Start()
log.Println("Registering handlers...")
mux := http.NewServeMux()
mux.Handle(path.Join(BaseURL, "api") + "/", http.StripPrefix(path.Join(BaseURL, "api"), ApiHandler()))
mux.Handle(path.Join(BaseURL, "teams") + "/", http.StripPrefix(BaseURL, StaticHandler(staticDir)))
mux.Handle(path.Join(BaseURL, "themes") + "/", http.StripPrefix(BaseURL, StaticHandler(staticDir)))
mux.Handle(BaseURL, http.StripPrefix(BaseURL, http.FileServer(http.Dir(staticDir))))
// Wait shutdown signal
<-interrupt
log.Println(fmt.Sprintf("Ready, listening on %s", *bind))
if err := http.ListenAndServe(*bind, mux); err != nil {
log.Fatal("Unable to listen and serve: ", err)
}
log.Print("The service is shutting down...")
app.Stop()
log.Println("done")
}

View file

@ -1,284 +0,0 @@
#!/bin/bash
cd $(dirname "$0")
if [ -z "${PKI_BASEDIR}" ]; then
PKI_BASEDIR=$(dirname `pwd`) # equivalent to $(realpath `pwd`/..
fi
PKI_DIR=${PKI_BASEDIR}/PKI
SHARED_DIR=${PKI_DIR}/shared
OPENSSL_CONF=`pwd`/openssl.cnf
CAKEY=${PKI_DIR}/private/cakey.key
CAREQ=${PKI_DIR}/careq.csr
CACRT=${SHARED_DIR}/cacert.crt
CADER=${SHARED_DIR}/cacert.der
SRVKEY=${SHARED_DIR}/server.key
SRVREQ=${SHARED_DIR}/server.csr
SRVCRT=${SHARED_DIR}/server.crt
# Generate certificates valid for:
DAYS=2
STARTDATE=160125000000Z
ENDDATE=160126235959Z
VALIDITY="-startdate ${STARTDATE} -enddate ${ENDDATE}"
#VALIDITY="-days ${DAYS}"
if [ -z "$PS1" ]
then
GREEN="\033[1;32m"
RED="\033[1;31m"
COLOR_RST="\033[0m"
BOLD=""
END_BOLD=""
ECHO_OPTS="-e"
else
GREEN="<font color=green>"
RED="<font color=red>"
COLOR_RST="</font>"
BOLD="<strong>"
END_BOLD="</strong>"
ECHO_OPTS=""
fi
usage()
{
echo "Usage: $0 (-newca|-newserver IP/URL|-revokeserver|-newclient NAME|-revoke NAME|-gencrl)"
exit 1
}
clean()
{
if [ "$1" = "ca" ]; then
rm -rf ${PKI_DIR}/* ${SHARED_DIR}/*
mkdir -p ${PKI_DIR}/certs ${PKI_DIR}/crl ${PKI_DIR}/newcerts \
${PKI_DIR}/private ${PKI_DIR}/pkcs ${SHARED_DIR}
echo "01" > ${PKI_DIR}/crlnumber
elif [ "$1" = "client" ]; then
rm -rf ${PKI_DIR}/${2}.key ${PKI_DIR}/${2}.csr
fi
rm -rf $OUTPUT
}
gen_crl()
{
echo $ECHO_OPTS "${GREEN}Generate shared/crl.pem${COLOR_RST}"
if ! openssl ca -config ${OPENSSL_CONF} -gencrl -out ${SHARED_DIR}/crl.pem > $OUTPUT 2>&1
then
echo $ECHO_OPTS "${RED}Generate shared/crl.pem failed"
cat $OUTPUT
exit 5
fi
}
[ $# -lt 1 ] && usage
OUTPUT=$(mktemp)
case $1 in
"-newca" )
echo $ECHO_OPTS "${GREEN}Create the directories, take care this will delete the old directories ${COLOR_RST}"
clean "ca"
touch ${PKI_DIR}/index.txt
ESCAPED=$(echo "${PKI_DIR}" | sed 's/[\/\.]/\\&/g')
echo $ECHO_OPTS "${GREEN}Making CA key and csr${COLOR_RST}"
sed -i 's/=.*#COMMONNAME/= FIC CA #COMMONNAME/' $OPENSSL_CONF
sed -i "s/=.*#DIR/= ${ESCAPED} #DIR/" $OPENSSL_CONF
sed -i "s/=.*#DAYS/= ${DAYS} #DAYS/" $OPENSSL_CONF
type pwgen > /dev/null
if [ $? -ne 0 ]; then
echo "command not found: pwgen"
exit 5
fi
pass=`pwgen -n -B -y 12 1`
if ! openssl req -batch -new -keyout ${CAKEY} \
-out ${CAREQ} -passout pass:$pass \
-config ${OPENSSL_CONF} -extensions CORE_CA > $OUTPUT 2>&1
then
cat $OUTPUT
clean "ca"
exit 4
fi
# This line deleted the passphase for the FIC 2014 automatisation
if ! openssl rsa -passin pass:$pass -in ${CAKEY} \
-out ${CAKEY} > $OUTPUT 2>&1
then
cat $OUTPUT
clean "ca"
exit 4
fi
echo $ECHO_OPTS "${GREEN}Self signes the CA certificate${COLOR_RST}"
if ! openssl ca -batch -create_serial -out ${CACRT} \
${VALIDITY} -keyfile ${CAKEY} \
-selfsign -extensions CORE_CA -config ${OPENSSL_CONF} \
-infiles ${CAREQ} > $OUTPUT 2>&1
then
cat $OUTPUT
clean "ca"
exit 4
fi
echo $ECHO_OPTS "${GREEN}Generate DER format${COLOR_RST}"
openssl x509 -in ${CACRT} -inform PEM -out ${CADER} -outform DER
;;
"-newserver" )
if [ $# -lt 2 ]; then
echo "Give as first argument the production IP or the domain that this certificat will cover."
echo "eg.: $0 -newserver 10.42.23.69"
echo " $0 -newserver fic.srs.epita.fr"
exit 1
fi
echo $ECHO_OPTS "${GREEN}Making the Server key and cert${COLOR_RST}"
if ! [ -f ${CAKEY} ]; then
echo $ECHO_OPTS "${RED}Can not found the CA's key${COLOR_RST}"
exit 2
fi
sed -i "s/=.*#COMMONNAME/=$2#COMMONNAME/" $OPENSSL_CONF
sed -i "s/=.*#DAYS/= ${DAYS} #DAYS/" $OPENSSL_CONF
if ! openssl req -batch -new -keyout ${SRVKEY} -out ${SRVREQ} \
-days ${DAYS} -config ${OPENSSL_CONF} -extensions SERVER_SSL > $OUTPUT 2>&1
then
cat $OUTPUT
exit 4
fi
echo $ECHO_OPTS "${GREEN}Signing the Server crt${COLOR_RST}"
if ! openssl ca -policy policy_match -config ${OPENSSL_CONF} \
-out ${SRVCRT} -extensions SERVER_SSL -infiles ${SRVREQ}
then
echo $ECHO_OPTS "${RED}Signing failed for new server${COLOR_RST}"
rm -f ${SRVKEY} ${SRVREQ} ${SRVCRT}
cat $OUTPUT
exit 3
else
rm ${SRVREQ}
echo $ECHO_OPTS "${GREEN}Signed certificate is in ${SRVCRT}${COLOR_RST}"
fi
;;
"-revokeserver" )
echo $ECHO_OPTS "${GREEN}Revocate server certificate${COLOR_RST}"
if ! [ -f ${CAKEY} ]; then
echo $ECHO_OPTS "${RED}Can not found the CA's key${COLOR_RST}"
exit 2
fi
if ! openssl ca -revoke ${SRVCRT} -config ${OPENSSL_CONF} \
-keyfile ${CAKEY} -cert ${CACRT} > $OUTPUT 2>&1
then
echo $ECHO_OPTS "${RED}Server certificate revocation failed${COLOR_RST}"
cat $OUTPUT
exit 4
fi
rm ${SRVKEY} ${SRVCRT}
gen_crl
;;
"-newclient" )
if [ $# -ne 2 ]; then
echo "Usage: $0 -newclient NAME"
exit 1
fi
CLTNAM=$2
CLTREQ=${PKI_DIR}/${CLTNAM}.csr
CLTCRT=${PKI_DIR}/certs/${CLTNAM}.crt
CLTKEY=${PKI_DIR}/${CLTNAM}.key
CLTP12=${PKI_DIR}/pkcs/${CLTNAM}.p12
echo "=============================================================="
echo $ECHO_OPTS "${GREEN}Making the client key and csr of ${BOLD}${2}${END_BOLD}${COLOR_RST}"
ESCAPED=$(echo "${PKI_DIR}" | sed 's/[\/\.]/\\&/g')
sed -i "s/=.*#DIR/= ${ESCAPED} #DIR/" $OPENSSL_CONF
sed -i "s/=.*#DAYS/= ${DAYS} #DAYS/" $OPENSSL_CONF
if ! [ -f ${CAKEY} ]; then
echo $ECHO_OPTS "${RED}Can not found the CA's key${COLOR_RST}"
exit 2
fi
sed -i "s/=.*#COMMONNAME/= $2#COMMONNAME/" $OPENSSL_CONF
type pwgen > /dev/null
if [ $? -ne 0 ]; then
echo "command not found: pwgen"
exit 5
fi
pass=`pwgen -n -B -y 12 1`
if ! openssl req -batch -new -keyout "${CLTKEY}" -out "${CLTREQ}" \
-config ${OPENSSL_CONF} -passout pass:$pass -days ${DAYS} -extensions CLIENT_SSL > $OUTPUT 2>&1
then
cat $OUTPUT
clean "client" ${CLTNAM}
exit 4
fi
echo $ECHO_OPTS "${GREEN}Signing the Client crt${COLOR_RST}"
if ! openssl ca -batch -policy policy_match -out "${CLTCRT}" \
-config ${OPENSSL_CONF} -extensions CLIENT_SSL -infiles "${CLTREQ}" > $OUTPUT 2>&1
then
echo $ECHO_OPTS "${RED}Signing failed for $2 ${COLOR_RST}"
cat $OUTPUT
clean "client" ${CLTNAM}
exit 3
fi
echo $ECHO_OPTS "${GREEN}Export the Client files to pkcs12${COLOR_RST}"
if ! openssl pkcs12 -export -inkey "${CLTKEY}" -in "${CLTCRT}" -name ${2} \
-passin pass:$pass -out "${CLTP12}" \
-passout pass:$pass > $OUTPUT 2>&1
then
echo $ECHO_OPTS "${RED}pkcs12 export failed for ${BOLD}$2${END_BOLD}${COLOR_RST}"
cat $OUTPUT
clean "client" ${CLTNAM}
exit 4
else
echo $ECHO_OPTS "Exported pkcs12 file is ${CLTP12}"
fi
echo "$CLTNAM:$pass" >> ${PKI_DIR}/teams.pass
echo "$CLTNAM:$pass"
clean "client" ${CLTNAM}
;;
"-revoke" )
if [ $# -ne 2 ]; then
echo "Usage: $0 -revoke NAME"
exit 1
fi
CLTNAM=$2
CLTCRT=${PKI_DIR}/certs/${CLTNAM}.crt
CLTP12=${PKI_DIR}/pkcs/${CLTNAM}.p12
echo $ECHO_OPTS "${GREEN}Revocate ${BOLD}${CLTNAM}${END_BOLD}${COLOR_RST}"
if ! openssl ca -revoke "${CLTCRT}" -config "${OPENSSL_CONF}" \
-keyfile "${CAKEY}" -cert "${CACRT}" > $OUTPUT 2>&1
then
echo $ECHO_OPTS "${RED}Revocation failed for ${BOLD}${CLTNAM}${END_BOLD}${COLOR_RST}"
cat $OUTPUT
exit 4
fi
rm "${CLTCRT}" "${CLTP12}"
gen_crl
;;
"-gencrl" )
gen_crl
;;
* )
usage
;;
esac

133
admin/pki/ca.go Normal file
View file

@ -0,0 +1,133 @@
package pki
import (
"crypto/ecdsa"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"io/ioutil"
"math/big"
"os"
"path"
"time"
)
var passwordCA string
func SetCAPassword(pass string) {
passwordCA = pass
}
func CACertPath() string {
return path.Join(PKIDir, "shared", "ca.pem")
}
func CAPrivkeyPath() string {
return path.Join(PKIDir, "ca.key")
}
func GenerateCA(notBefore time.Time, notAfter time.Time) error {
ca := &x509.Certificate{
SerialNumber: big.NewInt(0),
Subject: pkix.Name{
Organization: []string{"EPITA"},
OrganizationalUnit: []string{"SRS laboratory"},
Country: []string{"FR"},
Locality: []string{"Paris"},
CommonName: "FIC CA",
},
NotBefore: notBefore,
NotAfter: notAfter,
IsCA: true,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
BasicConstraintsValid: true,
}
// Ensure directories exists
os.Mkdir(PKIDir, 0751)
os.Mkdir(path.Join(PKIDir, "shared"), 0751)
pub, priv, err := GeneratePrivKey()
if err != nil {
return err
}
ca_b, err := x509.CreateCertificate(rand.Reader, ca, ca, pub, priv)
if err != nil {
return err
}
// Save certificate to file
if err := saveCertificate(CACertPath(), ca_b); err != nil {
return err
}
// Save private key to file
if err := savePrivateKeyEncrypted(CAPrivkeyPath(), priv, passwordCA); err != nil {
return err
}
return nil
}
func LoadCA() (priv ecdsa.PrivateKey, ca x509.Certificate, err error) {
// Load certificate
if fd, errr := os.Open(CACertPath()); errr != nil {
return priv, ca, errr
} else {
defer fd.Close()
if cert, errr := ioutil.ReadAll(fd); errr != nil {
return priv, ca, errr
} else {
block, _ := pem.Decode(cert)
if block == nil || block.Type != "CERTIFICATE" {
return priv, ca, errors.New("failed to decode PEM block containing certificate")
}
if catmp, errr := x509.ParseCertificate(block.Bytes); errr != nil {
return priv, ca, errr
} else if catmp == nil {
return priv, ca, errors.New("failed to parse certificate")
} else {
ca = *catmp
}
}
}
// Load private key
if fd, errr := os.Open(CAPrivkeyPath()); errr != nil {
return priv, ca, errr
} else {
defer fd.Close()
if privkey, errr := ioutil.ReadAll(fd); errr != nil {
return priv, ca, errr
} else {
block, _ := pem.Decode(privkey)
if block == nil || block.Type != "EC PRIVATE KEY" {
return priv, ca, errors.New("failed to decode PEM block containing EC private key")
}
var decrypted_der []byte
if x509.IsEncryptedPEMBlock(block) {
decrypted_der, err = x509.DecryptPEMBlock(block, []byte(passwordCA))
if err != nil {
return
}
} else {
decrypted_der = block.Bytes
}
if tmppriv, errr := x509.ParseECPrivateKey(decrypted_der); errr != nil {
return priv, ca, errr
} else if tmppriv == nil {
return priv, ca, errors.New("failed to parse private key")
} else {
priv = *tmppriv
}
}
}
return
}

84
admin/pki/client.go Normal file
View file

@ -0,0 +1,84 @@
package pki
import (
"crypto/ecdsa"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"fmt"
"math"
"math/big"
"os"
"os/exec"
"path"
"time"
)
func ClientCertificatePath(serial uint64) string {
return path.Join(PKIDir, fmt.Sprintf("%0[2]*[1]X", serial, int(math.Ceil(math.Log2(float64(serial))/8)*2)), "cert.pem")
}
func ClientPrivkeyPath(serial uint64) string {
return path.Join(PKIDir, fmt.Sprintf("%0[2]*[1]X", serial, int(math.Ceil(math.Log2(float64(serial))/8)*2)), "privkey.pem")
}
func ClientP12Path(serial uint64) string {
return path.Join(PKIDir, fmt.Sprintf("%0[2]*[1]X", serial, int(math.Ceil(math.Log2(float64(serial))/8)*2)), "team.p12")
}
func GenerateClient(serial uint64, notBefore time.Time, notAfter time.Time, parent_cert *x509.Certificate, parent_priv *ecdsa.PrivateKey) error {
var certid big.Int
certid.SetUint64(serial)
client := &x509.Certificate{
SerialNumber: &certid,
Subject: pkix.Name{
Organization: []string{"EPITA"},
OrganizationalUnit: []string{"SRS laboratory"},
Country: []string{"FR"},
Locality: []string{"Paris"},
CommonName: fmt.Sprintf("TEAM-%0[2]*[1]X", serial, int(math.Ceil(math.Log2(float64(serial))/8)*2)),
},
NotBefore: notBefore,
NotAfter: notAfter,
IsCA: false,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
KeyUsage: x509.KeyUsageDigitalSignature,
BasicConstraintsValid: true,
}
pub, priv, err := GeneratePrivKey()
if err != nil {
return err
}
client_b, err := x509.CreateCertificate(rand.Reader, client, parent_cert, pub, parent_priv)
if err != nil {
return err
}
// Create intermediate directory
os.MkdirAll(path.Join(PKIDir, fmt.Sprintf("%0[2]*[1]X", serial, int(math.Ceil(math.Log2(float64(serial))/8)*2))), 0777)
// Save certificate to file
if err := saveCertificate(ClientCertificatePath(serial), client_b); err != nil {
return err
}
// Save private key to file
if err := savePrivateKey(ClientPrivkeyPath(serial), priv); err != nil {
return err
}
return nil
}
func WriteP12(serial uint64, password string) error {
cmd := exec.Command("/usr/bin/openssl", "pkcs12", "-export",
"-inkey", ClientPrivkeyPath(serial),
"-in", ClientCertificatePath(serial),
"-name", fmt.Sprintf("TEAM-%0[2]*[1]X", serial, int(math.Ceil(math.Log2(float64(serial))/8)*2)),
"-passout", "pass:" + password,
"-out", ClientP12Path(serial))
return cmd.Run()
}

62
admin/pki/common.go Normal file
View file

@ -0,0 +1,62 @@
package pki
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"encoding/pem"
"os"
)
var PKIDir string
func GeneratePrivKey() (pub *ecdsa.PublicKey, priv *ecdsa.PrivateKey, err error) {
if priv, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader); err == nil {
pub = &priv.PublicKey
}
return
}
func saveCertificate(path string, cert []byte) error {
if certOut, err := os.Create(path); err != nil {
return err
} else {
pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: cert})
certOut.Close()
}
return nil
}
func savePrivateKey(path string, private *ecdsa.PrivateKey) error {
if keyOut, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600); err != nil {
return err
} else if key_b, err := x509.MarshalECPrivateKey(private); err != nil {
return err
} else {
pem.Encode(keyOut, &pem.Block{Type: "EC PRIVATE KEY", Bytes: key_b})
keyOut.Close()
}
return nil
}
func savePrivateKeyEncrypted(path string, private *ecdsa.PrivateKey, password string) error {
if password == "" {
return savePrivateKey(path, private)
}
if keyOut, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600); err != nil {
return err
} else {
defer keyOut.Close()
if key_b, err := x509.MarshalECPrivateKey(private); err != nil {
return err
} else if key_c, err := x509.EncryptPEMBlock(rand.Reader, "EC PRIVATE KEY", key_b, []byte(password), x509.PEMCipherAES256); err != nil {
return err
} else {
pem.Encode(keyOut, key_c)
}
}
return nil
}

View file

@ -1,197 +0,0 @@
#
# OpenSSL example configuration file.
# This is mostly being used for generation of certificate requests.
#
# This definition stops the following lines choking if HOME isn't
# defined.
HOME = .
RANDFILE = $ENV::HOME/.rnd
# Extra OBJECT IDENTIFIER info:
#oid_file = $ENV::HOME/.oid
oid_section = new_oids
# To use this configuration file with the "-extfile" option of the
# "openssl x509" utility, name here the section containing the
# X.509v3 extensions to use:
# extensions =
# (Alternatively, use a configuration file that has only
# X.509v3 extensions in its main [= default] section.)
[ new_oids ]
# We can add new OIDs in here for use by 'ca', 'req' and 'ts'.
# Add a simple OID like this:
# testoid1=1.2.3.4
# Or use config file substitution like this:
# testoid2=${testoid1}.5.6
# Policies used by the TSA examples.
tsa_policy1 = 1.2.3.4.1
tsa_policy2 = 1.2.3.4.5.6
tsa_policy3 = 1.2.3.4.5.7
####################################################################
[ ca ]
default_ca = CA_default # The default ca section
####################################################################
[ CA_default ]
dir = /home/nemunaire/workspace/gowks/src/srs.epita.fr/fic-server/admin/PKI #DIR # Where everything is kept
certs = $dir/certs # Where the issued certs are kept
crl_dir = $dir/crl # Where the issued crl are kept
database = $dir/index.txt # database index file.
#unique_subject = no # Set to 'no' to allow creation of
# several ctificates with same subject.
new_certs_dir = $dir/newcerts # default place for new certs.
certificate = $dir/shared/cacert.crt # The CA certificate
serial = $dir/serial # The current serial number
crlnumber = $dir/crlnumber # the current crl number
# must be commented out to leave a V1 CRL
crl = $dir/shared/crl.pem # The current CRL
private_key = $dir/private/cakey.key # The private key
RANDFILE = $dir/private/.rand # private random number file
# Comment out the following two lines for the "traditional"
# (and highly broken) format.
name_opt = ca_default # Subject Name options
cert_opt = ca_default # Certificate field options
# Extension copying option: use with caution.
# copy_extensions = copy
# Extensions to add to a CRL. Note: Netscape communicator chokes on V2 CRLs
# so this is commented out by default to leave a V1 CRL.
# crlnumber must also be commented out to leave a V1 CRL.
# crl_extensions = crl_ext
default_days = 2 #DAYS
default_crl_days= 1 # how long before next CRL
default_md = default # use public key default MD
preserve = no # keep passed DN ordering
# A few difference way of specifying how similar the request should look
# For type CA, the listed attributes must be the same, and the optional
# and supplied fields are just that :-)
policy = policy_match
# For the CA policy
[ policy_match ]
countryName = match
stateOrProvinceName = match
organizationName = match
organizationalUnitName = optional
commonName = supplied
emailAddress = optional
# For the 'anything' policy
# At this point in time, you must list all acceptable 'object'
# types.
[ policy_anything ]
countryName = optional
stateOrProvinceName = optional
localityName = optional
organizationName = optional
organizationalUnitName = optional
commonName = supplied
emailAddress = optional
####################################################################
[ req ]
default_bits = 2048
default_keyfile = privkey.pem
distinguished_name = req_distinguished_name
attributes = req_attributes
x509_extensions = v3_ca # The extentions to add to the self signed cert
# Passwords for private keys if not present they will be prompted for
# input_password = secret
# output_password = secret
# This sets a mask for permitted string types. There are several options.
# default: PrintableString, T61String, BMPString.
# pkix : PrintableString, BMPString (PKIX recommendation before 2004)
# utf8only: only UTF8Strings (PKIX recommendation after 2004).
# nombstr : PrintableString, T61String (no BMPStrings or UTF8Strings).
# MASK:XXXX a literal mask value.
# WARNING: ancient versions of Netscape crash on BMPStrings or UTF8Strings.
string_mask = utf8only
# req_extensions = v3_req # The extensions to add to a certificate request
[ req_distinguished_name ]
countryName = Country Name (2 letter code)
countryName_default = FR
countryName_min = 2
countryName_max = 2
stateOrProvinceName = State or Province Name (full name)
stateOrProvinceName_default = France
localityName = Locality Name (eg, city)
localityName_default = Paris
0.organizationName = Organization Name (eg, company)
0.organizationName_default = Epita
# we can do this but it is not needed normally :-)
#1.organizationName = Second Organization Name (eg, company)
#1.organizationName_default = World Wide Web Pty Ltd
organizationalUnitName = Organizational Unit Name (eg, section)
organizationalUnitName_default = SRS
commonName = Common Name (e.g. server FQDN or YOUR name)
commonName_default = Acier#COMMONNAME
commonName_max = 64
emailAddress = Email Address
emailAddress_max = 64
emailAddress_default = root@srs.epita.fr
# SET-ex3 = SET extension number 3
[ req_attributes ]
challengePassword = A challenge password
challengePassword_min = 4
challengePassword_max = 20
unstructuredName = An optional company name
[CORE_CA]
nsComment = "FIC CA"
basicConstraints = critical,CA:TRUE,pathlen:1
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer:always
issuerAltName = issuer:copy
keyUsage = keyCertSign, cRLSign
nsCertType = sslCA
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid:always,issuer
[SERVER_SSL]
nsComment = "FIC Server"
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer:always
issuerAltName = issuer:copy
basicConstraints = critical,CA:FALSE
keyUsage = digitalSignature, keyEncipherment
nsCertType = server
extendedKeyUsage = serverAuth
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid:always,issuer
[CLIENT_SSL]
nsComment = "FIC Client"
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer:always
issuerAltName = issuer:copy
basicConstraints = critical,CA:FALSE
keyUsage = digitalSignature, nonRepudiation
nsCertType = client
extendedKeyUsage = clientAuth
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid:always,issuer

79
admin/pki/team.go Normal file
View file

@ -0,0 +1,79 @@
package pki
import (
"fmt"
"io/ioutil"
"math"
"os"
"path"
"strconv"
"strings"
)
const SymlinkPrefix = "_AUTH_ID_"
func GetCertificateAssociation(serial uint64) string {
return fmt.Sprintf(SymlinkPrefix+"%0[2]*[1]X", serial, int(math.Ceil(math.Log2(float64(serial))/8)*2))
}
func GetAssociation(dirname string) (assocs string, err error) {
return os.Readlink(dirname)
}
func GetAssociations(dirname string) (assocs []string, err error) {
if ds, errr := ioutil.ReadDir(dirname); errr != nil {
return nil, errr
} else {
for _, d := range ds {
if d.Mode()&os.ModeSymlink == os.ModeSymlink {
assocs = append(assocs, d.Name())
}
}
return
}
}
func GetTeamSerials(dirname string, id_team int64) (serials []uint64, err error) {
// As futher comparaisons will be made with strings, convert it only one time
str_tid := fmt.Sprintf("%d", id_team)
var assocs []string
if assocs, err = GetAssociations(dirname); err != nil {
return
} else {
for _, assoc := range assocs {
var tid string
if tid, err = os.Readlink(path.Join(dirname, assoc)); err == nil && tid == str_tid && strings.HasPrefix(assoc, SymlinkPrefix) {
if serial, err := strconv.ParseUint(assoc[9:], 16, 64); err == nil {
serials = append(serials, serial)
}
}
}
}
return
}
func GetTeamAssociations(dirname string, id_team int64) (teamAssocs []string, err error) {
// As futher comparaisons will be made with strings, convert it only one time
str_tid := fmt.Sprintf("%d", id_team)
var assocs []string
if assocs, err = GetAssociations(dirname); err != nil {
return
} else {
for _, assoc := range assocs {
var tid string
if tid, err = os.Readlink(path.Join(dirname, assoc)); err == nil && tid == str_tid && !strings.HasPrefix(assoc, SymlinkPrefix) {
teamAssocs = append(teamAssocs, assoc)
}
}
}
return
}
func DeleteTeamAssociation(dirname string, assoc string) error {
if err := os.Remove(path.Join(dirname, assoc)); err != nil {
return err
}
return nil
}

View file

@ -1,18 +1,160 @@
package main
import (
"bytes"
"embed"
"errors"
"log"
"net/http"
"os"
"path"
"strings"
"text/template"
"srs.epita.fr/fic-server/admin/api"
"srs.epita.fr/fic-server/admin/sync"
"srs.epita.fr/fic-server/libfic"
"srs.epita.fr/fic-server/settings"
"github.com/gin-gonic/gin"
)
type staticRouting struct {
StaticDir string
//go:embed static
var assets embed.FS
var indexPage []byte
func genIndex(baseURL string) {
tplcfg := map[string]string{
"logo": "img/logo.png",
"title": "Challenge",
"urlbase": path.Clean(path.Join(baseURL+"/", "nuke"))[:len(path.Clean(path.Join(baseURL+"/", "nuke")))-4],
}
ci, err := api.GetChallengeInfo()
if err == nil && ci != nil {
tplcfg["title"] = ci.Title
if len(ci.MainLogo) > 0 {
tplcfg["logo"] = "/files/logo/" + path.Base(ci.MainLogo[0])
}
}
b := bytes.NewBufferString("")
if indexTmpl, err := template.New("index").Parse(indextpl); err != nil {
log.Fatal("Cannot create template:", err)
} else if err = indexTmpl.Execute(b, tplcfg); err != nil {
log.Fatal("An error occurs during template execution:", err)
} else {
indexPage = b.Bytes()
}
}
func StaticHandler(staticDir string) http.Handler {
return staticRouting{staticDir}
func serveIndex(c *gin.Context) {
c.Writer.Write(indexPage)
}
func (a staticRouting) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, path.Join(a.StaticDir, "index.html"))
var staticFS http.FileSystem
func serveFile(c *gin.Context, url string) {
c.Request.URL.Path = url
http.FileServer(staticFS).ServeHTTP(c.Writer, c.Request)
}
func declareStaticRoutes(router *gin.RouterGroup, cfg *settings.Settings, baseURL string) {
router.GET("/", func(c *gin.Context) {
serveIndex(c)
})
router.GET("/auth/*_", func(c *gin.Context) {
serveIndex(c)
})
router.GET("/claims/*_", func(c *gin.Context) {
serveIndex(c)
})
router.GET("/exercices/*_", func(c *gin.Context) {
serveIndex(c)
})
router.GET("/events/*_", func(c *gin.Context) {
serveIndex(c)
})
router.GET("/files", func(c *gin.Context) {
serveIndex(c)
})
router.GET("/forge-links", func(c *gin.Context) {
serveIndex(c)
})
router.GET("/public/*_", func(c *gin.Context) {
serveIndex(c)
})
router.GET("/pki/*_", func(c *gin.Context) {
serveIndex(c)
})
router.GET("/repositories", func(c *gin.Context) {
serveIndex(c)
})
router.GET("/settings", func(c *gin.Context) {
serveIndex(c)
})
router.GET("/sync", func(c *gin.Context) {
serveIndex(c)
})
router.GET("/tags/*_", func(c *gin.Context) {
serveIndex(c)
})
router.GET("/teams/*_", func(c *gin.Context) {
serveIndex(c)
})
router.GET("/themes/*_", func(c *gin.Context) {
serveIndex(c)
})
router.GET("/css/*_", func(c *gin.Context) {
serveFile(c, strings.TrimPrefix(c.Request.URL.Path, baseURL))
})
router.GET("/fonts/*_", func(c *gin.Context) {
serveFile(c, strings.TrimPrefix(c.Request.URL.Path, baseURL))
})
router.GET("/img/*_", func(c *gin.Context) {
serveFile(c, strings.TrimPrefix(c.Request.URL.Path, baseURL))
})
router.GET("/js/*_", func(c *gin.Context) {
serveFile(c, strings.TrimPrefix(c.Request.URL.Path, baseURL))
})
router.GET("/views/*_", func(c *gin.Context) {
serveFile(c, strings.TrimPrefix(c.Request.URL.Path, baseURL))
})
router.GET("/files/*_", func(c *gin.Context) {
filepath := path.Join(fic.FilesDir, strings.TrimPrefix(strings.TrimPrefix(c.Request.URL.Path, baseURL), "/files"))
if st, err := os.Stat(filepath); os.IsNotExist(err) || st.Size() == 0 {
if st, err := os.Stat(filepath + ".gz"); err == nil {
if fd, err := os.Open(filepath + ".gz"); err == nil {
c.DataFromReader(http.StatusOK, st.Size(), "application/octet-stream", fd, map[string]string{
"Content-Encoding": "gzip",
})
return
}
}
}
c.File(filepath)
})
router.GET("/submissions/*_", func(c *gin.Context) {
http.ServeFile(c.Writer, c.Request, path.Join(api.TimestampCheck, strings.TrimPrefix(c.Request.URL.Path, path.Join(baseURL, "submissions"))))
})
router.GET("/vids/*_", func(c *gin.Context) {
if importer, ok := sync.GlobalImporter.(sync.DirectAccessImporter); ok {
http.ServeFile(c.Writer, c.Request, importer.GetLocalPath(strings.TrimPrefix(c.Request.URL.Path, path.Join(baseURL, "vids"))))
} else {
c.AbortWithError(http.StatusBadRequest, errors.New("Only available with local importer."))
}
})
router.GET("/check_import.html", func(c *gin.Context) {
serveFile(c, "check_import.html")
})
router.GET("/full_import_report.json", func(c *gin.Context) {
http.ServeFile(c.Writer, c.Request, sync.DeepReportPath)
})
}

View file

@ -0,0 +1,52 @@
<!DOCTYPE html>
<html>
<head>
<title>Rapport d'import FIC</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link rel="stylesheet" href="css/bootstrap.min.css">
<style>li:hover { background: lightgrey; }
</style>
<script type="text/javascript">
function disp(data) {
if (data["_updated"]) {
document.getElementById("date_imp").innerHTML = new Intl.DateTimeFormat(undefined, {weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric'}).format(new Date(data["_updated"][data["_updated"].length - 1]));
}
data.themes["_ALL"] = data._themes;
Object.keys(data.themes).map(function(theme) {
if (data.themes[theme] != null && theme != "_date") {
var title = document.createElement("h3");
title.id = theme;
title.innerHTML = theme;
document.getElementById("content").appendChild(title);
var row = document.createElement("ul");
row.type = "square";
for (var i = 0; i < data.themes[theme].length; i++) {
var col = document.createElement("li");
col.innerHTML = data.themes[theme][i];
row.appendChild(col);
}
document.getElementById("content").appendChild(row);
document.getElementById("content").appendChild(document.createElement("hr"));
}
});
}
</script>
</head>
<body class="container">
<h1>Rapport d'import FIC</h1>
<p>
<strong>Date du dernier import&nbsp;:</strong> <span id="date_imp"></span>
</p>
<div id="content"></div>
<script type="text/javascript">
fetch('full_import_report.json')
.then(function(response) {
return response.json();
})
.then(function(report) {
disp(report);
});
</script>
</body>
</html>

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,805 @@
@font-face {
font-family: 'Glyphicons Halflings';
src: url('../fonts/glyphicons-halflings-regular.eot');
src: url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('../fonts/glyphicons-halflings-regular.woff2') format('woff2'), url('../fonts/glyphicons-halflings-regular.woff') format('woff'), url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg');
}
.glyphicon {
position: relative;
top: 1px;
display: inline-block;
font-family: 'Glyphicons Halflings';
font-style: normal;
font-weight: normal;
line-height: 1;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.glyphicon-asterisk:before {
content: "\002a";
}
.glyphicon-plus:before {
content: "\002b";
}
.glyphicon-euro:before,
.glyphicon-eur:before {
content: "\20ac";
}
.glyphicon-minus:before {
content: "\2212";
}
.glyphicon-cloud:before {
content: "\2601";
}
.glyphicon-envelope:before {
content: "\2709";
}
.glyphicon-pencil:before {
content: "\270f";
}
.glyphicon-glass:before {
content: "\e001";
}
.glyphicon-music:before {
content: "\e002";
}
.glyphicon-search:before {
content: "\e003";
}
.glyphicon-heart:before {
content: "\e005";
}
.glyphicon-star:before {
content: "\e006";
}
.glyphicon-star-empty:before {
content: "\e007";
}
.glyphicon-user:before {
content: "\e008";
}
.glyphicon-film:before {
content: "\e009";
}
.glyphicon-th-large:before {
content: "\e010";
}
.glyphicon-th:before {
content: "\e011";
}
.glyphicon-th-list:before {
content: "\e012";
}
.glyphicon-ok:before {
content: "\e013";
}
.glyphicon-remove:before {
content: "\e014";
}
.glyphicon-zoom-in:before {
content: "\e015";
}
.glyphicon-zoom-out:before {
content: "\e016";
}
.glyphicon-off:before {
content: "\e017";
}
.glyphicon-signal:before {
content: "\e018";
}
.glyphicon-cog:before {
content: "\e019";
}
.glyphicon-trash:before {
content: "\e020";
}
.glyphicon-home:before {
content: "\e021";
}
.glyphicon-file:before {
content: "\e022";
}
.glyphicon-time:before {
content: "\e023";
}
.glyphicon-road:before {
content: "\e024";
}
.glyphicon-download-alt:before {
content: "\e025";
}
.glyphicon-download:before {
content: "\e026";
}
.glyphicon-upload:before {
content: "\e027";
}
.glyphicon-inbox:before {
content: "\e028";
}
.glyphicon-play-circle:before {
content: "\e029";
}
.glyphicon-repeat:before {
content: "\e030";
}
.glyphicon-refresh:before {
content: "\e031";
}
.glyphicon-list-alt:before {
content: "\e032";
}
.glyphicon-lock:before {
content: "\e033";
}
.glyphicon-flag:before {
content: "\e034";
}
.glyphicon-headphones:before {
content: "\e035";
}
.glyphicon-volume-off:before {
content: "\e036";
}
.glyphicon-volume-down:before {
content: "\e037";
}
.glyphicon-volume-up:before {
content: "\e038";
}
.glyphicon-qrcode:before {
content: "\e039";
}
.glyphicon-barcode:before {
content: "\e040";
}
.glyphicon-tag:before {
content: "\e041";
}
.glyphicon-tags:before {
content: "\e042";
}
.glyphicon-book:before {
content: "\e043";
}
.glyphicon-bookmark:before {
content: "\e044";
}
.glyphicon-print:before {
content: "\e045";
}
.glyphicon-camera:before {
content: "\e046";
}
.glyphicon-font:before {
content: "\e047";
}
.glyphicon-bold:before {
content: "\e048";
}
.glyphicon-italic:before {
content: "\e049";
}
.glyphicon-text-height:before {
content: "\e050";
}
.glyphicon-text-width:before {
content: "\e051";
}
.glyphicon-align-left:before {
content: "\e052";
}
.glyphicon-align-center:before {
content: "\e053";
}
.glyphicon-align-right:before {
content: "\e054";
}
.glyphicon-align-justify:before {
content: "\e055";
}
.glyphicon-list:before {
content: "\e056";
}
.glyphicon-indent-left:before {
content: "\e057";
}
.glyphicon-indent-right:before {
content: "\e058";
}
.glyphicon-facetime-video:before {
content: "\e059";
}
.glyphicon-picture:before {
content: "\e060";
}
.glyphicon-map-marker:before {
content: "\e062";
}
.glyphicon-adjust:before {
content: "\e063";
}
.glyphicon-tint:before {
content: "\e064";
}
.glyphicon-edit:before {
content: "\e065";
}
.glyphicon-share:before {
content: "\e066";
}
.glyphicon-check:before {
content: "\e067";
}
.glyphicon-move:before {
content: "\e068";
}
.glyphicon-step-backward:before {
content: "\e069";
}
.glyphicon-fast-backward:before {
content: "\e070";
}
.glyphicon-backward:before {
content: "\e071";
}
.glyphicon-play:before {
content: "\e072";
}
.glyphicon-pause:before {
content: "\e073";
}
.glyphicon-stop:before {
content: "\e074";
}
.glyphicon-forward:before {
content: "\e075";
}
.glyphicon-fast-forward:before {
content: "\e076";
}
.glyphicon-step-forward:before {
content: "\e077";
}
.glyphicon-eject:before {
content: "\e078";
}
.glyphicon-chevron-left:before {
content: "\e079";
}
.glyphicon-chevron-right:before {
content: "\e080";
}
.glyphicon-plus-sign:before {
content: "\e081";
}
.glyphicon-minus-sign:before {
content: "\e082";
}
.glyphicon-remove-sign:before {
content: "\e083";
}
.glyphicon-ok-sign:before {
content: "\e084";
}
.glyphicon-question-sign:before {
content: "\e085";
}
.glyphicon-info-sign:before {
content: "\e086";
}
.glyphicon-screenshot:before {
content: "\e087";
}
.glyphicon-remove-circle:before {
content: "\e088";
}
.glyphicon-ok-circle:before {
content: "\e089";
}
.glyphicon-ban-circle:before {
content: "\e090";
}
.glyphicon-arrow-left:before {
content: "\e091";
}
.glyphicon-arrow-right:before {
content: "\e092";
}
.glyphicon-arrow-up:before {
content: "\e093";
}
.glyphicon-arrow-down:before {
content: "\e094";
}
.glyphicon-share-alt:before {
content: "\e095";
}
.glyphicon-resize-full:before {
content: "\e096";
}
.glyphicon-resize-small:before {
content: "\e097";
}
.glyphicon-exclamation-sign:before {
content: "\e101";
}
.glyphicon-gift:before {
content: "\e102";
}
.glyphicon-leaf:before {
content: "\e103";
}
.glyphicon-fire:before {
content: "\e104";
}
.glyphicon-eye-open:before {
content: "\e105";
}
.glyphicon-eye-close:before {
content: "\e106";
}
.glyphicon-warning-sign:before {
content: "\e107";
}
.glyphicon-plane:before {
content: "\e108";
}
.glyphicon-calendar:before {
content: "\e109";
}
.glyphicon-random:before {
content: "\e110";
}
.glyphicon-comment:before {
content: "\e111";
}
.glyphicon-magnet:before {
content: "\e112";
}
.glyphicon-chevron-up:before {
content: "\e113";
}
.glyphicon-chevron-down:before {
content: "\e114";
}
.glyphicon-retweet:before {
content: "\e115";
}
.glyphicon-shopping-cart:before {
content: "\e116";
}
.glyphicon-folder-close:before {
content: "\e117";
}
.glyphicon-folder-open:before {
content: "\e118";
}
.glyphicon-resize-vertical:before {
content: "\e119";
}
.glyphicon-resize-horizontal:before {
content: "\e120";
}
.glyphicon-hdd:before {
content: "\e121";
}
.glyphicon-bullhorn:before {
content: "\e122";
}
.glyphicon-bell:before {
content: "\e123";
}
.glyphicon-certificate:before {
content: "\e124";
}
.glyphicon-thumbs-up:before {
content: "\e125";
}
.glyphicon-thumbs-down:before {
content: "\e126";
}
.glyphicon-hand-right:before {
content: "\e127";
}
.glyphicon-hand-left:before {
content: "\e128";
}
.glyphicon-hand-up:before {
content: "\e129";
}
.glyphicon-hand-down:before {
content: "\e130";
}
.glyphicon-circle-arrow-right:before {
content: "\e131";
}
.glyphicon-circle-arrow-left:before {
content: "\e132";
}
.glyphicon-circle-arrow-up:before {
content: "\e133";
}
.glyphicon-circle-arrow-down:before {
content: "\e134";
}
.glyphicon-globe:before {
content: "\e135";
}
.glyphicon-wrench:before {
content: "\e136";
}
.glyphicon-tasks:before {
content: "\e137";
}
.glyphicon-filter:before {
content: "\e138";
}
.glyphicon-briefcase:before {
content: "\e139";
}
.glyphicon-fullscreen:before {
content: "\e140";
}
.glyphicon-dashboard:before {
content: "\e141";
}
.glyphicon-paperclip:before {
content: "\e142";
}
.glyphicon-heart-empty:before {
content: "\e143";
}
.glyphicon-link:before {
content: "\e144";
}
.glyphicon-phone:before {
content: "\e145";
}
.glyphicon-pushpin:before {
content: "\e146";
}
.glyphicon-usd:before {
content: "\e148";
}
.glyphicon-gbp:before {
content: "\e149";
}
.glyphicon-sort:before {
content: "\e150";
}
.glyphicon-sort-by-alphabet:before {
content: "\e151";
}
.glyphicon-sort-by-alphabet-alt:before {
content: "\e152";
}
.glyphicon-sort-by-order:before {
content: "\e153";
}
.glyphicon-sort-by-order-alt:before {
content: "\e154";
}
.glyphicon-sort-by-attributes:before {
content: "\e155";
}
.glyphicon-sort-by-attributes-alt:before {
content: "\e156";
}
.glyphicon-unchecked:before {
content: "\e157";
}
.glyphicon-expand:before {
content: "\e158";
}
.glyphicon-collapse-down:before {
content: "\e159";
}
.glyphicon-collapse-up:before {
content: "\e160";
}
.glyphicon-log-in:before {
content: "\e161";
}
.glyphicon-flash:before {
content: "\e162";
}
.glyphicon-log-out:before {
content: "\e163";
}
.glyphicon-new-window:before {
content: "\e164";
}
.glyphicon-record:before {
content: "\e165";
}
.glyphicon-save:before {
content: "\e166";
}
.glyphicon-open:before {
content: "\e167";
}
.glyphicon-saved:before {
content: "\e168";
}
.glyphicon-import:before {
content: "\e169";
}
.glyphicon-export:before {
content: "\e170";
}
.glyphicon-send:before {
content: "\e171";
}
.glyphicon-floppy-disk:before {
content: "\e172";
}
.glyphicon-floppy-saved:before {
content: "\e173";
}
.glyphicon-floppy-remove:before {
content: "\e174";
}
.glyphicon-floppy-save:before {
content: "\e175";
}
.glyphicon-floppy-open:before {
content: "\e176";
}
.glyphicon-credit-card:before {
content: "\e177";
}
.glyphicon-transfer:before {
content: "\e178";
}
.glyphicon-cutlery:before {
content: "\e179";
}
.glyphicon-header:before {
content: "\e180";
}
.glyphicon-compressed:before {
content: "\e181";
}
.glyphicon-earphone:before {
content: "\e182";
}
.glyphicon-phone-alt:before {
content: "\e183";
}
.glyphicon-tower:before {
content: "\e184";
}
.glyphicon-stats:before {
content: "\e185";
}
.glyphicon-sd-video:before {
content: "\e186";
}
.glyphicon-hd-video:before {
content: "\e187";
}
.glyphicon-subtitles:before {
content: "\e188";
}
.glyphicon-sound-stereo:before {
content: "\e189";
}
.glyphicon-sound-dolby:before {
content: "\e190";
}
.glyphicon-sound-5-1:before {
content: "\e191";
}
.glyphicon-sound-6-1:before {
content: "\e192";
}
.glyphicon-sound-7-1:before {
content: "\e193";
}
.glyphicon-copyright-mark:before {
content: "\e194";
}
.glyphicon-registration-mark:before {
content: "\e195";
}
.glyphicon-cloud-download:before {
content: "\e197";
}
.glyphicon-cloud-upload:before {
content: "\e198";
}
.glyphicon-tree-conifer:before {
content: "\e199";
}
.glyphicon-tree-deciduous:before {
content: "\e200";
}
.glyphicon-cd:before {
content: "\e201";
}
.glyphicon-save-file:before {
content: "\e202";
}
.glyphicon-open-file:before {
content: "\e203";
}
.glyphicon-level-up:before {
content: "\e204";
}
.glyphicon-copy:before {
content: "\e205";
}
.glyphicon-paste:before {
content: "\e206";
}
.glyphicon-alert:before {
content: "\e209";
}
.glyphicon-equalizer:before {
content: "\e210";
}
.glyphicon-king:before {
content: "\e211";
}
.glyphicon-queen:before {
content: "\e212";
}
.glyphicon-pawn:before {
content: "\e213";
}
.glyphicon-bishop:before {
content: "\e214";
}
.glyphicon-knight:before {
content: "\e215";
}
.glyphicon-baby-formula:before {
content: "\e216";
}
.glyphicon-tent:before {
content: "\26fa";
}
.glyphicon-blackboard:before {
content: "\e218";
}
.glyphicon-bed:before {
content: "\e219";
}
.glyphicon-apple:before {
content: "\f8ff";
}
.glyphicon-erase:before {
content: "\e221";
}
.glyphicon-hourglass:before {
content: "\231b";
}
.glyphicon-lamp:before {
content: "\e223";
}
.glyphicon-duplicate:before {
content: "\e224";
}
.glyphicon-piggy-bank:before {
content: "\e225";
}
.glyphicon-scissors:before {
content: "\e226";
}
.glyphicon-bitcoin:before {
content: "\e227";
}
.glyphicon-btc:before {
content: "\e227";
}
.glyphicon-xbt:before {
content: "\e227";
}
.glyphicon-yen:before {
content: "\00a5";
}
.glyphicon-jpy:before {
content: "\00a5";
}
.glyphicon-ruble:before {
content: "\20bd";
}
.glyphicon-rub:before {
content: "\20bd";
}
.glyphicon-scale:before {
content: "\e230";
}
.glyphicon-ice-lolly:before {
content: "\e231";
}
.glyphicon-ice-lolly-tasted:before {
content: "\e232";
}
.glyphicon-education:before {
content: "\e233";
}
.glyphicon-option-horizontal:before {
content: "\e234";
}
.glyphicon-option-vertical:before {
content: "\e235";
}
.glyphicon-menu-hamburger:before {
content: "\e236";
}
.glyphicon-modal-window:before {
content: "\e237";
}
.glyphicon-oil:before {
content: "\e238";
}
.glyphicon-grain:before {
content: "\e239";
}
.glyphicon-sunglasses:before {
content: "\e240";
}
.glyphicon-text-size:before {
content: "\e241";
}
.glyphicon-text-color:before {
content: "\e242";
}
.glyphicon-text-background:before {
content: "\e243";
}
.glyphicon-object-align-top:before {
content: "\e244";
}
.glyphicon-object-align-bottom:before {
content: "\e245";
}
.glyphicon-object-align-horizontal:before {
content: "\e246";
}
.glyphicon-object-align-left:before {
content: "\e247";
}
.glyphicon-object-align-vertical:before {
content: "\e248";
}
.glyphicon-object-align-right:before {
content: "\e249";
}
.glyphicon-triangle-right:before {
content: "\e250";
}
.glyphicon-triangle-left:before {
content: "\e251";
}
.glyphicon-triangle-bottom:before {
content: "\e252";
}
.glyphicon-triangle-top:before {
content: "\e253";
}
.glyphicon-console:before {
content: "\e254";
}
.glyphicon-superscript:before {
content: "\e255";
}
.glyphicon-subscript:before {
content: "\e256";
}
.glyphicon-menu-left:before {
content: "\e257";
}
.glyphicon-menu-right:before {
content: "\e258";
}
.glyphicon-menu-down:before {
content: "\e259";
}
.glyphicon-menu-up:before {
content: "\e260";
}

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

View file

@ -1,51 +0,0 @@
<!DOCTYPE html>
<html ng-app="FICApp">
<head>
<meta charset="utf-8">
<title>Challenge Forensic - Administration</title>
<link href="/css/bootstrap.min.css" rel="stylesheet">
<base href="/">
<script src="/js/d3.v3.min.js"></script>
</head>
<body>
<nav class="navbar navbar-inverse navbar-static-top">
<div class="container">
<div class="navbar-header">
<a class="navbar-brand" href="/">
<img alt="FIC" src="/img/fic.png" style="height: 100%">
</a>
</div>
<ul class="nav navbar-nav">
<li><a href="/teams">&Eacute;quipes</a></li>
<li><a href="/themes">Thèmes</a></li>
<li><a href="/exercices">Exercices</a></li>
<li><a href="/events">&Eacute;vénements</a></li>
</ul>
<p id="clock" class="navbar-text navbar-right" ng-controller="CountdownController">
<span id="hours">{{ time.hours | time }}</span>
<span class="point">:</span>
<span id="min">{{ time.minutes | time }}</span>
<span class="point">:</span>
<span id="sec">{{ time.seconds | time }}</span>
</p>
</div>
</nav>
<div class="container">
<div class="row">
<div class="col-sm-12" ng-view></div>
</div>
</div>
<script src="/js/jquery.min.js"></script>
<script src="/js/bootstrap.min.js"></script>
<script src="/js/angular.min.js"></script>
<script src="/js/angular-resource.min.js"></script>
<script src="/js/angular-route.min.js"></script>
<script src="/js/app.js"></script>
</body>
</html>

View file

@ -1,14 +1,15 @@
/*
AngularJS v1.4.8
(c) 2010-2015 Google, Inc. http://angularjs.org
AngularJS v1.7.9
(c) 2010-2018 Google, Inc. http://angularjs.org
License: MIT
*/
(function(I,f,C){'use strict';function D(t,e){e=e||{};f.forEach(e,function(f,k){delete e[k]});for(var k in t)!t.hasOwnProperty(k)||"$"===k.charAt(0)&&"$"===k.charAt(1)||(e[k]=t[k]);return e}var y=f.$$minErr("$resource"),B=/^(\.[a-zA-Z_$@][0-9a-zA-Z_$@]*)+$/;f.module("ngResource",["ng"]).provider("$resource",function(){var t=/^https?:\/\/[^\/]*/,e=this;this.defaults={stripTrailingSlashes:!0,actions:{get:{method:"GET"},save:{method:"POST"},query:{method:"GET",isArray:!0},remove:{method:"DELETE"},"delete":{method:"DELETE"}}};
this.$get=["$http","$q",function(k,F){function w(f,g){this.template=f;this.defaults=r({},e.defaults,g);this.urlParams={}}function z(l,g,s,h){function c(a,q){var c={};q=r({},g,q);u(q,function(b,q){x(b)&&(b=b());var m;if(b&&b.charAt&&"@"==b.charAt(0)){m=a;var d=b.substr(1);if(null==d||""===d||"hasOwnProperty"===d||!B.test("."+d))throw y("badmember",d);for(var d=d.split("."),n=0,g=d.length;n<g&&f.isDefined(m);n++){var e=d[n];m=null!==m?m[e]:C}}else m=b;c[q]=m});return c}function G(a){return a.resource}
function d(a){D(a||{},this)}var t=new w(l,h);s=r({},e.defaults.actions,s);d.prototype.toJSON=function(){var a=r({},this);delete a.$promise;delete a.$resolved;return a};u(s,function(a,q){var g=/^(POST|PUT|PATCH)$/i.test(a.method);d[q]=function(b,A,m,e){var n={},h,l,s;switch(arguments.length){case 4:s=e,l=m;case 3:case 2:if(x(A)){if(x(b)){l=b;s=A;break}l=A;s=m}else{n=b;h=A;l=m;break}case 1:x(b)?l=b:g?h=b:n=b;break;case 0:break;default:throw y("badargs",arguments.length);}var w=this instanceof d,p=w?
h:a.isArray?[]:new d(h),v={},z=a.interceptor&&a.interceptor.response||G,B=a.interceptor&&a.interceptor.responseError||C;u(a,function(a,b){switch(b){default:v[b]=H(a);break;case "params":case "isArray":case "interceptor":break;case "timeout":v[b]=a}});g&&(v.data=h);t.setUrlParams(v,r({},c(h,a.params||{}),n),a.url);n=k(v).then(function(b){var c=b.data,m=p.$promise;if(c){if(f.isArray(c)!==!!a.isArray)throw y("badcfg",q,a.isArray?"array":"object",f.isArray(c)?"array":"object",v.method,v.url);a.isArray?
(p.length=0,u(c,function(b){"object"===typeof b?p.push(new d(b)):p.push(b)})):(D(c,p),p.$promise=m)}p.$resolved=!0;b.resource=p;return b},function(b){p.$resolved=!0;(s||E)(b);return F.reject(b)});n=n.then(function(b){var a=z(b);(l||E)(a,b.headers);return a},B);return w?n:(p.$promise=n,p.$resolved=!1,p)};d.prototype["$"+q]=function(b,a,c){x(b)&&(c=a,a=b,b={});b=d[q].call(this,b,this,a,c);return b.$promise||b}});d.bind=function(a){return z(l,r({},g,a),s)};return d}var E=f.noop,u=f.forEach,r=f.extend,
H=f.copy,x=f.isFunction;w.prototype={setUrlParams:function(l,g,e){var h=this,c=e||h.template,k,d,r="",a=h.urlParams={};u(c.split(/\W/),function(d){if("hasOwnProperty"===d)throw y("badname");!/^\d+$/.test(d)&&d&&(new RegExp("(^|[^\\\\]):"+d+"(\\W|$)")).test(c)&&(a[d]=!0)});c=c.replace(/\\:/g,":");c=c.replace(t,function(a){r=a;return""});g=g||{};u(h.urlParams,function(a,e){k=g.hasOwnProperty(e)?g[e]:h.defaults[e];f.isDefined(k)&&null!==k?(d=encodeURIComponent(k).replace(/%40/gi,"@").replace(/%3A/gi,
":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%20/g,"%20").replace(/%26/gi,"&").replace(/%3D/gi,"=").replace(/%2B/gi,"+"),c=c.replace(new RegExp(":"+e+"(\\W|$)","g"),function(b,a){return d+a})):c=c.replace(new RegExp("(/?):"+e+"(\\W|$)","g"),function(b,a,c){return"/"==c.charAt(0)?c:a+c})});h.defaults.stripTrailingSlashes&&(c=c.replace(/\/+$/,"")||"/");c=c.replace(/\/\.(?=\w+($|\?))/,".");l.url=r+c.replace(/\/\\\./,"/.");u(g,function(a,c){h.urlParams[c]||(l.params=l.params||{},l.params[c]=
a)})}};return z}]})})(window,window.angular);
(function(T,a){'use strict';function M(m,f){f=f||{};a.forEach(f,function(a,d){delete f[d]});for(var d in m)!m.hasOwnProperty(d)||"$"===d.charAt(0)&&"$"===d.charAt(1)||(f[d]=m[d]);return f}var B=a.$$minErr("$resource"),H=/^(\.[a-zA-Z_$@][0-9a-zA-Z_$@]*)+$/;a.module("ngResource",["ng"]).info({angularVersion:"1.7.9"}).provider("$resource",function(){var m=/^https?:\/\/\[[^\]]*][^/]*/,f=this;this.defaults={stripTrailingSlashes:!0,cancellable:!1,actions:{get:{method:"GET"},save:{method:"POST"},query:{method:"GET",
isArray:!0},remove:{method:"DELETE"},"delete":{method:"DELETE"}}};this.$get=["$http","$log","$q","$timeout",function(d,F,G,N){function C(a,d){this.template=a;this.defaults=n({},f.defaults,d);this.urlParams={}}var O=a.noop,r=a.forEach,n=a.extend,R=a.copy,P=a.isArray,D=a.isDefined,x=a.isFunction,I=a.isNumber,y=a.$$encodeUriQuery,S=a.$$encodeUriSegment;C.prototype={setUrlParams:function(a,d,f){var g=this,c=f||g.template,s,h,n="",b=g.urlParams=Object.create(null);r(c.split(/\W/),function(a){if("hasOwnProperty"===
a)throw B("badname");!/^\d+$/.test(a)&&a&&(new RegExp("(^|[^\\\\]):"+a+"(\\W|$)")).test(c)&&(b[a]={isQueryParamValue:(new RegExp("\\?.*=:"+a+"(?:\\W|$)")).test(c)})});c=c.replace(/\\:/g,":");c=c.replace(m,function(b){n=b;return""});d=d||{};r(g.urlParams,function(b,a){s=d.hasOwnProperty(a)?d[a]:g.defaults[a];D(s)&&null!==s?(h=b.isQueryParamValue?y(s,!0):S(s),c=c.replace(new RegExp(":"+a+"(\\W|$)","g"),function(b,a){return h+a})):c=c.replace(new RegExp("(/?):"+a+"(\\W|$)","g"),function(b,a,e){return"/"===
e.charAt(0)?e:a+e})});g.defaults.stripTrailingSlashes&&(c=c.replace(/\/+$/,"")||"/");c=c.replace(/\/\.(?=\w+($|\?))/,".");a.url=n+c.replace(/\/(\\|%5C)\./,"/.");r(d,function(b,c){g.urlParams[c]||(a.params=a.params||{},a.params[c]=b)})}};return function(m,y,z,g){function c(b,c){var d={};c=n({},y,c);r(c,function(c,f){x(c)&&(c=c(b));var e;if(c&&c.charAt&&"@"===c.charAt(0)){e=b;var k=c.substr(1);if(null==k||""===k||"hasOwnProperty"===k||!H.test("."+k))throw B("badmember",k);for(var k=k.split("."),h=0,
n=k.length;h<n&&a.isDefined(e);h++){var g=k[h];e=null!==e?e[g]:void 0}}else e=c;d[f]=e});return d}function s(b){return b.resource}function h(b){M(b||{},this)}var Q=new C(m,g);z=n({},f.defaults.actions,z);h.prototype.toJSON=function(){var b=n({},this);delete b.$promise;delete b.$resolved;delete b.$cancelRequest;return b};r(z,function(b,a){var f=!0===b.hasBody||!1!==b.hasBody&&/^(POST|PUT|PATCH)$/i.test(b.method),g=b.timeout,m=D(b.cancellable)?b.cancellable:Q.defaults.cancellable;g&&!I(g)&&(F.debug("ngResource:\n Only numeric values are allowed as `timeout`.\n Promises are not supported in $resource, because the same value would be used for multiple requests. If you are looking for a way to cancel requests, you should use the `cancellable` option."),
delete b.timeout,g=null);h[a]=function(e,k,J,y){function z(a){p.catch(O);null!==u&&u.resolve(a)}var K={},v,t,w;switch(arguments.length){case 4:w=y,t=J;case 3:case 2:if(x(k)){if(x(e)){t=e;w=k;break}t=k;w=J}else{K=e;v=k;t=J;break}case 1:x(e)?t=e:f?v=e:K=e;break;case 0:break;default:throw B("badargs",arguments.length);}var E=this instanceof h,l=E?v:b.isArray?[]:new h(v),q={},C=b.interceptor&&b.interceptor.request||void 0,D=b.interceptor&&b.interceptor.requestError||void 0,F=b.interceptor&&b.interceptor.response||
s,H=b.interceptor&&b.interceptor.responseError||G.reject,I=t?function(a){t(a,A.headers,A.status,A.statusText)}:void 0;w=w||void 0;var u,L,A;r(b,function(a,b){switch(b){default:q[b]=R(a);case "params":case "isArray":case "interceptor":case "cancellable":}});!E&&m&&(u=G.defer(),q.timeout=u.promise,g&&(L=N(u.resolve,g)));f&&(q.data=v);Q.setUrlParams(q,n({},c(v,b.params||{}),K),b.url);var p=G.resolve(q).then(C).catch(D).then(d),p=p.then(function(c){var e=c.data;if(e){if(P(e)!==!!b.isArray)throw B("badcfg",
a,b.isArray?"array":"object",P(e)?"array":"object",q.method,q.url);if(b.isArray)l.length=0,r(e,function(a){"object"===typeof a?l.push(new h(a)):l.push(a)});else{var d=l.$promise;M(e,l);l.$promise=d}}c.resource=l;A=c;return F(c)},function(a){a.resource=l;A=a;return H(a)}),p=p["finally"](function(){l.$resolved=!0;!E&&m&&(l.$cancelRequest=O,N.cancel(L),u=L=q.timeout=null)});p.then(I,w);return E?p:(l.$promise=p,l.$resolved=!1,m&&(l.$cancelRequest=z),l)};h.prototype["$"+a]=function(b,c,d){x(b)&&(d=c,c=
b,b={});b=h[a].call(this,b,this,c,d);return b.$promise||b}});return h}}]})})(window,window.angular);
//# sourceMappingURL=angular-resource.min.js.map

View file

@ -1 +0,0 @@
../../../frontend/static/js/angular-route.min.js

17
admin/static/js/angular-route.min.js vendored Normal file
View file

@ -0,0 +1,17 @@
/*
AngularJS v1.7.9
(c) 2010-2018 Google, Inc. http://angularjs.org
License: MIT
*/
(function(I,b){'use strict';function z(b,h){var d=[],c=b.replace(/([().])/g,"\\$1").replace(/(\/)?:(\w+)(\*\?|[?*])?/g,function(b,c,h,k){b="?"===k||"*?"===k;k="*"===k||"*?"===k;d.push({name:h,optional:b});c=c||"";return(b?"(?:"+c:c+"(?:")+(k?"(.+?)":"([^/]+)")+(b?"?)?":")")}).replace(/([/$*])/g,"\\$1");h.ignoreTrailingSlashes&&(c=c.replace(/\/+$/,"")+"/*");return{keys:d,regexp:new RegExp("^"+c+"(?:[?#]|$)",h.caseInsensitiveMatch?"i":"")}}function A(b){p&&b.get("$route")}function v(u,h,d){return{restrict:"ECA",
terminal:!0,priority:400,transclude:"element",link:function(c,f,g,l,k){function q(){r&&(d.cancel(r),r=null);m&&(m.$destroy(),m=null);s&&(r=d.leave(s),r.done(function(b){!1!==b&&(r=null)}),s=null)}function C(){var g=u.current&&u.current.locals;if(b.isDefined(g&&g.$template)){var g=c.$new(),l=u.current;s=k(g,function(g){d.enter(g,null,s||f).done(function(d){!1===d||!b.isDefined(w)||w&&!c.$eval(w)||h()});q()});m=l.scope=g;m.$emit("$viewContentLoaded");m.$eval(p)}else q()}var m,s,r,w=g.autoscroll,p=g.onload||
"";c.$on("$routeChangeSuccess",C);C()}}}function x(b,h,d){return{restrict:"ECA",priority:-400,link:function(c,f){var g=d.current,l=g.locals;f.html(l.$template);var k=b(f.contents());if(g.controller){l.$scope=c;var q=h(g.controller,l);g.controllerAs&&(c[g.controllerAs]=q);f.data("$ngControllerController",q);f.children().data("$ngControllerController",q)}c[g.resolveAs||"$resolve"]=l;k(c)}}}var D,E,F,G,y=b.module("ngRoute",[]).info({angularVersion:"1.7.9"}).provider("$route",function(){function u(d,
c){return b.extend(Object.create(d),c)}D=b.isArray;E=b.isObject;F=b.isDefined;G=b.noop;var h={};this.when=function(d,c){var f;f=void 0;if(D(c)){f=f||[];for(var g=0,l=c.length;g<l;g++)f[g]=c[g]}else if(E(c))for(g in f=f||{},c)if("$"!==g.charAt(0)||"$"!==g.charAt(1))f[g]=c[g];f=f||c;b.isUndefined(f.reloadOnUrl)&&(f.reloadOnUrl=!0);b.isUndefined(f.reloadOnSearch)&&(f.reloadOnSearch=!0);b.isUndefined(f.caseInsensitiveMatch)&&(f.caseInsensitiveMatch=this.caseInsensitiveMatch);h[d]=b.extend(f,{originalPath:d},
d&&z(d,f));d&&(g="/"===d[d.length-1]?d.substr(0,d.length-1):d+"/",h[g]=b.extend({originalPath:d,redirectTo:d},z(g,f)));return this};this.caseInsensitiveMatch=!1;this.otherwise=function(b){"string"===typeof b&&(b={redirectTo:b});this.when(null,b);return this};p=!0;this.eagerInstantiationEnabled=function(b){return F(b)?(p=b,this):p};this.$get=["$rootScope","$location","$routeParams","$q","$injector","$templateRequest","$sce","$browser",function(d,c,f,g,l,k,q,p){function m(a){var e=t.current;n=A();(x=
!B&&n&&e&&n.$$route===e.$$route&&(!n.reloadOnUrl||!n.reloadOnSearch&&b.equals(n.pathParams,e.pathParams)))||!e&&!n||d.$broadcast("$routeChangeStart",n,e).defaultPrevented&&a&&a.preventDefault()}function s(){var a=t.current,e=n;if(x)a.params=e.params,b.copy(a.params,f),d.$broadcast("$routeUpdate",a);else if(e||a){B=!1;t.current=e;var c=g.resolve(e);p.$$incOutstandingRequestCount("$route");c.then(r).then(w).then(function(g){return g&&c.then(y).then(function(c){e===t.current&&(e&&(e.locals=c,b.copy(e.params,
f)),d.$broadcast("$routeChangeSuccess",e,a))})}).catch(function(b){e===t.current&&d.$broadcast("$routeChangeError",e,a,b)}).finally(function(){p.$$completeOutstandingRequest(G,"$route")})}}function r(a){var e={route:a,hasRedirection:!1};if(a)if(a.redirectTo)if(b.isString(a.redirectTo))e.path=v(a.redirectTo,a.params),e.search=a.params,e.hasRedirection=!0;else{var d=c.path(),f=c.search();a=a.redirectTo(a.pathParams,d,f);b.isDefined(a)&&(e.url=a,e.hasRedirection=!0)}else if(a.resolveRedirectTo)return g.resolve(l.invoke(a.resolveRedirectTo)).then(function(a){b.isDefined(a)&&
(e.url=a,e.hasRedirection=!0);return e});return e}function w(a){var b=!0;if(a.route!==t.current)b=!1;else if(a.hasRedirection){var g=c.url(),d=a.url;d?c.url(d).replace():d=c.path(a.path).search(a.search).replace().url();d!==g&&(b=!1)}return b}function y(a){if(a){var e=b.extend({},a.resolve);b.forEach(e,function(a,c){e[c]=b.isString(a)?l.get(a):l.invoke(a,null,null,c)});a=z(a);b.isDefined(a)&&(e.$template=a);return g.all(e)}}function z(a){var e,c;b.isDefined(e=a.template)?b.isFunction(e)&&(e=e(a.params)):
b.isDefined(c=a.templateUrl)&&(b.isFunction(c)&&(c=c(a.params)),b.isDefined(c)&&(a.loadedTemplateUrl=q.valueOf(c),e=k(c)));return e}function A(){var a,e;b.forEach(h,function(d,g){var f;if(f=!e){var h=c.path();f=d.keys;var l={};if(d.regexp)if(h=d.regexp.exec(h)){for(var k=1,p=h.length;k<p;++k){var m=f[k-1],n=h[k];m&&n&&(l[m.name]=n)}f=l}else f=null;else f=null;f=a=f}f&&(e=u(d,{params:b.extend({},c.search(),a),pathParams:a}),e.$$route=d)});return e||h[null]&&u(h[null],{params:{},pathParams:{}})}function v(a,
c){var d=[];b.forEach((a||"").split(":"),function(a,b){if(0===b)d.push(a);else{var f=a.match(/(\w+)(?:[?*])?(.*)/),g=f[1];d.push(c[g]);d.push(f[2]||"");delete c[g]}});return d.join("")}var B=!1,n,x,t={routes:h,reload:function(){B=!0;var a={defaultPrevented:!1,preventDefault:function(){this.defaultPrevented=!0;B=!1}};d.$evalAsync(function(){m(a);a.defaultPrevented||s()})},updateParams:function(a){if(this.current&&this.current.$$route)a=b.extend({},this.current.params,a),c.path(v(this.current.$$route.originalPath,
a)),c.search(a);else throw H("norout");}};d.$on("$locationChangeStart",m);d.$on("$locationChangeSuccess",s);return t}]}).run(A),H=b.$$minErr("ngRoute"),p;A.$inject=["$injector"];y.provider("$routeParams",function(){this.$get=function(){return{}}});y.directive("ngView",v);y.directive("ngView",x);v.$inject=["$route","$anchorScroll","$animate"];x.$inject=["$compile","$controller","$route"]})(window,window.angular);
//# sourceMappingURL=angular-route.min.js.map

18
admin/static/js/angular-sanitize.min.js vendored Normal file
View file

@ -0,0 +1,18 @@
/*
AngularJS v1.7.9
(c) 2010-2018 Google, Inc. http://angularjs.org
License: MIT
*/
(function(s,c){'use strict';function P(c){var h=[];C(h,E).chars(c);return h.join("")}var D=c.$$minErr("$sanitize"),F,h,G,H,I,q,E,J,K,C;c.module("ngSanitize",[]).provider("$sanitize",function(){function f(a,e){return B(a.split(","),e)}function B(a,e){var d={},b;for(b=0;b<a.length;b++)d[e?q(a[b]):a[b]]=!0;return d}function t(a,e){e&&e.length&&h(a,B(e))}function Q(a){for(var e={},d=0,b=a.length;d<b;d++){var k=a[d];e[k.name]=k.value}return e}function L(a){return a.replace(/&/g,"&amp;").replace(z,function(a){var d=
a.charCodeAt(0);a=a.charCodeAt(1);return"&#"+(1024*(d-55296)+(a-56320)+65536)+";"}).replace(u,function(a){return"&#"+a.charCodeAt(0)+";"}).replace(/</g,"&lt;").replace(/>/g,"&gt;")}function A(a){for(;a;){if(a.nodeType===s.Node.ELEMENT_NODE)for(var e=a.attributes,d=0,b=e.length;d<b;d++){var k=e[d],g=k.name.toLowerCase();if("xmlns:ns1"===g||0===g.lastIndexOf("ns1:",0))a.removeAttributeNode(k),d--,b--}(e=a.firstChild)&&A(e);a=v("nextSibling",a)}}function v(a,e){var d=e[a];if(d&&J.call(e,d))throw D("elclob",
e.outerHTML||e.outerText);return d}var y=!1,g=!1;this.$get=["$$sanitizeUri",function(a){y=!0;g&&h(m,l);return function(e){var d=[];K(e,C(d,function(b,d){return!/^unsafe:/.test(a(b,d))}));return d.join("")}}];this.enableSvg=function(a){return I(a)?(g=a,this):g};this.addValidElements=function(a){y||(H(a)&&(a={htmlElements:a}),t(l,a.svgElements),t(r,a.htmlVoidElements),t(m,a.htmlVoidElements),t(m,a.htmlElements));return this};this.addValidAttrs=function(a){y||h(M,B(a,!0));return this};F=c.bind;h=c.extend;
G=c.forEach;H=c.isArray;I=c.isDefined;q=c.$$lowercase;E=c.noop;K=function(a,e){null===a||void 0===a?a="":"string"!==typeof a&&(a=""+a);var d=N(a);if(!d)return"";var b=5;do{if(0===b)throw D("uinput");b--;a=d.innerHTML;d=N(a)}while(a!==d.innerHTML);for(b=d.firstChild;b;){switch(b.nodeType){case 1:e.start(b.nodeName.toLowerCase(),Q(b.attributes));break;case 3:e.chars(b.textContent)}var k;if(!(k=b.firstChild)&&(1===b.nodeType&&e.end(b.nodeName.toLowerCase()),k=v("nextSibling",b),!k))for(;null==k;){b=
v("parentNode",b);if(b===d)break;k=v("nextSibling",b);1===b.nodeType&&e.end(b.nodeName.toLowerCase())}b=k}for(;b=d.firstChild;)d.removeChild(b)};C=function(a,e){var d=!1,b=F(a,a.push);return{start:function(a,g){a=q(a);!d&&w[a]&&(d=a);d||!0!==m[a]||(b("<"),b(a),G(g,function(d,g){var c=q(g),f="img"===a&&"src"===c||"background"===c;!0!==M[c]||!0===O[c]&&!e(d,f)||(b(" "),b(g),b('="'),b(L(d)),b('"'))}),b(">"))},end:function(a){a=q(a);d||!0!==m[a]||!0===r[a]||(b("</"),b(a),b(">"));a==d&&(d=!1)},chars:function(a){d||
b(L(a))}}};J=s.Node.prototype.contains||function(a){return!!(this.compareDocumentPosition(a)&16)};var z=/[\uD800-\uDBFF][\uDC00-\uDFFF]/g,u=/([^#-~ |!])/g,r=f("area,br,col,hr,img,wbr"),x=f("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"),p=f("rp,rt"),n=h({},p,x),x=h({},x,f("address,article,aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,section,table,ul")),p=h({},p,f("a,abbr,acronym,b,bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s,samp,small,span,strike,strong,sub,sup,time,tt,u,var")),
l=f("circle,defs,desc,ellipse,font-face,font-face-name,font-face-src,g,glyph,hkern,image,linearGradient,line,marker,metadata,missing-glyph,mpath,path,polygon,polyline,radialGradient,rect,stop,svg,switch,text,title,tspan"),w=f("script,style"),m=h({},r,x,p,n),O=f("background,cite,href,longdesc,src,xlink:href,xml:base"),n=f("abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,scope,scrolling,shape,size,span,start,summary,tabindex,target,title,type,valign,value,vspace,width"),
p=f("accent-height,accumulate,additive,alphabetic,arabic-form,ascent,baseProfile,bbox,begin,by,calcMode,cap-height,class,color,color-rendering,content,cx,cy,d,dx,dy,descent,display,dur,end,fill,fill-rule,font-family,font-size,font-stretch,font-style,font-variant,font-weight,from,fx,fy,g1,g2,glyph-name,gradientUnits,hanging,height,horiz-adv-x,horiz-origin-x,ideographic,k,keyPoints,keySplines,keyTimes,lang,marker-end,marker-mid,marker-start,markerHeight,markerUnits,markerWidth,mathematical,max,min,offset,opacity,orient,origin,overline-position,overline-thickness,panose-1,path,pathLength,points,preserveAspectRatio,r,refX,refY,repeatCount,repeatDur,requiredExtensions,requiredFeatures,restart,rotate,rx,ry,slope,stemh,stemv,stop-color,stop-opacity,strikethrough-position,strikethrough-thickness,stroke,stroke-dasharray,stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,stroke-opacity,stroke-width,systemLanguage,target,text-anchor,to,transform,type,u1,u2,underline-position,underline-thickness,unicode,unicode-range,units-per-em,values,version,viewBox,visibility,width,widths,x,x-height,x1,x2,xlink:actuate,xlink:arcrole,xlink:role,xlink:show,xlink:title,xlink:type,xml:base,xml:lang,xml:space,xmlns,xmlns:xlink,y,y1,y2,zoomAndPan",
!0),M=h({},O,p,n),N=function(a,e){function d(b){b="<remove></remove>"+b;try{var d=(new a.DOMParser).parseFromString(b,"text/html").body;d.firstChild.remove();return d}catch(e){}}function b(a){c.innerHTML=a;e.documentMode&&A(c);return c}var g;if(e&&e.implementation)g=e.implementation.createHTMLDocument("inert");else throw D("noinert");var c=(g.documentElement||g.getDocumentElement()).querySelector("body");c.innerHTML='<svg><g onload="this.parentNode.remove()"></g></svg>';return c.querySelector("svg")?
(c.innerHTML='<svg><p><style><img src="</style><img src=x onerror=alert(1)//">',c.querySelector("svg img")?d:b):function(b){b="<remove></remove>"+b;try{b=encodeURI(b)}catch(d){return}var e=new a.XMLHttpRequest;e.responseType="document";e.open("GET","data:text/html;charset=utf-8,"+b,!1);e.send(null);b=e.response.body;b.firstChild.remove();return b}}(s,s.document)}).info({angularVersion:"1.7.9"});c.module("ngSanitize").filter("linky",["$sanitize",function(f){var h=/((s?ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"\u201d\u2019]/i,
t=/^mailto:/i,q=c.$$minErr("linky"),s=c.isDefined,A=c.isFunction,v=c.isObject,y=c.isString;return function(c,z,u){function r(c){c&&l.push(P(c))}function x(c,g){var f,a=p(c);l.push("<a ");for(f in a)l.push(f+'="'+a[f]+'" ');!s(z)||"target"in a||l.push('target="',z,'" ');l.push('href="',c.replace(/"/g,"&quot;"),'">');r(g);l.push("</a>")}if(null==c||""===c)return c;if(!y(c))throw q("notstring",c);for(var p=A(u)?u:v(u)?function(){return u}:function(){return{}},n=c,l=[],w,m;c=n.match(h);)w=c[0],c[2]||
c[4]||(w=(c[3]?"http://":"mailto:")+w),m=c.index,r(n.substr(0,m)),x(w,c[0].replace(t,"")),n=n.substring(m+c[0].length);r(n);return f(l.join(""))}}])})(window,window.angular);
//# sourceMappingURL=angular-sanitize.min.js.map

View file

@ -1 +0,0 @@
../../../frontend/static/js/angular.min.js

350
admin/static/js/angular.min.js vendored Normal file
View file

@ -0,0 +1,350 @@
/*
AngularJS v1.7.9
(c) 2010-2018 Google, Inc. http://angularjs.org
License: MIT
*/
(function(C){'use strict';function re(a){if(D(a))w(a.objectMaxDepth)&&(Wb.objectMaxDepth=Xb(a.objectMaxDepth)?a.objectMaxDepth:NaN),w(a.urlErrorParamsEnabled)&&Ga(a.urlErrorParamsEnabled)&&(Wb.urlErrorParamsEnabled=a.urlErrorParamsEnabled);else return Wb}function Xb(a){return W(a)&&0<a}function F(a,b){b=b||Error;return function(){var d=arguments[0],c;c="["+(a?a+":":"")+d+"] http://errors.angularjs.org/1.7.9/"+(a?a+"/":"")+d;for(d=1;d<arguments.length;d++){c=c+(1==d?"?":"&")+"p"+(d-1)+"=";var e=encodeURIComponent,
f;f=arguments[d];f="function"==typeof f?f.toString().replace(/ \{[\s\S]*$/,""):"undefined"==typeof f?"undefined":"string"!=typeof f?JSON.stringify(f):f;c+=e(f)}return new b(c)}}function ya(a){if(null==a||$a(a))return!1;if(H(a)||A(a)||x&&a instanceof x)return!0;var b="length"in Object(a)&&a.length;return W(b)&&(0<=b&&b-1 in a||"function"===typeof a.item)}function r(a,b,d){var c,e;if(a)if(B(a))for(c in a)"prototype"!==c&&"length"!==c&&"name"!==c&&a.hasOwnProperty(c)&&b.call(d,a[c],c,a);else if(H(a)||
ya(a)){var f="object"!==typeof a;c=0;for(e=a.length;c<e;c++)(f||c in a)&&b.call(d,a[c],c,a)}else if(a.forEach&&a.forEach!==r)a.forEach(b,d,a);else if(Nc(a))for(c in a)b.call(d,a[c],c,a);else if("function"===typeof a.hasOwnProperty)for(c in a)a.hasOwnProperty(c)&&b.call(d,a[c],c,a);else for(c in a)ta.call(a,c)&&b.call(d,a[c],c,a);return a}function Oc(a,b,d){for(var c=Object.keys(a).sort(),e=0;e<c.length;e++)b.call(d,a[c[e]],c[e]);return c}function Yb(a){return function(b,d){a(d,b)}}function se(){return++pb}
function Zb(a,b,d){for(var c=a.$$hashKey,e=0,f=b.length;e<f;++e){var g=b[e];if(D(g)||B(g))for(var k=Object.keys(g),h=0,l=k.length;h<l;h++){var m=k[h],p=g[m];d&&D(p)?ha(p)?a[m]=new Date(p.valueOf()):ab(p)?a[m]=new RegExp(p):p.nodeName?a[m]=p.cloneNode(!0):$b(p)?a[m]=p.clone():"__proto__"!==m&&(D(a[m])||(a[m]=H(p)?[]:{}),Zb(a[m],[p],!0)):a[m]=p}}c?a.$$hashKey=c:delete a.$$hashKey;return a}function S(a){return Zb(a,Ha.call(arguments,1),!1)}function te(a){return Zb(a,Ha.call(arguments,1),!0)}function fa(a){return parseInt(a,
10)}function ac(a,b){return S(Object.create(a),b)}function E(){}function Ta(a){return a}function ia(a){return function(){return a}}function bc(a){return B(a.toString)&&a.toString!==la}function z(a){return"undefined"===typeof a}function w(a){return"undefined"!==typeof a}function D(a){return null!==a&&"object"===typeof a}function Nc(a){return null!==a&&"object"===typeof a&&!Pc(a)}function A(a){return"string"===typeof a}function W(a){return"number"===typeof a}function ha(a){return"[object Date]"===la.call(a)}
function H(a){return Array.isArray(a)||a instanceof Array}function cc(a){switch(la.call(a)){case "[object Error]":return!0;case "[object Exception]":return!0;case "[object DOMException]":return!0;default:return a instanceof Error}}function B(a){return"function"===typeof a}function ab(a){return"[object RegExp]"===la.call(a)}function $a(a){return a&&a.window===a}function bb(a){return a&&a.$evalAsync&&a.$watch}function Ga(a){return"boolean"===typeof a}function ue(a){return a&&W(a.length)&&ve.test(la.call(a))}
function $b(a){return!(!a||!(a.nodeName||a.prop&&a.attr&&a.find))}function we(a){var b={};a=a.split(",");var d;for(d=0;d<a.length;d++)b[a[d]]=!0;return b}function ua(a){return K(a.nodeName||a[0]&&a[0].nodeName)}function cb(a,b){var d=a.indexOf(b);0<=d&&a.splice(d,1);return d}function Ia(a,b,d){function c(a,b,c){c--;if(0>c)return"...";var d=b.$$hashKey,f;if(H(a)){f=0;for(var g=a.length;f<g;f++)b.push(e(a[f],c))}else if(Nc(a))for(f in a)b[f]=e(a[f],c);else if(a&&"function"===typeof a.hasOwnProperty)for(f in a)a.hasOwnProperty(f)&&
(b[f]=e(a[f],c));else for(f in a)ta.call(a,f)&&(b[f]=e(a[f],c));d?b.$$hashKey=d:delete b.$$hashKey;return b}function e(a,b){if(!D(a))return a;var d=g.indexOf(a);if(-1!==d)return k[d];if($a(a)||bb(a))throw pa("cpws");var d=!1,e=f(a);void 0===e&&(e=H(a)?[]:Object.create(Pc(a)),d=!0);g.push(a);k.push(e);return d?c(a,e,b):e}function f(a){switch(la.call(a)){case "[object Int8Array]":case "[object Int16Array]":case "[object Int32Array]":case "[object Float32Array]":case "[object Float64Array]":case "[object Uint8Array]":case "[object Uint8ClampedArray]":case "[object Uint16Array]":case "[object Uint32Array]":return new a.constructor(e(a.buffer),
a.byteOffset,a.length);case "[object ArrayBuffer]":if(!a.slice){var b=new ArrayBuffer(a.byteLength);(new Uint8Array(b)).set(new Uint8Array(a));return b}return a.slice(0);case "[object Boolean]":case "[object Number]":case "[object String]":case "[object Date]":return new a.constructor(a.valueOf());case "[object RegExp]":return b=new RegExp(a.source,a.toString().match(/[^/]*$/)[0]),b.lastIndex=a.lastIndex,b;case "[object Blob]":return new a.constructor([a],{type:a.type})}if(B(a.cloneNode))return a.cloneNode(!0)}
var g=[],k=[];d=Xb(d)?d:NaN;if(b){if(ue(b)||"[object ArrayBuffer]"===la.call(b))throw pa("cpta");if(a===b)throw pa("cpi");H(b)?b.length=0:r(b,function(a,c){"$$hashKey"!==c&&delete b[c]});g.push(a);k.push(b);return c(a,b,d)}return e(a,d)}function dc(a,b){return a===b||a!==a&&b!==b}function va(a,b){if(a===b)return!0;if(null===a||null===b)return!1;if(a!==a&&b!==b)return!0;var d=typeof a,c;if(d===typeof b&&"object"===d)if(H(a)){if(!H(b))return!1;if((d=a.length)===b.length){for(c=0;c<d;c++)if(!va(a[c],
b[c]))return!1;return!0}}else{if(ha(a))return ha(b)?dc(a.getTime(),b.getTime()):!1;if(ab(a))return ab(b)?a.toString()===b.toString():!1;if(bb(a)||bb(b)||$a(a)||$a(b)||H(b)||ha(b)||ab(b))return!1;d=T();for(c in a)if("$"!==c.charAt(0)&&!B(a[c])){if(!va(a[c],b[c]))return!1;d[c]=!0}for(c in b)if(!(c in d)&&"$"!==c.charAt(0)&&w(b[c])&&!B(b[c]))return!1;return!0}return!1}function db(a,b,d){return a.concat(Ha.call(b,d))}function Va(a,b){var d=2<arguments.length?Ha.call(arguments,2):[];return!B(b)||b instanceof
RegExp?b:d.length?function(){return arguments.length?b.apply(a,db(d,arguments,0)):b.apply(a,d)}:function(){return arguments.length?b.apply(a,arguments):b.call(a)}}function Qc(a,b){var d=b;"string"===typeof a&&"$"===a.charAt(0)&&"$"===a.charAt(1)?d=void 0:$a(b)?d="$WINDOW":b&&C.document===b?d="$DOCUMENT":bb(b)&&(d="$SCOPE");return d}function eb(a,b){if(!z(a))return W(b)||(b=b?2:null),JSON.stringify(a,Qc,b)}function Rc(a){return A(a)?JSON.parse(a):a}function ec(a,b){a=a.replace(xe,"");var d=Date.parse("Jan 01, 1970 00:00:00 "+
a)/6E4;return X(d)?b:d}function Sc(a,b){a=new Date(a.getTime());a.setMinutes(a.getMinutes()+b);return a}function fc(a,b,d){d=d?-1:1;var c=a.getTimezoneOffset();b=ec(b,c);return Sc(a,d*(b-c))}function za(a){a=x(a).clone().empty();var b=x("<div></div>").append(a).html();try{return a[0].nodeType===Pa?K(b):b.match(/^(<[^>]+>)/)[1].replace(/^<([\w-]+)/,function(a,b){return"<"+K(b)})}catch(d){return K(b)}}function Tc(a){try{return decodeURIComponent(a)}catch(b){}}function gc(a){var b={};r((a||"").split("&"),
function(a){var c,e,f;a&&(e=a=a.replace(/\+/g,"%20"),c=a.indexOf("="),-1!==c&&(e=a.substring(0,c),f=a.substring(c+1)),e=Tc(e),w(e)&&(f=w(f)?Tc(f):!0,ta.call(b,e)?H(b[e])?b[e].push(f):b[e]=[b[e],f]:b[e]=f))});return b}function ye(a){var b=[];r(a,function(a,c){H(a)?r(a,function(a){b.push(ba(c,!0)+(!0===a?"":"="+ba(a,!0)))}):b.push(ba(c,!0)+(!0===a?"":"="+ba(a,!0)))});return b.length?b.join("&"):""}function hc(a){return ba(a,!0).replace(/%26/gi,"&").replace(/%3D/gi,"=").replace(/%2B/gi,"+")}function ba(a,
b){return encodeURIComponent(a).replace(/%40/gi,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%3B/gi,";").replace(/%20/g,b?"%20":"+")}function ze(a,b){var d,c,e=Qa.length;for(c=0;c<e;++c)if(d=Qa[c]+b,A(d=a.getAttribute(d)))return d;return null}function Ae(a,b){var d,c,e={};r(Qa,function(b){b+="app";!d&&a.hasAttribute&&a.hasAttribute(b)&&(d=a,c=a.getAttribute(b))});r(Qa,function(b){b+="app";var e;!d&&(e=a.querySelector("["+b.replace(":","\\:")+"]"))&&(d=e,c=e.getAttribute(b))});
d&&(Be?(e.strictDi=null!==ze(d,"strict-di"),b(d,c?[c]:[],e)):C.console.error("AngularJS: disabling automatic bootstrap. <script> protocol indicates an extension, document.location.href does not match."))}function Uc(a,b,d){D(d)||(d={});d=S({strictDi:!1},d);var c=function(){a=x(a);if(a.injector()){var c=a[0]===C.document?"document":za(a);throw pa("btstrpd",c.replace(/</,"&lt;").replace(/>/,"&gt;"));}b=b||[];b.unshift(["$provide",function(b){b.value("$rootElement",a)}]);d.debugInfoEnabled&&b.push(["$compileProvider",
function(a){a.debugInfoEnabled(!0)}]);b.unshift("ng");c=fb(b,d.strictDi);c.invoke(["$rootScope","$rootElement","$compile","$injector",function(a,b,c,d){a.$apply(function(){b.data("$injector",d);c(b)(a)})}]);return c},e=/^NG_ENABLE_DEBUG_INFO!/,f=/^NG_DEFER_BOOTSTRAP!/;C&&e.test(C.name)&&(d.debugInfoEnabled=!0,C.name=C.name.replace(e,""));if(C&&!f.test(C.name))return c();C.name=C.name.replace(f,"");ca.resumeBootstrap=function(a){r(a,function(a){b.push(a)});return c()};B(ca.resumeDeferredBootstrap)&&
ca.resumeDeferredBootstrap()}function Ce(){C.name="NG_ENABLE_DEBUG_INFO!"+C.name;C.location.reload()}function De(a){a=ca.element(a).injector();if(!a)throw pa("test");return a.get("$$testability")}function Vc(a,b){b=b||"_";return a.replace(Ee,function(a,c){return(c?b:"")+a.toLowerCase()})}function Fe(){var a;if(!Wc){var b=qb();(rb=z(b)?C.jQuery:b?C[b]:void 0)&&rb.fn.on?(x=rb,S(rb.fn,{scope:Wa.scope,isolateScope:Wa.isolateScope,controller:Wa.controller,injector:Wa.injector,inheritedData:Wa.inheritedData})):
x=Y;a=x.cleanData;x.cleanData=function(b){for(var c,e=0,f;null!=(f=b[e]);e++)(c=(x._data(f)||{}).events)&&c.$destroy&&x(f).triggerHandler("$destroy");a(b)};ca.element=x;Wc=!0}}function gb(a,b,d){if(!a)throw pa("areq",b||"?",d||"required");return a}function sb(a,b,d){d&&H(a)&&(a=a[a.length-1]);gb(B(a),b,"not a function, got "+(a&&"object"===typeof a?a.constructor.name||"Object":typeof a));return a}function Ja(a,b){if("hasOwnProperty"===a)throw pa("badname",b);}function Ge(a,b,d){if(!b)return a;b=b.split(".");
for(var c,e=a,f=b.length,g=0;g<f;g++)c=b[g],a&&(a=(e=a)[c]);return!d&&B(a)?Va(e,a):a}function tb(a){for(var b=a[0],d=a[a.length-1],c,e=1;b!==d&&(b=b.nextSibling);e++)if(c||a[e]!==b)c||(c=x(Ha.call(a,0,e))),c.push(b);return c||a}function T(){return Object.create(null)}function ic(a){if(null==a)return"";switch(typeof a){case "string":break;case "number":a=""+a;break;default:a=!bc(a)||H(a)||ha(a)?eb(a):a.toString()}return a}function He(a){function b(a,b,c){return a[b]||(a[b]=c())}var d=F("$injector"),
c=F("ng");a=b(a,"angular",Object);a.$$minErr=a.$$minErr||F;return b(a,"module",function(){var a={};return function(f,g,k){var h={};if("hasOwnProperty"===f)throw c("badname","module");g&&a.hasOwnProperty(f)&&(a[f]=null);return b(a,f,function(){function a(b,c,d,f){f||(f=e);return function(){f[d||"push"]([b,c,arguments]);return t}}function b(a,c,d){d||(d=e);return function(b,e){e&&B(e)&&(e.$$moduleName=f);d.push([a,c,arguments]);return t}}if(!g)throw d("nomod",f);var e=[],n=[],s=[],G=a("$injector","invoke",
"push",n),t={_invokeQueue:e,_configBlocks:n,_runBlocks:s,info:function(a){if(w(a)){if(!D(a))throw c("aobj","value");h=a;return this}return h},requires:g,name:f,provider:b("$provide","provider"),factory:b("$provide","factory"),service:b("$provide","service"),value:a("$provide","value"),constant:a("$provide","constant","unshift"),decorator:b("$provide","decorator",n),animation:b("$animateProvider","register"),filter:b("$filterProvider","register"),controller:b("$controllerProvider","register"),directive:b("$compileProvider",
"directive"),component:b("$compileProvider","component"),config:G,run:function(a){s.push(a);return this}};k&&G(k);return t})}})}function ja(a,b){if(H(a)){b=b||[];for(var d=0,c=a.length;d<c;d++)b[d]=a[d]}else if(D(a))for(d in b=b||{},a)if("$"!==d.charAt(0)||"$"!==d.charAt(1))b[d]=a[d];return b||a}function Ie(a,b){var d=[];Xb(b)&&(a=ca.copy(a,null,b));return JSON.stringify(a,function(a,b){b=Qc(a,b);if(D(b)){if(0<=d.indexOf(b))return"...";d.push(b)}return b})}function Je(a){S(a,{errorHandlingConfig:re,
bootstrap:Uc,copy:Ia,extend:S,merge:te,equals:va,element:x,forEach:r,injector:fb,noop:E,bind:Va,toJson:eb,fromJson:Rc,identity:Ta,isUndefined:z,isDefined:w,isString:A,isFunction:B,isObject:D,isNumber:W,isElement:$b,isArray:H,version:Ke,isDate:ha,callbacks:{$$counter:0},getTestability:De,reloadWithDebugInfo:Ce,$$minErr:F,$$csp:Aa,$$encodeUriSegment:hc,$$encodeUriQuery:ba,$$lowercase:K,$$stringify:ic,$$uppercase:ub});kc=He(C);kc("ng",["ngLocale"],["$provide",function(a){a.provider({$$sanitizeUri:Le});
a.provider("$compile",Xc).directive({a:Me,input:Yc,textarea:Yc,form:Ne,script:Oe,select:Pe,option:Qe,ngBind:Re,ngBindHtml:Se,ngBindTemplate:Te,ngClass:Ue,ngClassEven:Ve,ngClassOdd:We,ngCloak:Xe,ngController:Ye,ngForm:Ze,ngHide:$e,ngIf:af,ngInclude:bf,ngInit:cf,ngNonBindable:df,ngPluralize:ef,ngRef:ff,ngRepeat:gf,ngShow:hf,ngStyle:jf,ngSwitch:kf,ngSwitchWhen:lf,ngSwitchDefault:mf,ngOptions:nf,ngTransclude:of,ngModel:pf,ngList:qf,ngChange:rf,pattern:Zc,ngPattern:Zc,required:$c,ngRequired:$c,minlength:ad,
ngMinlength:ad,maxlength:bd,ngMaxlength:bd,ngValue:sf,ngModelOptions:tf}).directive({ngInclude:uf,input:vf}).directive(vb).directive(cd);a.provider({$anchorScroll:wf,$animate:xf,$animateCss:yf,$$animateJs:zf,$$animateQueue:Af,$$AnimateRunner:Bf,$$animateAsyncRun:Cf,$browser:Df,$cacheFactory:Ef,$controller:Ff,$document:Gf,$$isDocumentHidden:Hf,$exceptionHandler:If,$filter:dd,$$forceReflow:Jf,$interpolate:Kf,$interval:Lf,$$intervalFactory:Mf,$http:Nf,$httpParamSerializer:Of,$httpParamSerializerJQLike:Pf,
$httpBackend:Qf,$xhrFactory:Rf,$jsonpCallbacks:Sf,$location:Tf,$log:Uf,$parse:Vf,$rootScope:Wf,$q:Xf,$$q:Yf,$sce:Zf,$sceDelegate:$f,$sniffer:ag,$$taskTrackerFactory:bg,$templateCache:cg,$templateRequest:dg,$$testability:eg,$timeout:fg,$window:gg,$$rAF:hg,$$jqLite:ig,$$Map:jg,$$cookieReader:kg})}]).info({angularVersion:"1.7.9"})}function wb(a,b){return b.toUpperCase()}function xb(a){return a.replace(lg,wb)}function lc(a){a=a.nodeType;return 1===a||!a||9===a}function ed(a,b){var d,c,e=b.createDocumentFragment(),
f=[];if(mc.test(a)){d=e.appendChild(b.createElement("div"));c=(mg.exec(a)||["",""])[1].toLowerCase();c=oa[c]||oa._default;d.innerHTML=c[1]+a.replace(ng,"<$1></$2>")+c[2];for(c=c[0];c--;)d=d.lastChild;f=db(f,d.childNodes);d=e.firstChild;d.textContent=""}else f.push(b.createTextNode(a));e.textContent="";e.innerHTML="";r(f,function(a){e.appendChild(a)});return e}function Y(a){if(a instanceof Y)return a;var b;A(a)&&(a=U(a),b=!0);if(!(this instanceof Y)){if(b&&"<"!==a.charAt(0))throw nc("nosel");return new Y(a)}if(b){b=
C.document;var d;a=(d=og.exec(a))?[b.createElement(d[1])]:(d=ed(a,b))?d.childNodes:[];oc(this,a)}else B(a)?fd(a):oc(this,a)}function pc(a){return a.cloneNode(!0)}function yb(a,b){!b&&lc(a)&&x.cleanData([a]);a.querySelectorAll&&x.cleanData(a.querySelectorAll("*"))}function gd(a){for(var b in a)return!1;return!0}function hd(a){var b=a.ng339,d=b&&Ka[b],c=d&&d.events,d=d&&d.data;d&&!gd(d)||c&&!gd(c)||(delete Ka[b],a.ng339=void 0)}function id(a,b,d,c){if(w(c))throw nc("offargs");var e=(c=zb(a))&&c.events,
f=c&&c.handle;if(f){if(b){var g=function(b){var c=e[b];w(d)&&cb(c||[],d);w(d)&&c&&0<c.length||(a.removeEventListener(b,f),delete e[b])};r(b.split(" "),function(a){g(a);Ab[a]&&g(Ab[a])})}else for(b in e)"$destroy"!==b&&a.removeEventListener(b,f),delete e[b];hd(a)}}function qc(a,b){var d=a.ng339;if(d=d&&Ka[d])b?delete d.data[b]:d.data={},hd(a)}function zb(a,b){var d=a.ng339,d=d&&Ka[d];b&&!d&&(a.ng339=d=++pg,d=Ka[d]={events:{},data:{},handle:void 0});return d}function rc(a,b,d){if(lc(a)){var c,e=w(d),
f=!e&&b&&!D(b),g=!b;a=(a=zb(a,!f))&&a.data;if(e)a[xb(b)]=d;else{if(g)return a;if(f)return a&&a[xb(b)];for(c in b)a[xb(c)]=b[c]}}}function Bb(a,b){return a.getAttribute?-1<(" "+(a.getAttribute("class")||"")+" ").replace(/[\n\t]/g," ").indexOf(" "+b+" "):!1}function Cb(a,b){if(b&&a.setAttribute){var d=(" "+(a.getAttribute("class")||"")+" ").replace(/[\n\t]/g," "),c=d;r(b.split(" "),function(a){a=U(a);c=c.replace(" "+a+" "," ")});c!==d&&a.setAttribute("class",U(c))}}function Db(a,b){if(b&&a.setAttribute){var d=
(" "+(a.getAttribute("class")||"")+" ").replace(/[\n\t]/g," "),c=d;r(b.split(" "),function(a){a=U(a);-1===c.indexOf(" "+a+" ")&&(c+=a+" ")});c!==d&&a.setAttribute("class",U(c))}}function oc(a,b){if(b)if(b.nodeType)a[a.length++]=b;else{var d=b.length;if("number"===typeof d&&b.window!==b){if(d)for(var c=0;c<d;c++)a[a.length++]=b[c]}else a[a.length++]=b}}function jd(a,b){return Eb(a,"$"+(b||"ngController")+"Controller")}function Eb(a,b,d){9===a.nodeType&&(a=a.documentElement);for(b=H(b)?b:[b];a;){for(var c=
0,e=b.length;c<e;c++)if(w(d=x.data(a,b[c])))return d;a=a.parentNode||11===a.nodeType&&a.host}}function kd(a){for(yb(a,!0);a.firstChild;)a.removeChild(a.firstChild)}function Fb(a,b){b||yb(a);var d=a.parentNode;d&&d.removeChild(a)}function qg(a,b){b=b||C;if("complete"===b.document.readyState)b.setTimeout(a);else x(b).on("load",a)}function fd(a){function b(){C.document.removeEventListener("DOMContentLoaded",b);C.removeEventListener("load",b);a()}"complete"===C.document.readyState?C.setTimeout(a):(C.document.addEventListener("DOMContentLoaded",
b),C.addEventListener("load",b))}function ld(a,b){var d=Gb[b.toLowerCase()];return d&&md[ua(a)]&&d}function rg(a,b){var d=function(c,d){c.isDefaultPrevented=function(){return c.defaultPrevented};var f=b[d||c.type],g=f?f.length:0;if(g){if(z(c.immediatePropagationStopped)){var k=c.stopImmediatePropagation;c.stopImmediatePropagation=function(){c.immediatePropagationStopped=!0;c.stopPropagation&&c.stopPropagation();k&&k.call(c)}}c.isImmediatePropagationStopped=function(){return!0===c.immediatePropagationStopped};
var h=f.specialHandlerWrapper||sg;1<g&&(f=ja(f));for(var l=0;l<g;l++)c.isImmediatePropagationStopped()||h(a,c,f[l])}};d.elem=a;return d}function sg(a,b,d){d.call(a,b)}function tg(a,b,d){var c=b.relatedTarget;c&&(c===a||ug.call(a,c))||d.call(a,b)}function ig(){this.$get=function(){return S(Y,{hasClass:function(a,b){a.attr&&(a=a[0]);return Bb(a,b)},addClass:function(a,b){a.attr&&(a=a[0]);return Db(a,b)},removeClass:function(a,b){a.attr&&(a=a[0]);return Cb(a,b)}})}}function La(a,b){var d=a&&a.$$hashKey;
if(d)return"function"===typeof d&&(d=a.$$hashKey()),d;d=typeof a;return d="function"===d||"object"===d&&null!==a?a.$$hashKey=d+":"+(b||se)():d+":"+a}function nd(){this._keys=[];this._values=[];this._lastKey=NaN;this._lastIndex=-1}function od(a){a=Function.prototype.toString.call(a).replace(vg,"");return a.match(wg)||a.match(xg)}function yg(a){return(a=od(a))?"function("+(a[1]||"").replace(/[\s\r\n]+/," ")+")":"fn"}function fb(a,b){function d(a){return function(b,c){if(D(b))r(b,Yb(a));else return a(b,
c)}}function c(a,b){Ja(a,"service");if(B(b)||H(b))b=n.instantiate(b);if(!b.$get)throw Ba("pget",a);return p[a+"Provider"]=b}function e(a,b){return function(){var c=t.invoke(b,this);if(z(c))throw Ba("undef",a);return c}}function f(a,b,d){return c(a,{$get:!1!==d?e(a,b):b})}function g(a){gb(z(a)||H(a),"modulesToLoad","not an array");var b=[],c;r(a,function(a){function d(a){var b,c;b=0;for(c=a.length;b<c;b++){var e=a[b],f=n.get(e[0]);f[e[1]].apply(f,e[2])}}if(!m.get(a)){m.set(a,!0);try{A(a)?(c=kc(a),
t.modules[a]=c,b=b.concat(g(c.requires)).concat(c._runBlocks),d(c._invokeQueue),d(c._configBlocks)):B(a)?b.push(n.invoke(a)):H(a)?b.push(n.invoke(a)):sb(a,"module")}catch(e){throw H(a)&&(a=a[a.length-1]),e.message&&e.stack&&-1===e.stack.indexOf(e.message)&&(e=e.message+"\n"+e.stack),Ba("modulerr",a,e.stack||e.message||e);}}});return b}function k(a,c){function d(b,e){if(a.hasOwnProperty(b)){if(a[b]===h)throw Ba("cdep",b+" <- "+l.join(" <- "));return a[b]}try{return l.unshift(b),a[b]=h,a[b]=c(b,e),
a[b]}catch(f){throw a[b]===h&&delete a[b],f;}finally{l.shift()}}function e(a,c,f){var g=[];a=fb.$$annotate(a,b,f);for(var h=0,k=a.length;h<k;h++){var l=a[h];if("string"!==typeof l)throw Ba("itkn",l);g.push(c&&c.hasOwnProperty(l)?c[l]:d(l,f))}return g}return{invoke:function(a,b,c,d){"string"===typeof c&&(d=c,c=null);c=e(a,c,d);H(a)&&(a=a[a.length-1]);d=a;if(Ca||"function"!==typeof d)d=!1;else{var f=d.$$ngIsClass;Ga(f)||(f=d.$$ngIsClass=/^class\b/.test(Function.prototype.toString.call(d)));d=f}return d?
(c.unshift(null),new (Function.prototype.bind.apply(a,c))):a.apply(b,c)},instantiate:function(a,b,c){var d=H(a)?a[a.length-1]:a;a=e(a,b,c);a.unshift(null);return new (Function.prototype.bind.apply(d,a))},get:d,annotate:fb.$$annotate,has:function(b){return p.hasOwnProperty(b+"Provider")||a.hasOwnProperty(b)}}}b=!0===b;var h={},l=[],m=new Hb,p={$provide:{provider:d(c),factory:d(f),service:d(function(a,b){return f(a,["$injector",function(a){return a.instantiate(b)}])}),value:d(function(a,b){return f(a,
ia(b),!1)}),constant:d(function(a,b){Ja(a,"constant");p[a]=b;s[a]=b}),decorator:function(a,b){var c=n.get(a+"Provider"),d=c.$get;c.$get=function(){var a=t.invoke(d,c);return t.invoke(b,null,{$delegate:a})}}}},n=p.$injector=k(p,function(a,b){ca.isString(b)&&l.push(b);throw Ba("unpr",l.join(" <- "));}),s={},G=k(s,function(a,b){var c=n.get(a+"Provider",b);return t.invoke(c.$get,c,void 0,a)}),t=G;p.$injectorProvider={$get:ia(G)};t.modules=n.modules=T();var N=g(a),t=G.get("$injector");t.strictDi=b;r(N,
function(a){a&&t.invoke(a)});t.loadNewModules=function(a){r(g(a),function(a){a&&t.invoke(a)})};return t}function wf(){var a=!0;this.disableAutoScrolling=function(){a=!1};this.$get=["$window","$location","$rootScope",function(b,d,c){function e(a){var b=null;Array.prototype.some.call(a,function(a){if("a"===ua(a))return b=a,!0});return b}function f(a){if(a){a.scrollIntoView();var c;c=g.yOffset;B(c)?c=c():$b(c)?(c=c[0],c="fixed"!==b.getComputedStyle(c).position?0:c.getBoundingClientRect().bottom):W(c)||
(c=0);c&&(a=a.getBoundingClientRect().top,b.scrollBy(0,a-c))}else b.scrollTo(0,0)}function g(a){a=A(a)?a:W(a)?a.toString():d.hash();var b;a?(b=k.getElementById(a))?f(b):(b=e(k.getElementsByName(a)))?f(b):"top"===a&&f(null):f(null)}var k=b.document;a&&c.$watch(function(){return d.hash()},function(a,b){a===b&&""===a||qg(function(){c.$evalAsync(g)})});return g}]}function hb(a,b){if(!a&&!b)return"";if(!a)return b;if(!b)return a;H(a)&&(a=a.join(" "));H(b)&&(b=b.join(" "));return a+" "+b}function zg(a){A(a)&&
(a=a.split(" "));var b=T();r(a,function(a){a.length&&(b[a]=!0)});return b}function ra(a){return D(a)?a:{}}function Ag(a,b,d,c,e){function f(){qa=null;k()}function g(){t=y();t=z(t)?null:t;va(t,P)&&(t=P);N=P=t}function k(){var a=N;g();if(v!==h.url()||a!==t)v=h.url(),N=t,r(J,function(a){a(h.url(),t)})}var h=this,l=a.location,m=a.history,p=a.setTimeout,n=a.clearTimeout,s={},G=e(d);h.isMock=!1;h.$$completeOutstandingRequest=G.completeTask;h.$$incOutstandingRequestCount=G.incTaskCount;h.notifyWhenNoOutstandingRequests=
G.notifyWhenNoPendingTasks;var t,N,v=l.href,jc=b.find("base"),qa=null,y=c.history?function(){try{return m.state}catch(a){}}:E;g();h.url=function(b,d,e){z(e)&&(e=null);l!==a.location&&(l=a.location);m!==a.history&&(m=a.history);if(b){var f=N===e;b=ga(b).href;if(v===b&&(!c.history||f))return h;var k=v&&Da(v)===Da(b);v=b;N=e;!c.history||k&&f?(k||(qa=b),d?l.replace(b):k?(d=l,e=b,f=e.indexOf("#"),e=-1===f?"":e.substr(f),d.hash=e):l.href=b,l.href!==b&&(qa=b)):(m[d?"replaceState":"pushState"](e,"",b),g());
qa&&(qa=b);return h}return(qa||l.href).replace(/#$/,"")};h.state=function(){return t};var J=[],I=!1,P=null;h.onUrlChange=function(b){if(!I){if(c.history)x(a).on("popstate",f);x(a).on("hashchange",f);I=!0}J.push(b);return b};h.$$applicationDestroyed=function(){x(a).off("hashchange popstate",f)};h.$$checkUrlChange=k;h.baseHref=function(){var a=jc.attr("href");return a?a.replace(/^(https?:)?\/\/[^/]*/,""):""};h.defer=function(a,b,c){var d;b=b||0;c=c||G.DEFAULT_TASK_TYPE;G.incTaskCount(c);d=p(function(){delete s[d];
G.completeTask(a,c)},b);s[d]=c;return d};h.defer.cancel=function(a){if(s.hasOwnProperty(a)){var b=s[a];delete s[a];n(a);G.completeTask(E,b);return!0}return!1}}function Df(){this.$get=["$window","$log","$sniffer","$document","$$taskTrackerFactory",function(a,b,d,c,e){return new Ag(a,c,b,d,e)}]}function Ef(){this.$get=function(){function a(a,c){function e(a){a!==p&&(n?n===a&&(n=a.n):n=a,f(a.n,a.p),f(a,p),p=a,p.n=null)}function f(a,b){a!==b&&(a&&(a.p=b),b&&(b.n=a))}if(a in b)throw F("$cacheFactory")("iid",
a);var g=0,k=S({},c,{id:a}),h=T(),l=c&&c.capacity||Number.MAX_VALUE,m=T(),p=null,n=null;return b[a]={put:function(a,b){if(!z(b)){if(l<Number.MAX_VALUE){var c=m[a]||(m[a]={key:a});e(c)}a in h||g++;h[a]=b;g>l&&this.remove(n.key);return b}},get:function(a){if(l<Number.MAX_VALUE){var b=m[a];if(!b)return;e(b)}return h[a]},remove:function(a){if(l<Number.MAX_VALUE){var b=m[a];if(!b)return;b===p&&(p=b.p);b===n&&(n=b.n);f(b.n,b.p);delete m[a]}a in h&&(delete h[a],g--)},removeAll:function(){h=T();g=0;m=T();
p=n=null},destroy:function(){m=k=h=null;delete b[a]},info:function(){return S({},k,{size:g})}}}var b={};a.info=function(){var a={};r(b,function(b,e){a[e]=b.info()});return a};a.get=function(a){return b[a]};return a}}function cg(){this.$get=["$cacheFactory",function(a){return a("templates")}]}function Xc(a,b){function d(a,b,c){var d=/^([@&]|[=<](\*?))(\??)\s*([\w$]*)$/,e=T();r(a,function(a,f){a=a.trim();if(a in p)e[f]=p[a];else{var g=a.match(d);if(!g)throw $("iscp",b,f,a,c?"controller bindings definition":
"isolate scope definition");e[f]={mode:g[1][0],collection:"*"===g[2],optional:"?"===g[3],attrName:g[4]||f};g[4]&&(p[a]=e[f])}});return e}function c(a){var b=a.charAt(0);if(!b||b!==K(b))throw $("baddir",a);if(a!==a.trim())throw $("baddir",a);}function e(a){var b=a.require||a.controller&&a.name;!H(b)&&D(b)&&r(b,function(a,c){var d=a.match(l);a.substring(d[0].length)||(b[c]=d[0]+c)});return b}var f={},g=/^\s*directive:\s*([\w-]+)\s+(.*)$/,k=/(([\w-]+)(?::([^;]+))?;?)/,h=we("ngSrc,ngSrcset,src,srcset"),
l=/^(?:(\^\^?)?(\?)?(\^\^?)?)?/,m=/^(on[a-z]+|formaction)$/,p=T();this.directive=function qa(b,d){gb(b,"name");Ja(b,"directive");A(b)?(c(b),gb(d,"directiveFactory"),f.hasOwnProperty(b)||(f[b]=[],a.factory(b+"Directive",["$injector","$exceptionHandler",function(a,c){var d=[];r(f[b],function(f,g){try{var h=a.invoke(f);B(h)?h={compile:ia(h)}:!h.compile&&h.link&&(h.compile=ia(h.link));h.priority=h.priority||0;h.index=g;h.name=h.name||b;h.require=e(h);var k=h,l=h.restrict;if(l&&(!A(l)||!/[EACM]/.test(l)))throw $("badrestrict",
l,b);k.restrict=l||"EA";h.$$moduleName=f.$$moduleName;d.push(h)}catch(m){c(m)}});return d}])),f[b].push(d)):r(b,Yb(qa));return this};this.component=function y(a,b){function c(a){function e(b){return B(b)||H(b)?function(c,d){return a.invoke(b,this,{$element:c,$attrs:d})}:b}var f=b.template||b.templateUrl?b.template:"",g={controller:d,controllerAs:Bg(b.controller)||b.controllerAs||"$ctrl",template:e(f),templateUrl:e(b.templateUrl),transclude:b.transclude,scope:{},bindToController:b.bindings||{},restrict:"E",
require:b.require};r(b,function(a,b){"$"===b.charAt(0)&&(g[b]=a)});return g}if(!A(a))return r(a,Yb(Va(this,y))),this;var d=b.controller||function(){};r(b,function(a,b){"$"===b.charAt(0)&&(c[b]=a,B(d)&&(d[b]=a))});c.$inject=["$injector"];return this.directive(a,c)};this.aHrefSanitizationWhitelist=function(a){return w(a)?(b.aHrefSanitizationWhitelist(a),this):b.aHrefSanitizationWhitelist()};this.imgSrcSanitizationWhitelist=function(a){return w(a)?(b.imgSrcSanitizationWhitelist(a),this):b.imgSrcSanitizationWhitelist()};
var n=!0;this.debugInfoEnabled=function(a){return w(a)?(n=a,this):n};var s=!1;this.strictComponentBindingsEnabled=function(a){return w(a)?(s=a,this):s};var G=10;this.onChangesTtl=function(a){return arguments.length?(G=a,this):G};var t=!0;this.commentDirectivesEnabled=function(a){return arguments.length?(t=a,this):t};var N=!0;this.cssClassDirectivesEnabled=function(a){return arguments.length?(N=a,this):N};var v=T();this.addPropertySecurityContext=function(a,b,c){var d=a.toLowerCase()+"|"+b.toLowerCase();
if(d in v&&v[d]!==c)throw $("ctxoverride",a,b,v[d],c);v[d]=c;return this};(function(){function a(b,c){r(c,function(a){v[a.toLowerCase()]=b})}a(V.HTML,["iframe|srcdoc","*|innerHTML","*|outerHTML"]);a(V.CSS,["*|style"]);a(V.URL,"area|href area|ping a|href a|ping blockquote|cite body|background del|cite input|src ins|cite q|cite".split(" "));a(V.MEDIA_URL,"audio|src img|src img|srcset source|src source|srcset track|src video|src video|poster".split(" "));a(V.RESOURCE_URL,"*|formAction applet|code applet|codebase base|href embed|src frame|src form|action head|profile html|manifest iframe|src link|href media|src object|codebase object|data script|src".split(" "))})();
this.$get=["$injector","$interpolate","$exceptionHandler","$templateRequest","$parse","$controller","$rootScope","$sce","$animate",function(a,b,c,e,p,M,L,u,R){function q(){try{if(!--Ja)throw Ua=void 0,$("infchng",G);L.$apply(function(){for(var a=0,b=Ua.length;a<b;++a)try{Ua[a]()}catch(d){c(d)}Ua=void 0})}finally{Ja++}}function ma(a,b){if(!a)return a;if(!A(a))throw $("srcset",b,a.toString());for(var c="",d=U(a),e=/(\s+\d+x\s*,|\s+\d+w\s*,|\s+,|,\s+)/,e=/\s/.test(d)?e:/(,)/,d=d.split(e),e=Math.floor(d.length/
2),f=0;f<e;f++)var g=2*f,c=c+u.getTrustedMediaUrl(U(d[g])),c=c+(" "+U(d[g+1]));d=U(d[2*f]).split(/\s/);c+=u.getTrustedMediaUrl(U(d[0]));2===d.length&&(c+=" "+U(d[1]));return c}function w(a,b){if(b){var c=Object.keys(b),d,e,f;d=0;for(e=c.length;d<e;d++)f=c[d],this[f]=b[f]}else this.$attr={};this.$$element=a}function O(a,b,c){Fa.innerHTML="<span "+b+">";b=Fa.firstChild.attributes;var d=b[0];b.removeNamedItem(d.name);d.value=c;a.attributes.setNamedItem(d)}function sa(a,b){try{a.addClass(b)}catch(c){}}
function da(a,b,c,d,e){a instanceof x||(a=x(a));var f=Xa(a,b,a,c,d,e);da.$$addScopeClass(a);var g=null;return function(b,c,d){if(!a)throw $("multilink");gb(b,"scope");e&&e.needsNewScope&&(b=b.$parent.$new());d=d||{};var h=d.parentBoundTranscludeFn,k=d.transcludeControllers;d=d.futureParentElement;h&&h.$$boundTransclude&&(h=h.$$boundTransclude);g||(g=(d=d&&d[0])?"foreignobject"!==ua(d)&&la.call(d).match(/SVG/)?"svg":"html":"html");d="html"!==g?x(ja(g,x("<div></div>").append(a).html())):c?Wa.clone.call(a):
a;if(k)for(var l in k)d.data("$"+l+"Controller",k[l].instance);da.$$addScopeInfo(d,b);c&&c(d,b);f&&f(b,d,d,h);c||(a=f=null);return d}}function Xa(a,b,c,d,e,f){function g(a,c,d,e){var f,k,l,m,p,I,t;if(n)for(t=Array(c.length),m=0;m<h.length;m+=3)f=h[m],t[f]=c[f];else t=c;m=0;for(p=h.length;m<p;)k=t[h[m++]],c=h[m++],f=h[m++],c?(c.scope?(l=a.$new(),da.$$addScopeInfo(x(k),l)):l=a,I=c.transcludeOnThisElement?ka(a,c.transclude,e):!c.templateOnThisElement&&e?e:!e&&b?ka(a,b):null,c(f,l,k,d,I)):f&&f(a,k.childNodes,
void 0,e)}for(var h=[],k=H(a)||a instanceof x,l,m,p,I,n,t=0;t<a.length;t++){l=new w;11===Ca&&ib(a,t,k);m=sc(a[t],[],l,0===t?d:void 0,e);(f=m.length?aa(m,a[t],l,b,c,null,[],[],f):null)&&f.scope&&da.$$addScopeClass(l.$$element);l=f&&f.terminal||!(p=a[t].childNodes)||!p.length?null:Xa(p,f?(f.transcludeOnThisElement||!f.templateOnThisElement)&&f.transclude:b);if(f||l)h.push(t,f,l),I=!0,n=n||f;f=null}return I?g:null}function ib(a,b,c){var d=a[b],e=d.parentNode,f;if(d.nodeType===Pa)for(;;){f=e?d.nextSibling:
a[b+1];if(!f||f.nodeType!==Pa)break;d.nodeValue+=f.nodeValue;f.parentNode&&f.parentNode.removeChild(f);c&&f===a[b+1]&&a.splice(b+1,1)}}function ka(a,b,c){function d(e,f,g,h,k){e||(e=a.$new(!1,k),e.$$transcluded=!0);return b(e,f,{parentBoundTranscludeFn:c,transcludeControllers:g,futureParentElement:h})}var e=d.$$slots=T(),f;for(f in b.$$slots)e[f]=b.$$slots[f]?ka(a,b.$$slots[f],c):null;return d}function sc(a,b,d,e,f){var g=d.$attr,h;switch(a.nodeType){case 1:h=ua(a);X(b,wa(h),"E",e,f);for(var l,m,
n,t,J,s=a.attributes,v=0,G=s&&s.length;v<G;v++){var P=!1,N=!1,r=!1,y=!1,u=!1,M;l=s[v];m=l.name;t=l.value;n=wa(m.toLowerCase());(J=n.match(Ra))?(r="Attr"===J[1],y="Prop"===J[1],u="On"===J[1],m=m.replace(pd,"").toLowerCase().substr(4+J[1].length).replace(/_(.)/g,function(a,b){return b.toUpperCase()})):(M=n.match(Sa))&&ca(M[1])&&(P=m,N=m.substr(0,m.length-5)+"end",m=m.substr(0,m.length-6));if(y||u)d[n]=t,g[n]=l.name,y?Ea(a,b,n,m):b.push(qd(p,L,c,n,m,!1));else{n=wa(m.toLowerCase());g[n]=m;if(r||!d.hasOwnProperty(n))d[n]=
t,ld(a,n)&&(d[n]=!0);Ia(a,b,t,n,r);X(b,n,"A",e,f,P,N)}}"input"===h&&"hidden"===a.getAttribute("type")&&a.setAttribute("autocomplete","off");if(!Qa)break;g=a.className;D(g)&&(g=g.animVal);if(A(g)&&""!==g)for(;a=k.exec(g);)n=wa(a[2]),X(b,n,"C",e,f)&&(d[n]=U(a[3])),g=g.substr(a.index+a[0].length);break;case Pa:na(b,a.nodeValue);break;case 8:if(!Oa)break;F(a,b,d,e,f)}b.sort(ia);return b}function F(a,b,c,d,e){try{var f=g.exec(a.nodeValue);if(f){var h=wa(f[1]);X(b,h,"M",d,e)&&(c[h]=U(f[2]))}}catch(k){}}
function V(a,b,c){var d=[],e=0;if(b&&a.hasAttribute&&a.hasAttribute(b)){do{if(!a)throw $("uterdir",b,c);1===a.nodeType&&(a.hasAttribute(b)&&e++,a.hasAttribute(c)&&e--);d.push(a);a=a.nextSibling}while(0<e)}else d.push(a);return x(d)}function Y(a,b,c){return function(d,e,f,g,h){e=V(e[0],b,c);return a(d,e,f,g,h)}}function Z(a,b,c,d,e,f){var g;return a?da(b,c,d,e,f):function(){g||(g=da(b,c,d,e,f),b=c=f=null);return g.apply(this,arguments)}}function aa(a,b,d,e,f,g,h,k,l){function m(a,b,c,d){if(a){c&&(a=
Y(a,c,d));a.require=u.require;a.directiveName=Q;if(s===u||u.$$isolateScope)a=Aa(a,{isolateScope:!0});h.push(a)}if(b){c&&(b=Y(b,c,d));b.require=u.require;b.directiveName=Q;if(s===u||u.$$isolateScope)b=Aa(b,{isolateScope:!0});k.push(b)}}function p(a,e,f,g,l){function m(a,b,c,d){var e;bb(a)||(d=c,c=b,b=a,a=void 0);N&&(e=P);c||(c=N?Q.parent():Q);if(d){var f=l.$$slots[d];if(f)return f(a,b,e,c,R);if(z(f))throw $("noslot",d,za(Q));}else return l(a,b,e,c,R)}var n,u,L,y,G,P,M,Q;b===f?(g=d,Q=d.$$element):(Q=
x(f),g=new w(Q,d));G=e;s?y=e.$new(!0):t&&(G=e.$parent);l&&(M=m,M.$$boundTransclude=l,M.isSlotFilled=function(a){return!!l.$$slots[a]});J&&(P=ea(Q,g,M,J,y,e,s));s&&(da.$$addScopeInfo(Q,y,!0,!(v&&(v===s||v===s.$$originalDirective))),da.$$addScopeClass(Q,!0),y.$$isolateBindings=s.$$isolateBindings,u=Da(e,g,y,y.$$isolateBindings,s),u.removeWatches&&y.$on("$destroy",u.removeWatches));for(n in P){u=J[n];L=P[n];var Cg=u.$$bindings.bindToController;L.instance=L();Q.data("$"+u.name+"Controller",L.instance);
L.bindingInfo=Da(G,g,L.instance,Cg,u)}r(J,function(a,b){var c=a.require;a.bindToController&&!H(c)&&D(c)&&S(P[b].instance,W(b,c,Q,P))});r(P,function(a){var b=a.instance;if(B(b.$onChanges))try{b.$onChanges(a.bindingInfo.initialChanges)}catch(d){c(d)}if(B(b.$onInit))try{b.$onInit()}catch(e){c(e)}B(b.$doCheck)&&(G.$watch(function(){b.$doCheck()}),b.$doCheck());B(b.$onDestroy)&&G.$on("$destroy",function(){b.$onDestroy()})});n=0;for(u=h.length;n<u;n++)L=h[n],Ba(L,L.isolateScope?y:e,Q,g,L.require&&W(L.directiveName,
L.require,Q,P),M);var R=e;s&&(s.template||null===s.templateUrl)&&(R=y);a&&a(R,f.childNodes,void 0,l);for(n=k.length-1;0<=n;n--)L=k[n],Ba(L,L.isolateScope?y:e,Q,g,L.require&&W(L.directiveName,L.require,Q,P),M);r(P,function(a){a=a.instance;B(a.$postLink)&&a.$postLink()})}l=l||{};for(var n=-Number.MAX_VALUE,t=l.newScopeDirective,J=l.controllerDirectives,s=l.newIsolateScopeDirective,v=l.templateDirective,L=l.nonTlbTranscludeDirective,G=!1,P=!1,N=l.hasElementTranscludeDirective,y=d.$$element=x(b),u,Q,
M,R=e,q,ma=!1,Ib=!1,O,sa=0,A=a.length;sa<A;sa++){u=a[sa];var E=u.$$start,ib=u.$$end;E&&(y=V(b,E,ib));M=void 0;if(n>u.priority)break;if(O=u.scope)u.templateUrl||(D(O)?(ba("new/isolated scope",s||t,u,y),s=u):ba("new/isolated scope",s,u,y)),t=t||u;Q=u.name;if(!ma&&(u.replace&&(u.templateUrl||u.template)||u.transclude&&!u.$$tlb)){for(O=sa+1;ma=a[O++];)if(ma.transclude&&!ma.$$tlb||ma.replace&&(ma.templateUrl||ma.template)){Ib=!0;break}ma=!0}!u.templateUrl&&u.controller&&(J=J||T(),ba("'"+Q+"' controller",
J[Q],u,y),J[Q]=u);if(O=u.transclude)if(G=!0,u.$$tlb||(ba("transclusion",L,u,y),L=u),"element"===O)N=!0,n=u.priority,M=y,y=d.$$element=x(da.$$createComment(Q,d[Q])),b=y[0],pa(f,Ha.call(M,0),b),R=Z(Ib,M,e,n,g&&g.name,{nonTlbTranscludeDirective:L});else{var ka=T();if(D(O)){M=C.document.createDocumentFragment();var Xa=T(),F=T();r(O,function(a,b){var c="?"===a.charAt(0);a=c?a.substring(1):a;Xa[a]=b;ka[b]=null;F[b]=c});r(y.contents(),function(a){var b=Xa[wa(ua(a))];b?(F[b]=!0,ka[b]=ka[b]||C.document.createDocumentFragment(),
ka[b].appendChild(a)):M.appendChild(a)});r(F,function(a,b){if(!a)throw $("reqslot",b);});for(var K in ka)ka[K]&&(R=x(ka[K].childNodes),ka[K]=Z(Ib,R,e));M=x(M.childNodes)}else M=x(pc(b)).contents();y.empty();R=Z(Ib,M,e,void 0,void 0,{needsNewScope:u.$$isolateScope||u.$$newScope});R.$$slots=ka}if(u.template)if(P=!0,ba("template",v,u,y),v=u,O=B(u.template)?u.template(y,d):u.template,O=Na(O),u.replace){g=u;M=mc.test(O)?rd(ja(u.templateNamespace,U(O))):[];b=M[0];if(1!==M.length||1!==b.nodeType)throw $("tplrt",
Q,"");pa(f,y,b);A={$attr:{}};O=sc(b,[],A);var Dg=a.splice(sa+1,a.length-(sa+1));(s||t)&&fa(O,s,t);a=a.concat(O).concat(Dg);ga(d,A);A=a.length}else y.html(O);if(u.templateUrl)P=!0,ba("template",v,u,y),v=u,u.replace&&(g=u),p=ha(a.splice(sa,a.length-sa),y,d,f,G&&R,h,k,{controllerDirectives:J,newScopeDirective:t!==u&&t,newIsolateScopeDirective:s,templateDirective:v,nonTlbTranscludeDirective:L}),A=a.length;else if(u.compile)try{q=u.compile(y,d,R);var X=u.$$originalDirective||u;B(q)?m(null,Va(X,q),E,ib):
q&&m(Va(X,q.pre),Va(X,q.post),E,ib)}catch(ca){c(ca,za(y))}u.terminal&&(p.terminal=!0,n=Math.max(n,u.priority))}p.scope=t&&!0===t.scope;p.transcludeOnThisElement=G;p.templateOnThisElement=P;p.transclude=R;l.hasElementTranscludeDirective=N;return p}function W(a,b,c,d){var e;if(A(b)){var f=b.match(l);b=b.substring(f[0].length);var g=f[1]||f[3],f="?"===f[2];"^^"===g?c=c.parent():e=(e=d&&d[b])&&e.instance;if(!e){var h="$"+b+"Controller";e="^^"===g&&c[0]&&9===c[0].nodeType?null:g?c.inheritedData(h):c.data(h)}if(!e&&
!f)throw $("ctreq",b,a);}else if(H(b))for(e=[],g=0,f=b.length;g<f;g++)e[g]=W(a,b[g],c,d);else D(b)&&(e={},r(b,function(b,f){e[f]=W(a,b,c,d)}));return e||null}function ea(a,b,c,d,e,f,g){var h=T(),k;for(k in d){var l=d[k],m={$scope:l===g||l.$$isolateScope?e:f,$element:a,$attrs:b,$transclude:c},p=l.controller;"@"===p&&(p=b[l.name]);m=M(p,m,!0,l.controllerAs);h[l.name]=m;a.data("$"+l.name+"Controller",m.instance)}return h}function fa(a,b,c){for(var d=0,e=a.length;d<e;d++)a[d]=ac(a[d],{$$isolateScope:b,
$$newScope:c})}function X(b,c,e,g,h,k,l){if(c===h)return null;var m=null;if(f.hasOwnProperty(c)){h=a.get(c+"Directive");for(var p=0,n=h.length;p<n;p++)if(c=h[p],(z(g)||g>c.priority)&&-1!==c.restrict.indexOf(e)){k&&(c=ac(c,{$$start:k,$$end:l}));if(!c.$$bindings){var I=m=c,t=c.name,u={isolateScope:null,bindToController:null};D(I.scope)&&(!0===I.bindToController?(u.bindToController=d(I.scope,t,!0),u.isolateScope={}):u.isolateScope=d(I.scope,t,!1));D(I.bindToController)&&(u.bindToController=d(I.bindToController,
t,!0));if(u.bindToController&&!I.controller)throw $("noctrl",t);m=m.$$bindings=u;D(m.isolateScope)&&(c.$$isolateBindings=m.isolateScope)}b.push(c);m=c}}return m}function ca(b){if(f.hasOwnProperty(b))for(var c=a.get(b+"Directive"),d=0,e=c.length;d<e;d++)if(b=c[d],b.multiElement)return!0;return!1}function ga(a,b){var c=b.$attr,d=a.$attr;r(a,function(d,e){"$"!==e.charAt(0)&&(b[e]&&b[e]!==d&&(d=d.length?d+(("style"===e?";":" ")+b[e]):b[e]),a.$set(e,d,!0,c[e]))});r(b,function(b,e){a.hasOwnProperty(e)||
"$"===e.charAt(0)||(a[e]=b,"class"!==e&&"style"!==e&&(d[e]=c[e]))})}function ha(a,b,d,f,g,h,k,l){var m=[],p,n,t=b[0],u=a.shift(),J=ac(u,{templateUrl:null,transclude:null,replace:null,$$originalDirective:u}),s=B(u.templateUrl)?u.templateUrl(b,d):u.templateUrl,L=u.templateNamespace;b.empty();e(s).then(function(c){var e,I;c=Na(c);if(u.replace){c=mc.test(c)?rd(ja(L,U(c))):[];e=c[0];if(1!==c.length||1!==e.nodeType)throw $("tplrt",u.name,s);c={$attr:{}};pa(f,b,e);var v=sc(e,[],c);D(u.scope)&&fa(v,!0);a=
v.concat(a);ga(d,c)}else e=t,b.html(c);a.unshift(J);p=aa(a,e,d,g,b,u,h,k,l);r(f,function(a,c){a===e&&(f[c]=b[0])});for(n=Xa(b[0].childNodes,g);m.length;){c=m.shift();I=m.shift();var y=m.shift(),P=m.shift(),v=b[0];if(!c.$$destroyed){if(I!==t){var G=I.className;l.hasElementTranscludeDirective&&u.replace||(v=pc(e));pa(y,x(I),v);sa(x(v),G)}I=p.transcludeOnThisElement?ka(c,p.transclude,P):P;p(n,c,v,f,I)}}m=null}).catch(function(a){cc(a)&&c(a)});return function(a,b,c,d,e){a=e;b.$$destroyed||(m?m.push(b,
c,d,a):(p.transcludeOnThisElement&&(a=ka(b,p.transclude,e)),p(n,b,c,d,a)))}}function ia(a,b){var c=b.priority-a.priority;return 0!==c?c:a.name!==b.name?a.name<b.name?-1:1:a.index-b.index}function ba(a,b,c,d){function e(a){return a?" (module: "+a+")":""}if(b)throw $("multidir",b.name,e(b.$$moduleName),c.name,e(c.$$moduleName),a,za(d));}function na(a,c){var d=b(c,!0);d&&a.push({priority:0,compile:function(a){a=a.parent();var b=!!a.length;b&&da.$$addBindingClass(a);return function(a,c){var e=c.parent();
b||da.$$addBindingClass(e);da.$$addBindingInfo(e,d.expressions);a.$watch(d,function(a){c[0].nodeValue=a})}}})}function ja(a,b){a=K(a||"html");switch(a){case "svg":case "math":var c=C.document.createElement("div");c.innerHTML="<"+a+">"+b+"</"+a+">";return c.childNodes[0].childNodes;default:return b}}function oa(a,b){if("srcdoc"===b)return u.HTML;if("src"===b||"ngSrc"===b)return-1===["img","video","audio","source","track"].indexOf(a)?u.RESOURCE_URL:u.MEDIA_URL;if("xlinkHref"===b)return"image"===a?u.MEDIA_URL:
"a"===a?u.URL:u.RESOURCE_URL;if("form"===a&&"action"===b||"base"===a&&"href"===b||"link"===a&&"href"===b)return u.RESOURCE_URL;if("a"===a&&("href"===b||"ngHref"===b))return u.URL}function xa(a,b){var c=b.toLowerCase();return v[a+"|"+c]||v["*|"+c]}function ya(a){return ma(u.valueOf(a),"ng-prop-srcset")}function Ea(a,b,c,d){if(m.test(d))throw $("nodomevents");a=ua(a);var e=xa(a,d),f=Ta;"srcset"!==d||"img"!==a&&"source"!==a?e&&(f=u.getTrusted.bind(u,e)):f=ya;b.push({priority:100,compile:function(a,b){var e=
p(b[c]),g=p(b[c],function(a){return u.valueOf(a)});return{pre:function(a,b){function c(){var g=e(a);b[0][d]=f(g)}c();a.$watch(g,c)}}}})}function Ia(a,c,d,e,f){var g=ua(a),k=oa(g,e),l=h[e]||f,p=b(d,!f,k,l);if(p){if("multiple"===e&&"select"===g)throw $("selmulti",za(a));if(m.test(e))throw $("nodomevents");c.push({priority:100,compile:function(){return{pre:function(a,c,f){c=f.$$observers||(f.$$observers=T());var g=f[e];g!==d&&(p=g&&b(g,!0,k,l),d=g);p&&(f[e]=p(a),(c[e]||(c[e]=[])).$$inter=!0,(f.$$observers&&
f.$$observers[e].$$scope||a).$watch(p,function(a,b){"class"===e&&a!==b?f.$updateClass(a,b):f.$set(e,a)}))}}}})}}function pa(a,b,c){var d=b[0],e=b.length,f=d.parentNode,g,h;if(a)for(g=0,h=a.length;g<h;g++)if(a[g]===d){a[g++]=c;h=g+e-1;for(var k=a.length;g<k;g++,h++)h<k?a[g]=a[h]:delete a[g];a.length-=e-1;a.context===d&&(a.context=c);break}f&&f.replaceChild(c,d);a=C.document.createDocumentFragment();for(g=0;g<e;g++)a.appendChild(b[g]);x.hasData(d)&&(x.data(c,x.data(d)),x(d).off("$destroy"));x.cleanData(a.querySelectorAll("*"));
for(g=1;g<e;g++)delete b[g];b[0]=c;b.length=1}function Aa(a,b){return S(function(){return a.apply(null,arguments)},a,b)}function Ba(a,b,d,e,f,g){try{a(b,d,e,f,g)}catch(h){c(h,za(d))}}function ra(a,b){if(s)throw $("missingattr",a,b);}function Da(a,c,d,e,f){function g(b,c,e){B(d.$onChanges)&&!dc(c,e)&&(Ua||(a.$$postDigest(q),Ua=[]),m||(m={},Ua.push(h)),m[b]&&(e=m[b].previousValue),m[b]=new Jb(e,c))}function h(){d.$onChanges(m);m=void 0}var k=[],l={},m;r(e,function(e,h){var m=e.attrName,n=e.optional,
I,t,u,s;switch(e.mode){case "@":n||ta.call(c,m)||(ra(m,f.name),d[h]=c[m]=void 0);n=c.$observe(m,function(a){if(A(a)||Ga(a))g(h,a,d[h]),d[h]=a});c.$$observers[m].$$scope=a;I=c[m];A(I)?d[h]=b(I)(a):Ga(I)&&(d[h]=I);l[h]=new Jb(tc,d[h]);k.push(n);break;case "=":if(!ta.call(c,m)){if(n)break;ra(m,f.name);c[m]=void 0}if(n&&!c[m])break;t=p(c[m]);s=t.literal?va:dc;u=t.assign||function(){I=d[h]=t(a);throw $("nonassign",c[m],m,f.name);};I=d[h]=t(a);n=function(b){s(b,d[h])||(s(b,I)?u(a,b=d[h]):d[h]=b);return I=
b};n.$stateful=!0;n=e.collection?a.$watchCollection(c[m],n):a.$watch(p(c[m],n),null,t.literal);k.push(n);break;case "<":if(!ta.call(c,m)){if(n)break;ra(m,f.name);c[m]=void 0}if(n&&!c[m])break;t=p(c[m]);var v=t.literal,L=d[h]=t(a);l[h]=new Jb(tc,d[h]);n=a[e.collection?"$watchCollection":"$watch"](t,function(a,b){if(b===a){if(b===L||v&&va(b,L))return;b=L}g(h,a,b);d[h]=a});k.push(n);break;case "&":n||ta.call(c,m)||ra(m,f.name);t=c.hasOwnProperty(m)?p(c[m]):E;if(t===E&&n)break;d[h]=function(b){return t(a,
b)}}});return{initialChanges:l,removeWatches:k.length&&function(){for(var a=0,b=k.length;a<b;++a)k[a]()}}}var Ma=/^\w/,Fa=C.document.createElement("div"),Oa=t,Qa=N,Ja=G,Ua;w.prototype={$normalize:wa,$addClass:function(a){a&&0<a.length&&R.addClass(this.$$element,a)},$removeClass:function(a){a&&0<a.length&&R.removeClass(this.$$element,a)},$updateClass:function(a,b){var c=sd(a,b);c&&c.length&&R.addClass(this.$$element,c);(c=sd(b,a))&&c.length&&R.removeClass(this.$$element,c)},$set:function(a,b,d,e){var f=
ld(this.$$element[0],a),g=td[a],h=a;f?(this.$$element.prop(a,b),e=f):g&&(this[g]=b,h=g);this[a]=b;e?this.$attr[a]=e:(e=this.$attr[a])||(this.$attr[a]=e=Vc(a,"-"));"img"===ua(this.$$element)&&"srcset"===a&&(this[a]=b=ma(b,"$set('srcset', value)"));!1!==d&&(null===b||z(b)?this.$$element.removeAttr(e):Ma.test(e)?f&&!1===b?this.$$element.removeAttr(e):this.$$element.attr(e,b):O(this.$$element[0],e,b));(a=this.$$observers)&&r(a[h],function(a){try{a(b)}catch(d){c(d)}})},$observe:function(a,b){var c=this,
d=c.$$observers||(c.$$observers=T()),e=d[a]||(d[a]=[]);e.push(b);L.$evalAsync(function(){e.$$inter||!c.hasOwnProperty(a)||z(c[a])||b(c[a])});return function(){cb(e,b)}}};var Ka=b.startSymbol(),La=b.endSymbol(),Na="{{"===Ka&&"}}"===La?Ta:function(a){return a.replace(/\{\{/g,Ka).replace(/}}/g,La)},Ra=/^ng(Attr|Prop|On)([A-Z].*)$/,Sa=/^(.+)Start$/;da.$$addBindingInfo=n?function(a,b){var c=a.data("$binding")||[];H(b)?c=c.concat(b):c.push(b);a.data("$binding",c)}:E;da.$$addBindingClass=n?function(a){sa(a,
"ng-binding")}:E;da.$$addScopeInfo=n?function(a,b,c,d){a.data(c?d?"$isolateScopeNoTemplate":"$isolateScope":"$scope",b)}:E;da.$$addScopeClass=n?function(a,b){sa(a,b?"ng-isolate-scope":"ng-scope")}:E;da.$$createComment=function(a,b){var c="";n&&(c=" "+(a||"")+": ",b&&(c+=b+" "));return C.document.createComment(c)};return da}]}function Jb(a,b){this.previousValue=a;this.currentValue=b}function wa(a){return a.replace(pd,"").replace(Eg,function(a,d,c){return c?d.toUpperCase():d})}function sd(a,b){var d=
"",c=a.split(/\s+/),e=b.split(/\s+/),f=0;a:for(;f<c.length;f++){for(var g=c[f],k=0;k<e.length;k++)if(g===e[k])continue a;d+=(0<d.length?" ":"")+g}return d}function rd(a){a=x(a);var b=a.length;if(1>=b)return a;for(;b--;){var d=a[b];(8===d.nodeType||d.nodeType===Pa&&""===d.nodeValue.trim())&&Fg.call(a,b,1)}return a}function Bg(a,b){if(b&&A(b))return b;if(A(a)){var d=ud.exec(a);if(d)return d[3]}}function Ff(){var a={};this.has=function(b){return a.hasOwnProperty(b)};this.register=function(b,d){Ja(b,
"controller");D(b)?S(a,b):a[b]=d};this.$get=["$injector",function(b){function d(a,b,d,g){if(!a||!D(a.$scope))throw F("$controller")("noscp",g,b);a.$scope[b]=d}return function(c,e,f,g){var k,h,l;f=!0===f;g&&A(g)&&(l=g);if(A(c)){g=c.match(ud);if(!g)throw vd("ctrlfmt",c);h=g[1];l=l||g[3];c=a.hasOwnProperty(h)?a[h]:Ge(e.$scope,h,!0);if(!c)throw vd("ctrlreg",h);sb(c,h,!0)}if(f)return f=(H(c)?c[c.length-1]:c).prototype,k=Object.create(f||null),l&&d(e,l,k,h||c.name),S(function(){var a=b.invoke(c,k,e,h);
a!==k&&(D(a)||B(a))&&(k=a,l&&d(e,l,k,h||c.name));return k},{instance:k,identifier:l});k=b.instantiate(c,e,h);l&&d(e,l,k,h||c.name);return k}}]}function Gf(){this.$get=["$window",function(a){return x(a.document)}]}function Hf(){this.$get=["$document","$rootScope",function(a,b){function d(){e=c.hidden}var c=a[0],e=c&&c.hidden;a.on("visibilitychange",d);b.$on("$destroy",function(){a.off("visibilitychange",d)});return function(){return e}}]}function If(){this.$get=["$log",function(a){return function(b,
d){a.error.apply(a,arguments)}}]}function uc(a){return D(a)?ha(a)?a.toISOString():eb(a):a}function Of(){this.$get=function(){return function(a){if(!a)return"";var b=[];Oc(a,function(a,c){null===a||z(a)||B(a)||(H(a)?r(a,function(a){b.push(ba(c)+"="+ba(uc(a)))}):b.push(ba(c)+"="+ba(uc(a))))});return b.join("&")}}}function Pf(){this.$get=function(){return function(a){function b(a,e,f){H(a)?r(a,function(a,c){b(a,e+"["+(D(a)?c:"")+"]")}):D(a)&&!ha(a)?Oc(a,function(a,c){b(a,e+(f?"":"[")+c+(f?"":"]"))}):
(B(a)&&(a=a()),d.push(ba(e)+"="+(null==a?"":ba(uc(a)))))}if(!a)return"";var d=[];b(a,"",!0);return d.join("&")}}}function vc(a,b){if(A(a)){var d=a.replace(Gg,"").trim();if(d){var c=b("Content-Type"),c=c&&0===c.indexOf(wd),e;(e=c)||(e=(e=d.match(Hg))&&Ig[e[0]].test(d));if(e)try{a=Rc(d)}catch(f){if(!c)return a;throw Kb("baddata",a,f);}}}return a}function xd(a){var b=T(),d;A(a)?r(a.split("\n"),function(a){d=a.indexOf(":");var e=K(U(a.substr(0,d)));a=U(a.substr(d+1));e&&(b[e]=b[e]?b[e]+", "+a:a)}):D(a)&&
r(a,function(a,d){var f=K(d),g=U(a);f&&(b[f]=b[f]?b[f]+", "+g:g)});return b}function yd(a){var b;return function(d){b||(b=xd(a));return d?(d=b[K(d)],void 0===d&&(d=null),d):b}}function zd(a,b,d,c){if(B(c))return c(a,b,d);r(c,function(c){a=c(a,b,d)});return a}function Nf(){var a=this.defaults={transformResponse:[vc],transformRequest:[function(a){return D(a)&&"[object File]"!==la.call(a)&&"[object Blob]"!==la.call(a)&&"[object FormData]"!==la.call(a)?eb(a):a}],headers:{common:{Accept:"application/json, text/plain, */*"},
post:ja(wc),put:ja(wc),patch:ja(wc)},xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN",paramSerializer:"$httpParamSerializer",jsonpCallbackParam:"callback"},b=!1;this.useApplyAsync=function(a){return w(a)?(b=!!a,this):b};var d=this.interceptors=[],c=this.xsrfWhitelistedOrigins=[];this.$get=["$browser","$httpBackend","$$cookieReader","$cacheFactory","$rootScope","$q","$injector","$sce",function(e,f,g,k,h,l,m,p){function n(b){function c(a,b){for(var d=0,e=b.length;d<e;){var f=b[d++],g=b[d++];
a=a.then(f,g)}b.length=0;return a}function d(a,b){var c,e={};r(a,function(a,d){B(a)?(c=a(b),null!=c&&(e[d]=c)):e[d]=a});return e}function f(a){var b=S({},a);b.data=zd(a.data,a.headers,a.status,g.transformResponse);a=a.status;return 200<=a&&300>a?b:l.reject(b)}if(!D(b))throw F("$http")("badreq",b);if(!A(p.valueOf(b.url)))throw F("$http")("badreq",b.url);var g=S({method:"get",transformRequest:a.transformRequest,transformResponse:a.transformResponse,paramSerializer:a.paramSerializer,jsonpCallbackParam:a.jsonpCallbackParam},
b);g.headers=function(b){var c=a.headers,e=S({},b.headers),f,g,h,c=S({},c.common,c[K(b.method)]);a:for(f in c){g=K(f);for(h in e)if(K(h)===g)continue a;e[f]=c[f]}return d(e,ja(b))}(b);g.method=ub(g.method);g.paramSerializer=A(g.paramSerializer)?m.get(g.paramSerializer):g.paramSerializer;e.$$incOutstandingRequestCount("$http");var h=[],k=[];b=l.resolve(g);r(v,function(a){(a.request||a.requestError)&&h.unshift(a.request,a.requestError);(a.response||a.responseError)&&k.push(a.response,a.responseError)});
b=c(b,h);b=b.then(function(b){var c=b.headers,d=zd(b.data,yd(c),void 0,b.transformRequest);z(d)&&r(c,function(a,b){"content-type"===K(b)&&delete c[b]});z(b.withCredentials)&&!z(a.withCredentials)&&(b.withCredentials=a.withCredentials);return s(b,d).then(f,f)});b=c(b,k);return b=b.finally(function(){e.$$completeOutstandingRequest(E,"$http")})}function s(c,d){function e(a){if(a){var c={};r(a,function(a,d){c[d]=function(c){function d(){a(c)}b?h.$applyAsync(d):h.$$phase?d():h.$apply(d)}});return c}}function k(a,
c,d,e,f){function g(){m(c,a,d,e,f)}R&&(200<=a&&300>a?R.put(O,[a,c,xd(d),e,f]):R.remove(O));b?h.$applyAsync(g):(g(),h.$$phase||h.$apply())}function m(a,b,d,e,f){b=-1<=b?b:0;(200<=b&&300>b?L.resolve:L.reject)({data:a,status:b,headers:yd(d),config:c,statusText:e,xhrStatus:f})}function s(a){m(a.data,a.status,ja(a.headers()),a.statusText,a.xhrStatus)}function v(){var a=n.pendingRequests.indexOf(c);-1!==a&&n.pendingRequests.splice(a,1)}var L=l.defer(),u=L.promise,R,q,ma=c.headers,x="jsonp"===K(c.method),
O=c.url;x?O=p.getTrustedResourceUrl(O):A(O)||(O=p.valueOf(O));O=G(O,c.paramSerializer(c.params));x&&(O=t(O,c.jsonpCallbackParam));n.pendingRequests.push(c);u.then(v,v);!c.cache&&!a.cache||!1===c.cache||"GET"!==c.method&&"JSONP"!==c.method||(R=D(c.cache)?c.cache:D(a.cache)?a.cache:N);R&&(q=R.get(O),w(q)?q&&B(q.then)?q.then(s,s):H(q)?m(q[1],q[0],ja(q[2]),q[3],q[4]):m(q,200,{},"OK","complete"):R.put(O,u));z(q)&&((q=jc(c.url)?g()[c.xsrfCookieName||a.xsrfCookieName]:void 0)&&(ma[c.xsrfHeaderName||a.xsrfHeaderName]=
q),f(c.method,O,d,k,ma,c.timeout,c.withCredentials,c.responseType,e(c.eventHandlers),e(c.uploadEventHandlers)));return u}function G(a,b){0<b.length&&(a+=(-1===a.indexOf("?")?"?":"&")+b);return a}function t(a,b){var c=a.split("?");if(2<c.length)throw Kb("badjsonp",a);c=gc(c[1]);r(c,function(c,d){if("JSON_CALLBACK"===c)throw Kb("badjsonp",a);if(d===b)throw Kb("badjsonp",b,a);});return a+=(-1===a.indexOf("?")?"?":"&")+b+"=JSON_CALLBACK"}var N=k("$http");a.paramSerializer=A(a.paramSerializer)?m.get(a.paramSerializer):
a.paramSerializer;var v=[];r(d,function(a){v.unshift(A(a)?m.get(a):m.invoke(a))});var jc=Jg(c);n.pendingRequests=[];(function(a){r(arguments,function(a){n[a]=function(b,c){return n(S({},c||{},{method:a,url:b}))}})})("get","delete","head","jsonp");(function(a){r(arguments,function(a){n[a]=function(b,c,d){return n(S({},d||{},{method:a,url:b,data:c}))}})})("post","put","patch");n.defaults=a;return n}]}function Rf(){this.$get=function(){return function(){return new C.XMLHttpRequest}}}function Qf(){this.$get=
["$browser","$jsonpCallbacks","$document","$xhrFactory",function(a,b,d,c){return Kg(a,c,a.defer,b,d[0])}]}function Kg(a,b,d,c,e){function f(a,b,d){a=a.replace("JSON_CALLBACK",b);var f=e.createElement("script"),m=null;f.type="text/javascript";f.src=a;f.async=!0;m=function(a){f.removeEventListener("load",m);f.removeEventListener("error",m);e.body.removeChild(f);f=null;var g=-1,s="unknown";a&&("load"!==a.type||c.wasCalled(b)||(a={type:"error"}),s=a.type,g="error"===a.type?404:200);d&&d(g,s)};f.addEventListener("load",
m);f.addEventListener("error",m);e.body.appendChild(f);return m}return function(e,k,h,l,m,p,n,s,G,t){function N(a){J="timeout"===a;qa&&qa();y&&y.abort()}function v(a,b,c,e,f,g){w(P)&&d.cancel(P);qa=y=null;a(b,c,e,f,g)}k=k||a.url();if("jsonp"===K(e))var q=c.createCallback(k),qa=f(k,q,function(a,b){var d=200===a&&c.getResponse(q);v(l,a,d,"",b,"complete");c.removeCallback(q)});else{var y=b(e,k),J=!1;y.open(e,k,!0);r(m,function(a,b){w(a)&&y.setRequestHeader(b,a)});y.onload=function(){var a=y.statusText||
"",b="response"in y?y.response:y.responseText,c=1223===y.status?204:y.status;0===c&&(c=b?200:"file"===ga(k).protocol?404:0);v(l,c,b,y.getAllResponseHeaders(),a,"complete")};y.onerror=function(){v(l,-1,null,null,"","error")};y.ontimeout=function(){v(l,-1,null,null,"","timeout")};y.onabort=function(){v(l,-1,null,null,"",J?"timeout":"abort")};r(G,function(a,b){y.addEventListener(b,a)});r(t,function(a,b){y.upload.addEventListener(b,a)});n&&(y.withCredentials=!0);if(s)try{y.responseType=s}catch(I){if("json"!==
s)throw I;}y.send(z(h)?null:h)}if(0<p)var P=d(function(){N("timeout")},p);else p&&B(p.then)&&p.then(function(){N(w(p.$$timeoutId)?"timeout":"abort")})}}function Kf(){var a="{{",b="}}";this.startSymbol=function(b){return b?(a=b,this):a};this.endSymbol=function(a){return a?(b=a,this):b};this.$get=["$parse","$exceptionHandler","$sce",function(d,c,e){function f(a){return"\\\\\\"+a}function g(c){return c.replace(p,a).replace(n,b)}function k(a,b,c,d){var e=a.$watch(function(a){e();return d(a)},b,c);return e}
function h(f,h,n,p){function v(a){try{return a=n&&!r?e.getTrusted(n,a):e.valueOf(a),p&&!w(a)?a:ic(a)}catch(b){c(Ma.interr(f,b))}}var r=n===e.URL||n===e.MEDIA_URL;if(!f.length||-1===f.indexOf(a)){if(h)return;h=g(f);r&&(h=e.getTrusted(n,h));h=ia(h);h.exp=f;h.expressions=[];h.$$watchDelegate=k;return h}p=!!p;for(var q,y,J=0,I=[],P,Q=f.length,M=[],L=[],u;J<Q;)if(-1!==(q=f.indexOf(a,J))&&-1!==(y=f.indexOf(b,q+l)))J!==q&&M.push(g(f.substring(J,q))),J=f.substring(q+l,y),I.push(J),J=y+m,L.push(M.length),
M.push("");else{J!==Q&&M.push(g(f.substring(J)));break}u=1===M.length&&1===L.length;var R=r&&u?void 0:v;P=I.map(function(a){return d(a,R)});if(!h||I.length){var x=function(a){for(var b=0,c=I.length;b<c;b++){if(p&&z(a[b]))return;M[L[b]]=a[b]}if(r)return e.getTrusted(n,u?M[0]:M.join(""));n&&1<M.length&&Ma.throwNoconcat(f);return M.join("")};return S(function(a){var b=0,d=I.length,e=Array(d);try{for(;b<d;b++)e[b]=P[b](a);return x(e)}catch(g){c(Ma.interr(f,g))}},{exp:f,expressions:I,$$watchDelegate:function(a,
b){var c;return a.$watchGroup(P,function(d,e){var f=x(d);b.call(this,f,d!==e?c:f,a);c=f})}})}}var l=a.length,m=b.length,p=new RegExp(a.replace(/./g,f),"g"),n=new RegExp(b.replace(/./g,f),"g");h.startSymbol=function(){return a};h.endSymbol=function(){return b};return h}]}function Lf(){this.$get=["$$intervalFactory","$window",function(a,b){var d={},c=function(a){b.clearInterval(a);delete d[a]},e=a(function(a,c,e){a=b.setInterval(a,c);d[a]=e;return a},c);e.cancel=function(a){if(!a)return!1;if(!a.hasOwnProperty("$$intervalId"))throw Lg("badprom");
if(!d.hasOwnProperty(a.$$intervalId))return!1;a=a.$$intervalId;var b=d[a],e=b.promise;e.$$state&&(e.$$state.pur=!0);b.reject("canceled");c(a);return!0};return e}]}function Mf(){this.$get=["$browser","$q","$$q","$rootScope",function(a,b,d,c){return function(e,f){return function(g,k,h,l){function m(){p?g.apply(null,n):g(s)}var p=4<arguments.length,n=p?Ha.call(arguments,4):[],s=0,G=w(l)&&!l,t=(G?d:b).defer(),r=t.promise;h=w(h)?h:0;r.$$intervalId=e(function(){G?a.defer(m):c.$evalAsync(m);t.notify(s++);
0<h&&s>=h&&(t.resolve(s),f(r.$$intervalId));G||c.$apply()},k,t,G);return r}}}]}function Ad(a,b){var d=ga(a);b.$$protocol=d.protocol;b.$$host=d.hostname;b.$$port=fa(d.port)||Mg[d.protocol]||null}function Bd(a,b,d){if(Ng.test(a))throw jb("badpath",a);var c="/"!==a.charAt(0);c&&(a="/"+a);a=ga(a);for(var c=(c&&"/"===a.pathname.charAt(0)?a.pathname.substring(1):a.pathname).split("/"),e=c.length;e--;)c[e]=decodeURIComponent(c[e]),d&&(c[e]=c[e].replace(/\//g,"%2F"));d=c.join("/");b.$$path=d;b.$$search=gc(a.search);
b.$$hash=decodeURIComponent(a.hash);b.$$path&&"/"!==b.$$path.charAt(0)&&(b.$$path="/"+b.$$path)}function xc(a,b){return a.slice(0,b.length)===b}function xa(a,b){if(xc(b,a))return b.substr(a.length)}function Da(a){var b=a.indexOf("#");return-1===b?a:a.substr(0,b)}function yc(a,b,d){this.$$html5=!0;d=d||"";Ad(a,this);this.$$parse=function(a){var d=xa(b,a);if(!A(d))throw jb("ipthprfx",a,b);Bd(d,this,!0);this.$$path||(this.$$path="/");this.$$compose()};this.$$normalizeUrl=function(a){return b+a.substr(1)};
this.$$parseLinkUrl=function(c,e){if(e&&"#"===e[0])return this.hash(e.slice(1)),!0;var f,g;w(f=xa(a,c))?(g=f,g=d&&w(f=xa(d,f))?b+(xa("/",f)||f):a+g):w(f=xa(b,c))?g=b+f:b===c+"/"&&(g=b);g&&this.$$parse(g);return!!g}}function zc(a,b,d){Ad(a,this);this.$$parse=function(c){var e=xa(a,c)||xa(b,c),f;z(e)||"#"!==e.charAt(0)?this.$$html5?f=e:(f="",z(e)&&(a=c,this.replace())):(f=xa(d,e),z(f)&&(f=e));Bd(f,this,!1);c=this.$$path;var e=a,g=/^\/[A-Z]:(\/.*)/;xc(f,e)&&(f=f.replace(e,""));g.exec(f)||(c=(f=g.exec(c))?
f[1]:c);this.$$path=c;this.$$compose()};this.$$normalizeUrl=function(b){return a+(b?d+b:"")};this.$$parseLinkUrl=function(b,d){return Da(a)===Da(b)?(this.$$parse(b),!0):!1}}function Cd(a,b,d){this.$$html5=!0;zc.apply(this,arguments);this.$$parseLinkUrl=function(c,e){if(e&&"#"===e[0])return this.hash(e.slice(1)),!0;var f,g;a===Da(c)?f=c:(g=xa(b,c))?f=a+d+g:b===c+"/"&&(f=b);f&&this.$$parse(f);return!!f};this.$$normalizeUrl=function(b){return a+d+b}}function Lb(a){return function(){return this[a]}}function Dd(a,
b){return function(d){if(z(d))return this[a];this[a]=b(d);this.$$compose();return this}}function Tf(){var a="!",b={enabled:!1,requireBase:!0,rewriteLinks:!0};this.hashPrefix=function(b){return w(b)?(a=b,this):a};this.html5Mode=function(a){if(Ga(a))return b.enabled=a,this;if(D(a)){Ga(a.enabled)&&(b.enabled=a.enabled);Ga(a.requireBase)&&(b.requireBase=a.requireBase);if(Ga(a.rewriteLinks)||A(a.rewriteLinks))b.rewriteLinks=a.rewriteLinks;return this}return b};this.$get=["$rootScope","$browser","$sniffer",
"$rootElement","$window",function(d,c,e,f,g){function k(a,b){return a===b||ga(a).href===ga(b).href}function h(a,b,d){var e=m.url(),f=m.$$state;try{c.url(a,b,d),m.$$state=c.state()}catch(g){throw m.url(e),m.$$state=f,g;}}function l(a,b){d.$broadcast("$locationChangeSuccess",m.absUrl(),a,m.$$state,b)}var m,p;p=c.baseHref();var n=c.url(),s;if(b.enabled){if(!p&&b.requireBase)throw jb("nobase");s=n.substring(0,n.indexOf("/",n.indexOf("//")+2))+(p||"/");p=e.history?yc:Cd}else s=Da(n),p=zc;var r=s.substr(0,
Da(s).lastIndexOf("/")+1);m=new p(s,r,"#"+a);m.$$parseLinkUrl(n,n);m.$$state=c.state();var t=/^\s*(javascript|mailto):/i;f.on("click",function(a){var e=b.rewriteLinks;if(e&&!a.ctrlKey&&!a.metaKey&&!a.shiftKey&&2!==a.which&&2!==a.button){for(var g=x(a.target);"a"!==ua(g[0]);)if(g[0]===f[0]||!(g=g.parent())[0])return;if(!A(e)||!z(g.attr(e))){var e=g.prop("href"),h=g.attr("href")||g.attr("xlink:href");D(e)&&"[object SVGAnimatedString]"===e.toString()&&(e=ga(e.animVal).href);t.test(e)||!e||g.attr("target")||
a.isDefaultPrevented()||!m.$$parseLinkUrl(e,h)||(a.preventDefault(),m.absUrl()!==c.url()&&d.$apply())}}});m.absUrl()!==n&&c.url(m.absUrl(),!0);var N=!0;c.onUrlChange(function(a,b){xc(a,r)?(d.$evalAsync(function(){var c=m.absUrl(),e=m.$$state,f;m.$$parse(a);m.$$state=b;f=d.$broadcast("$locationChangeStart",a,c,b,e).defaultPrevented;m.absUrl()===a&&(f?(m.$$parse(c),m.$$state=e,h(c,!1,e)):(N=!1,l(c,e)))}),d.$$phase||d.$digest()):g.location.href=a});d.$watch(function(){if(N||m.$$urlUpdatedByLocation){m.$$urlUpdatedByLocation=
!1;var a=c.url(),b=m.absUrl(),f=c.state(),g=m.$$replace,n=!k(a,b)||m.$$html5&&e.history&&f!==m.$$state;if(N||n)N=!1,d.$evalAsync(function(){var b=m.absUrl(),c=d.$broadcast("$locationChangeStart",b,a,m.$$state,f).defaultPrevented;m.absUrl()===b&&(c?(m.$$parse(a),m.$$state=f):(n&&h(b,g,f===m.$$state?null:m.$$state),l(a,f)))})}m.$$replace=!1});return m}]}function Uf(){var a=!0,b=this;this.debugEnabled=function(b){return w(b)?(a=b,this):a};this.$get=["$window",function(d){function c(a){cc(a)&&(a.stack&&
f?a=a.message&&-1===a.stack.indexOf(a.message)?"Error: "+a.message+"\n"+a.stack:a.stack:a.sourceURL&&(a=a.message+"\n"+a.sourceURL+":"+a.line));return a}function e(a){var b=d.console||{},e=b[a]||b.log||E;return function(){var a=[];r(arguments,function(b){a.push(c(b))});return Function.prototype.apply.call(e,b,a)}}var f=Ca||/\bEdge\//.test(d.navigator&&d.navigator.userAgent);return{log:e("log"),info:e("info"),warn:e("warn"),error:e("error"),debug:function(){var c=e("debug");return function(){a&&c.apply(b,
arguments)}}()}}]}function Og(a){return a+""}function Pg(a,b){return"undefined"!==typeof a?a:b}function Ed(a,b){return"undefined"===typeof a?b:"undefined"===typeof b?a:a+b}function Qg(a,b){switch(a.type){case q.MemberExpression:if(a.computed)return!1;break;case q.UnaryExpression:return 1;case q.BinaryExpression:return"+"!==a.operator?1:!1;case q.CallExpression:return!1}return void 0===b?Fd:b}function Z(a,b,d){var c,e,f=a.isPure=Qg(a,d);switch(a.type){case q.Program:c=!0;r(a.body,function(a){Z(a.expression,
b,f);c=c&&a.expression.constant});a.constant=c;break;case q.Literal:a.constant=!0;a.toWatch=[];break;case q.UnaryExpression:Z(a.argument,b,f);a.constant=a.argument.constant;a.toWatch=a.argument.toWatch;break;case q.BinaryExpression:Z(a.left,b,f);Z(a.right,b,f);a.constant=a.left.constant&&a.right.constant;a.toWatch=a.left.toWatch.concat(a.right.toWatch);break;case q.LogicalExpression:Z(a.left,b,f);Z(a.right,b,f);a.constant=a.left.constant&&a.right.constant;a.toWatch=a.constant?[]:[a];break;case q.ConditionalExpression:Z(a.test,
b,f);Z(a.alternate,b,f);Z(a.consequent,b,f);a.constant=a.test.constant&&a.alternate.constant&&a.consequent.constant;a.toWatch=a.constant?[]:[a];break;case q.Identifier:a.constant=!1;a.toWatch=[a];break;case q.MemberExpression:Z(a.object,b,f);a.computed&&Z(a.property,b,f);a.constant=a.object.constant&&(!a.computed||a.property.constant);a.toWatch=a.constant?[]:[a];break;case q.CallExpression:c=d=a.filter?!b(a.callee.name).$stateful:!1;e=[];r(a.arguments,function(a){Z(a,b,f);c=c&&a.constant;e.push.apply(e,
a.toWatch)});a.constant=c;a.toWatch=d?e:[a];break;case q.AssignmentExpression:Z(a.left,b,f);Z(a.right,b,f);a.constant=a.left.constant&&a.right.constant;a.toWatch=[a];break;case q.ArrayExpression:c=!0;e=[];r(a.elements,function(a){Z(a,b,f);c=c&&a.constant;e.push.apply(e,a.toWatch)});a.constant=c;a.toWatch=e;break;case q.ObjectExpression:c=!0;e=[];r(a.properties,function(a){Z(a.value,b,f);c=c&&a.value.constant;e.push.apply(e,a.value.toWatch);a.computed&&(Z(a.key,b,!1),c=c&&a.key.constant,e.push.apply(e,
a.key.toWatch))});a.constant=c;a.toWatch=e;break;case q.ThisExpression:a.constant=!1;a.toWatch=[];break;case q.LocalsExpression:a.constant=!1,a.toWatch=[]}}function Gd(a){if(1===a.length){a=a[0].expression;var b=a.toWatch;return 1!==b.length?b:b[0]!==a?b:void 0}}function Hd(a){return a.type===q.Identifier||a.type===q.MemberExpression}function Id(a){if(1===a.body.length&&Hd(a.body[0].expression))return{type:q.AssignmentExpression,left:a.body[0].expression,right:{type:q.NGValueParameter},operator:"="}}
function Jd(a){this.$filter=a}function Kd(a){this.$filter=a}function Mb(a,b,d){this.ast=new q(a,d);this.astCompiler=d.csp?new Kd(b):new Jd(b)}function Ac(a){return B(a.valueOf)?a.valueOf():Rg.call(a)}function Vf(){var a=T(),b={"true":!0,"false":!1,"null":null,undefined:void 0},d,c;this.addLiteral=function(a,c){b[a]=c};this.setIdentifierFns=function(a,b){d=a;c=b;return this};this.$get=["$filter",function(e){function f(b,c){var d,f;switch(typeof b){case "string":return f=b=b.trim(),d=a[f],d||(d=new Nb(G),
d=(new Mb(d,e,G)).parse(b),a[f]=p(d)),s(d,c);case "function":return s(b,c);default:return s(E,c)}}function g(a,b,c){return null==a||null==b?a===b:"object"!==typeof a||(a=Ac(a),"object"!==typeof a||c)?a===b||a!==a&&b!==b:!1}function k(a,b,c,d,e){var f=d.inputs,h;if(1===f.length){var k=g,f=f[0];return a.$watch(function(a){var b=f(a);g(b,k,f.isPure)||(h=d(a,void 0,void 0,[b]),k=b&&Ac(b));return h},b,c,e)}for(var l=[],m=[],n=0,p=f.length;n<p;n++)l[n]=g,m[n]=null;return a.$watch(function(a){for(var b=
!1,c=0,e=f.length;c<e;c++){var k=f[c](a);if(b||(b=!g(k,l[c],f[c].isPure)))m[c]=k,l[c]=k&&Ac(k)}b&&(h=d(a,void 0,void 0,m));return h},b,c,e)}function h(a,b,c,d,e){function f(){h(m)&&k()}function g(a,b,c,d){m=u&&d?d[0]:n(a,b,c,d);h(m)&&a.$$postDigest(f);return s(m)}var h=d.literal?l:w,k,m,n=d.$$intercepted||d,s=d.$$interceptor||Ta,u=d.inputs&&!n.inputs;g.literal=d.literal;g.constant=d.constant;g.inputs=d.inputs;p(g);return k=a.$watch(g,b,c,e)}function l(a){var b=!0;r(a,function(a){w(a)||(b=!1)});return b}
function m(a,b,c,d){var e=a.$watch(function(a){e();return d(a)},b,c);return e}function p(a){a.constant?a.$$watchDelegate=m:a.oneTime?a.$$watchDelegate=h:a.inputs&&(a.$$watchDelegate=k);return a}function n(a,b){function c(d){return b(a(d))}c.$stateful=a.$stateful||b.$stateful;c.$$pure=a.$$pure&&b.$$pure;return c}function s(a,b){if(!b)return a;a.$$interceptor&&(b=n(a.$$interceptor,b),a=a.$$intercepted);var c=!1,d=function(d,e,f,g){d=c&&g?g[0]:a(d,e,f,g);return b(d)};d.$$intercepted=a;d.$$interceptor=
b;d.literal=a.literal;d.oneTime=a.oneTime;d.constant=a.constant;b.$stateful||(c=!a.inputs,d.inputs=a.inputs?a.inputs:[a],b.$$pure||(d.inputs=d.inputs.map(function(a){return a.isPure===Fd?function(b){return a(b)}:a})));return p(d)}var G={csp:Aa().noUnsafeEval,literals:Ia(b),isIdentifierStart:B(d)&&d,isIdentifierContinue:B(c)&&c};f.$$getAst=function(a){var b=new Nb(G);return(new Mb(b,e,G)).getAst(a).ast};return f}]}function Xf(){var a=!0;this.$get=["$rootScope","$exceptionHandler",function(b,d){return Ld(function(a){b.$evalAsync(a)},
d,a)}];this.errorOnUnhandledRejections=function(b){return w(b)?(a=b,this):a}}function Yf(){var a=!0;this.$get=["$browser","$exceptionHandler",function(b,d){return Ld(function(a){b.defer(a)},d,a)}];this.errorOnUnhandledRejections=function(b){return w(b)?(a=b,this):a}}function Ld(a,b,d){function c(){return new e}function e(){var a=this.promise=new f;this.resolve=function(b){h(a,b)};this.reject=function(b){m(a,b)};this.notify=function(b){n(a,b)}}function f(){this.$$state={status:0}}function g(){for(;!w&&
x.length;){var a=x.shift();if(!a.pur){a.pur=!0;var c=a.value,c="Possibly unhandled rejection: "+("function"===typeof c?c.toString().replace(/ \{[\s\S]*$/,""):z(c)?"undefined":"string"!==typeof c?Ie(c,void 0):c);cc(a.value)?b(a.value,c):b(c)}}}function k(c){!d||c.pending||2!==c.status||c.pur||(0===w&&0===x.length&&a(g),x.push(c));!c.processScheduled&&c.pending&&(c.processScheduled=!0,++w,a(function(){var e,f,k;k=c.pending;c.processScheduled=!1;c.pending=void 0;try{for(var l=0,n=k.length;l<n;++l){c.pur=
!0;f=k[l][0];e=k[l][c.status];try{B(e)?h(f,e(c.value)):1===c.status?h(f,c.value):m(f,c.value)}catch(p){m(f,p),p&&!0===p.$$passToExceptionHandler&&b(p)}}}finally{--w,d&&0===w&&a(g)}}))}function h(a,b){a.$$state.status||(b===a?p(a,v("qcycle",b)):l(a,b))}function l(a,b){function c(b){g||(g=!0,l(a,b))}function d(b){g||(g=!0,p(a,b))}function e(b){n(a,b)}var f,g=!1;try{if(D(b)||B(b))f=b.then;B(f)?(a.$$state.status=-1,f.call(b,c,d,e)):(a.$$state.value=b,a.$$state.status=1,k(a.$$state))}catch(h){d(h)}}function m(a,
b){a.$$state.status||p(a,b)}function p(a,b){a.$$state.value=b;a.$$state.status=2;k(a.$$state)}function n(c,d){var e=c.$$state.pending;0>=c.$$state.status&&e&&e.length&&a(function(){for(var a,c,f=0,g=e.length;f<g;f++){c=e[f][0];a=e[f][3];try{n(c,B(a)?a(d):d)}catch(h){b(h)}}})}function s(a){var b=new f;m(b,a);return b}function G(a,b,c){var d=null;try{B(c)&&(d=c())}catch(e){return s(e)}return d&&B(d.then)?d.then(function(){return b(a)},s):b(a)}function t(a,b,c,d){var e=new f;h(e,a);return e.then(b,c,
d)}function q(a){if(!B(a))throw v("norslvr",a);var b=new f;a(function(a){h(b,a)},function(a){m(b,a)});return b}var v=F("$q",TypeError),w=0,x=[];S(f.prototype,{then:function(a,b,c){if(z(a)&&z(b)&&z(c))return this;var d=new f;this.$$state.pending=this.$$state.pending||[];this.$$state.pending.push([d,a,b,c]);0<this.$$state.status&&k(this.$$state);return d},"catch":function(a){return this.then(null,a)},"finally":function(a,b){return this.then(function(b){return G(b,y,a)},function(b){return G(b,s,a)},
b)}});var y=t;q.prototype=f.prototype;q.defer=c;q.reject=s;q.when=t;q.resolve=y;q.all=function(a){var b=new f,c=0,d=H(a)?[]:{};r(a,function(a,e){c++;t(a).then(function(a){d[e]=a;--c||h(b,d)},function(a){m(b,a)})});0===c&&h(b,d);return b};q.race=function(a){var b=c();r(a,function(a){t(a).then(b.resolve,b.reject)});return b.promise};return q}function hg(){this.$get=["$window","$timeout",function(a,b){var d=a.requestAnimationFrame||a.webkitRequestAnimationFrame,c=a.cancelAnimationFrame||a.webkitCancelAnimationFrame||
a.webkitCancelRequestAnimationFrame,e=!!d,f=e?function(a){var b=d(a);return function(){c(b)}}:function(a){var c=b(a,16.66,!1);return function(){b.cancel(c)}};f.supported=e;return f}]}function Wf(){function a(a){function b(){this.$$watchers=this.$$nextSibling=this.$$childHead=this.$$childTail=null;this.$$listeners={};this.$$listenerCount={};this.$$watchersCount=0;this.$id=++pb;this.$$ChildScope=null;this.$$suspended=!1}b.prototype=a;return b}var b=10,d=F("$rootScope"),c=null,e=null;this.digestTtl=
function(a){arguments.length&&(b=a);return b};this.$get=["$exceptionHandler","$parse","$browser",function(f,g,k){function h(a){a.currentScope.$$destroyed=!0}function l(a){9===Ca&&(a.$$childHead&&l(a.$$childHead),a.$$nextSibling&&l(a.$$nextSibling));a.$parent=a.$$nextSibling=a.$$prevSibling=a.$$childHead=a.$$childTail=a.$root=a.$$watchers=null}function m(){this.$id=++pb;this.$$phase=this.$parent=this.$$watchers=this.$$nextSibling=this.$$prevSibling=this.$$childHead=this.$$childTail=null;this.$root=
this;this.$$suspended=this.$$destroyed=!1;this.$$listeners={};this.$$listenerCount={};this.$$watchersCount=0;this.$$isolateBindings=null}function p(a){if(v.$$phase)throw d("inprog",v.$$phase);v.$$phase=a}function n(a,b){do a.$$watchersCount+=b;while(a=a.$parent)}function s(a,b,c){do a.$$listenerCount[c]-=b,0===a.$$listenerCount[c]&&delete a.$$listenerCount[c];while(a=a.$parent)}function G(){}function t(){for(;y.length;)try{y.shift()()}catch(a){f(a)}e=null}function q(){null===e&&(e=k.defer(function(){v.$apply(t)},
null,"$applyAsync"))}m.prototype={constructor:m,$new:function(b,c){var d;c=c||this;b?(d=new m,d.$root=this.$root):(this.$$ChildScope||(this.$$ChildScope=a(this)),d=new this.$$ChildScope);d.$parent=c;d.$$prevSibling=c.$$childTail;c.$$childHead?(c.$$childTail.$$nextSibling=d,c.$$childTail=d):c.$$childHead=c.$$childTail=d;(b||c!==this)&&d.$on("$destroy",h);return d},$watch:function(a,b,d,e){var f=g(a);b=B(b)?b:E;if(f.$$watchDelegate)return f.$$watchDelegate(this,b,d,f,a);var h=this,k=h.$$watchers,l=
{fn:b,last:G,get:f,exp:e||a,eq:!!d};c=null;k||(k=h.$$watchers=[],k.$$digestWatchIndex=-1);k.unshift(l);k.$$digestWatchIndex++;n(this,1);return function(){var a=cb(k,l);0<=a&&(n(h,-1),a<k.$$digestWatchIndex&&k.$$digestWatchIndex--);c=null}},$watchGroup:function(a,b){function c(){h=!1;try{k?(k=!1,b(e,e,g)):b(e,d,g)}finally{for(var f=0;f<a.length;f++)d[f]=e[f]}}var d=Array(a.length),e=Array(a.length),f=[],g=this,h=!1,k=!0;if(!a.length){var l=!0;g.$evalAsync(function(){l&&b(e,e,g)});return function(){l=
!1}}if(1===a.length)return this.$watch(a[0],function(a,c,f){e[0]=a;d[0]=c;b(e,a===c?e:d,f)});r(a,function(a,b){var d=g.$watch(a,function(a){e[b]=a;h||(h=!0,g.$evalAsync(c))});f.push(d)});return function(){for(;f.length;)f.shift()()}},$watchCollection:function(a,b){function c(a){e=a;var b,d,g,h;if(!z(e)){if(D(e))if(ya(e))for(f!==n&&(f=n,t=f.length=0,l++),a=e.length,t!==a&&(l++,f.length=t=a),b=0;b<a;b++)h=f[b],g=e[b],d=h!==h&&g!==g,d||h===g||(l++,f[b]=g);else{f!==p&&(f=p={},t=0,l++);a=0;for(b in e)ta.call(e,
b)&&(a++,g=e[b],h=f[b],b in f?(d=h!==h&&g!==g,d||h===g||(l++,f[b]=g)):(t++,f[b]=g,l++));if(t>a)for(b in l++,f)ta.call(e,b)||(t--,delete f[b])}else f!==e&&(f=e,l++);return l}}c.$$pure=g(a).literal;c.$stateful=!c.$$pure;var d=this,e,f,h,k=1<b.length,l=0,m=g(a,c),n=[],p={},s=!0,t=0;return this.$watch(m,function(){s?(s=!1,b(e,e,d)):b(e,h,d);if(k)if(D(e))if(ya(e)){h=Array(e.length);for(var a=0;a<e.length;a++)h[a]=e[a]}else for(a in h={},e)ta.call(e,a)&&(h[a]=e[a]);else h=e})},$digest:function(){var a,
g,h,l,m,n,s,r=b,q,y=w.length?v:this,N=[],z,A;p("$digest");k.$$checkUrlChange();this===v&&null!==e&&(k.defer.cancel(e),t());c=null;do{s=!1;q=y;for(n=0;n<w.length;n++){try{A=w[n],l=A.fn,l(A.scope,A.locals)}catch(C){f(C)}c=null}w.length=0;a:do{if(n=!q.$$suspended&&q.$$watchers)for(n.$$digestWatchIndex=n.length;n.$$digestWatchIndex--;)try{if(a=n[n.$$digestWatchIndex])if(m=a.get,(g=m(q))!==(h=a.last)&&!(a.eq?va(g,h):X(g)&&X(h)))s=!0,c=a,a.last=a.eq?Ia(g,null):g,l=a.fn,l(g,h===G?g:h,q),5>r&&(z=4-r,N[z]||
(N[z]=[]),N[z].push({msg:B(a.exp)?"fn: "+(a.exp.name||a.exp.toString()):a.exp,newVal:g,oldVal:h}));else if(a===c){s=!1;break a}}catch(E){f(E)}if(!(n=!q.$$suspended&&q.$$watchersCount&&q.$$childHead||q!==y&&q.$$nextSibling))for(;q!==y&&!(n=q.$$nextSibling);)q=q.$parent}while(q=n);if((s||w.length)&&!r--)throw v.$$phase=null,d("infdig",b,N);}while(s||w.length);for(v.$$phase=null;J<x.length;)try{x[J++]()}catch(D){f(D)}x.length=J=0;k.$$checkUrlChange()},$suspend:function(){this.$$suspended=!0},$isSuspended:function(){return this.$$suspended},
$resume:function(){this.$$suspended=!1},$destroy:function(){if(!this.$$destroyed){var a=this.$parent;this.$broadcast("$destroy");this.$$destroyed=!0;this===v&&k.$$applicationDestroyed();n(this,-this.$$watchersCount);for(var b in this.$$listenerCount)s(this,this.$$listenerCount[b],b);a&&a.$$childHead===this&&(a.$$childHead=this.$$nextSibling);a&&a.$$childTail===this&&(a.$$childTail=this.$$prevSibling);this.$$prevSibling&&(this.$$prevSibling.$$nextSibling=this.$$nextSibling);this.$$nextSibling&&(this.$$nextSibling.$$prevSibling=
this.$$prevSibling);this.$destroy=this.$digest=this.$apply=this.$evalAsync=this.$applyAsync=E;this.$on=this.$watch=this.$watchGroup=function(){return E};this.$$listeners={};this.$$nextSibling=null;l(this)}},$eval:function(a,b){return g(a)(this,b)},$evalAsync:function(a,b){v.$$phase||w.length||k.defer(function(){w.length&&v.$digest()},null,"$evalAsync");w.push({scope:this,fn:g(a),locals:b})},$$postDigest:function(a){x.push(a)},$apply:function(a){try{p("$apply");try{return this.$eval(a)}finally{v.$$phase=
null}}catch(b){f(b)}finally{try{v.$digest()}catch(c){throw f(c),c;}}},$applyAsync:function(a){function b(){c.$eval(a)}var c=this;a&&y.push(b);a=g(a);q()},$on:function(a,b){var c=this.$$listeners[a];c||(this.$$listeners[a]=c=[]);c.push(b);var d=this;do d.$$listenerCount[a]||(d.$$listenerCount[a]=0),d.$$listenerCount[a]++;while(d=d.$parent);var e=this;return function(){var d=c.indexOf(b);-1!==d&&(delete c[d],s(e,1,a))}},$emit:function(a,b){var c=[],d,e=this,g=!1,h={name:a,targetScope:e,stopPropagation:function(){g=
!0},preventDefault:function(){h.defaultPrevented=!0},defaultPrevented:!1},k=db([h],arguments,1),l,m;do{d=e.$$listeners[a]||c;h.currentScope=e;l=0;for(m=d.length;l<m;l++)if(d[l])try{d[l].apply(null,k)}catch(n){f(n)}else d.splice(l,1),l--,m--;if(g)break;e=e.$parent}while(e);h.currentScope=null;return h},$broadcast:function(a,b){var c=this,d=this,e={name:a,targetScope:this,preventDefault:function(){e.defaultPrevented=!0},defaultPrevented:!1};if(!this.$$listenerCount[a])return e;for(var g=db([e],arguments,
1),h,k;c=d;){e.currentScope=c;d=c.$$listeners[a]||[];h=0;for(k=d.length;h<k;h++)if(d[h])try{d[h].apply(null,g)}catch(l){f(l)}else d.splice(h,1),h--,k--;if(!(d=c.$$listenerCount[a]&&c.$$childHead||c!==this&&c.$$nextSibling))for(;c!==this&&!(d=c.$$nextSibling);)c=c.$parent}e.currentScope=null;return e}};var v=new m,w=v.$$asyncQueue=[],x=v.$$postDigestQueue=[],y=v.$$applyAsyncQueue=[],J=0;return v}]}function Le(){var a=/^\s*(https?|s?ftp|mailto|tel|file):/,b=/^\s*((https?|ftp|file|blob):|data:image\/)/;
this.aHrefSanitizationWhitelist=function(b){return w(b)?(a=b,this):a};this.imgSrcSanitizationWhitelist=function(a){return w(a)?(b=a,this):b};this.$get=function(){return function(d,c){var e=c?b:a,f=ga(d&&d.trim()).href;return""===f||f.match(e)?d:"unsafe:"+f}}}function Sg(a){if("self"===a)return a;if(A(a)){if(-1<a.indexOf("***"))throw Ea("iwcard",a);a=Md(a).replace(/\\\*\\\*/g,".*").replace(/\\\*/g,"[^:/.?&;]*");return new RegExp("^"+a+"$")}if(ab(a))return new RegExp("^"+a.source+"$");throw Ea("imatcher");
}function Nd(a){var b=[];w(a)&&r(a,function(a){b.push(Sg(a))});return b}function $f(){this.SCE_CONTEXTS=V;var a=["self"],b=[];this.resourceUrlWhitelist=function(b){arguments.length&&(a=Nd(b));return a};this.resourceUrlBlacklist=function(a){arguments.length&&(b=Nd(a));return b};this.$get=["$injector","$$sanitizeUri",function(d,c){function e(a,b){var c;"self"===a?(c=Bc(b,Od))||(C.document.baseURI?c=C.document.baseURI:(Na||(Na=C.document.createElement("a"),Na.href=".",Na=Na.cloneNode(!1)),c=Na.href),
c=Bc(b,c)):c=!!a.exec(b.href);return c}function f(a){var b=function(a){this.$$unwrapTrustedValue=function(){return a}};a&&(b.prototype=new a);b.prototype.valueOf=function(){return this.$$unwrapTrustedValue()};b.prototype.toString=function(){return this.$$unwrapTrustedValue().toString()};return b}var g=function(a){throw Ea("unsafe");};d.has("$sanitize")&&(g=d.get("$sanitize"));var k=f(),h={};h[V.HTML]=f(k);h[V.CSS]=f(k);h[V.MEDIA_URL]=f(k);h[V.URL]=f(h[V.MEDIA_URL]);h[V.JS]=f(k);h[V.RESOURCE_URL]=
f(h[V.URL]);return{trustAs:function(a,b){var c=h.hasOwnProperty(a)?h[a]:null;if(!c)throw Ea("icontext",a,b);if(null===b||z(b)||""===b)return b;if("string"!==typeof b)throw Ea("itype",a);return new c(b)},getTrusted:function(d,f){if(null===f||z(f)||""===f)return f;var k=h.hasOwnProperty(d)?h[d]:null;if(k&&f instanceof k)return f.$$unwrapTrustedValue();B(f.$$unwrapTrustedValue)&&(f=f.$$unwrapTrustedValue());if(d===V.MEDIA_URL||d===V.URL)return c(f.toString(),d===V.MEDIA_URL);if(d===V.RESOURCE_URL){var k=
ga(f.toString()),n,s,r=!1;n=0;for(s=a.length;n<s;n++)if(e(a[n],k)){r=!0;break}if(r)for(n=0,s=b.length;n<s;n++)if(e(b[n],k)){r=!1;break}if(r)return f;throw Ea("insecurl",f.toString());}if(d===V.HTML)return g(f);throw Ea("unsafe");},valueOf:function(a){return a instanceof k?a.$$unwrapTrustedValue():a}}}]}function Zf(){var a=!0;this.enabled=function(b){arguments.length&&(a=!!b);return a};this.$get=["$parse","$sceDelegate",function(b,d){if(a&&8>Ca)throw Ea("iequirks");var c=ja(V);c.isEnabled=function(){return a};
c.trustAs=d.trustAs;c.getTrusted=d.getTrusted;c.valueOf=d.valueOf;a||(c.trustAs=c.getTrusted=function(a,b){return b},c.valueOf=Ta);c.parseAs=function(a,d){var e=b(d);return e.literal&&e.constant?e:b(d,function(b){return c.getTrusted(a,b)})};var e=c.parseAs,f=c.getTrusted,g=c.trustAs;r(V,function(a,b){var d=K(b);c[("parse_as_"+d).replace(Cc,wb)]=function(b){return e(a,b)};c[("get_trusted_"+d).replace(Cc,wb)]=function(b){return f(a,b)};c[("trust_as_"+d).replace(Cc,wb)]=function(b){return g(a,b)}});
return c}]}function ag(){this.$get=["$window","$document",function(a,b){var d={},c=!((!a.nw||!a.nw.process)&&a.chrome&&(a.chrome.app&&a.chrome.app.runtime||!a.chrome.app&&a.chrome.runtime&&a.chrome.runtime.id))&&a.history&&a.history.pushState,e=fa((/android (\d+)/.exec(K((a.navigator||{}).userAgent))||[])[1]),f=/Boxee/i.test((a.navigator||{}).userAgent),g=b[0]||{},k=g.body&&g.body.style,h=!1,l=!1;k&&(h=!!("transition"in k||"webkitTransition"in k),l=!!("animation"in k||"webkitAnimation"in k));return{history:!(!c||
4>e||f),hasEvent:function(a){if("input"===a&&Ca)return!1;if(z(d[a])){var b=g.createElement("div");d[a]="on"+a in b}return d[a]},csp:Aa(),transitions:h,animations:l,android:e}}]}function bg(){this.$get=ia(function(a){return new Tg(a)})}function Tg(a){function b(){var a=e.pop();return a&&a.cb}function d(a){for(var b=e.length-1;0<=b;--b){var c=e[b];if(c.type===a)return e.splice(b,1),c.cb}}var c={},e=[],f=this.ALL_TASKS_TYPE="$$all$$",g=this.DEFAULT_TASK_TYPE="$$default$$";this.completeTask=function(e,
h){h=h||g;try{e()}finally{var l;l=h||g;c[l]&&(c[l]--,c[f]--);l=c[h];var m=c[f];if(!m||!l)for(l=m?d:b;m=l(h);)try{m()}catch(p){a.error(p)}}};this.incTaskCount=function(a){a=a||g;c[a]=(c[a]||0)+1;c[f]=(c[f]||0)+1};this.notifyWhenNoPendingTasks=function(a,b){b=b||f;c[b]?e.push({type:b,cb:a}):a()}}function dg(){var a;this.httpOptions=function(b){return b?(a=b,this):a};this.$get=["$exceptionHandler","$templateCache","$http","$q","$sce",function(b,d,c,e,f){function g(k,h){g.totalPendingRequests++;if(!A(k)||
z(d.get(k)))k=f.getTrustedResourceUrl(k);var l=c.defaults&&c.defaults.transformResponse;H(l)?l=l.filter(function(a){return a!==vc}):l===vc&&(l=null);return c.get(k,S({cache:d,transformResponse:l},a)).finally(function(){g.totalPendingRequests--}).then(function(a){return d.put(k,a.data)},function(a){h||(a=Ug("tpload",k,a.status,a.statusText),b(a));return e.reject(a)})}g.totalPendingRequests=0;return g}]}function eg(){this.$get=["$rootScope","$browser","$location",function(a,b,d){return{findBindings:function(a,
b,d){a=a.getElementsByClassName("ng-binding");var g=[];r(a,function(a){var c=ca.element(a).data("$binding");c&&r(c,function(c){d?(new RegExp("(^|\\s)"+Md(b)+"(\\s|\\||$)")).test(c)&&g.push(a):-1!==c.indexOf(b)&&g.push(a)})});return g},findModels:function(a,b,d){for(var g=["ng-","data-ng-","ng\\:"],k=0;k<g.length;++k){var h=a.querySelectorAll("["+g[k]+"model"+(d?"=":"*=")+'"'+b+'"]');if(h.length)return h}},getLocation:function(){return d.url()},setLocation:function(b){b!==d.url()&&(d.url(b),a.$digest())},
whenStable:function(a){b.notifyWhenNoOutstandingRequests(a)}}}]}function fg(){this.$get=["$rootScope","$browser","$q","$$q","$exceptionHandler",function(a,b,d,c,e){function f(f,h,l){B(f)||(l=h,h=f,f=E);var m=Ha.call(arguments,3),p=w(l)&&!l,n=(p?c:d).defer(),s=n.promise,r;r=b.defer(function(){try{n.resolve(f.apply(null,m))}catch(b){n.reject(b),e(b)}finally{delete g[s.$$timeoutId]}p||a.$apply()},h,"$timeout");s.$$timeoutId=r;g[r]=n;return s}var g={};f.cancel=function(a){if(!a)return!1;if(!a.hasOwnProperty("$$timeoutId"))throw Vg("badprom");
if(!g.hasOwnProperty(a.$$timeoutId))return!1;a=a.$$timeoutId;var c=g[a],d=c.promise;d.$$state&&(d.$$state.pur=!0);c.reject("canceled");delete g[a];return b.defer.cancel(a)};return f}]}function ga(a){if(!A(a))return a;Ca&&(aa.setAttribute("href",a),a=aa.href);aa.setAttribute("href",a);a=aa.hostname;!Wg&&-1<a.indexOf(":")&&(a="["+a+"]");return{href:aa.href,protocol:aa.protocol?aa.protocol.replace(/:$/,""):"",host:aa.host,search:aa.search?aa.search.replace(/^\?/,""):"",hash:aa.hash?aa.hash.replace(/^#/,
""):"",hostname:a,port:aa.port,pathname:"/"===aa.pathname.charAt(0)?aa.pathname:"/"+aa.pathname}}function Jg(a){var b=[Od].concat(a.map(ga));return function(a){a=ga(a);return b.some(Bc.bind(null,a))}}function Bc(a,b){a=ga(a);b=ga(b);return a.protocol===b.protocol&&a.host===b.host}function gg(){this.$get=ia(C)}function Pd(a){function b(a){try{return decodeURIComponent(a)}catch(b){return a}}var d=a[0]||{},c={},e="";return function(){var a,g,k,h,l;try{a=d.cookie||""}catch(m){a=""}if(a!==e)for(e=a,a=
e.split("; "),c={},k=0;k<a.length;k++)g=a[k],h=g.indexOf("="),0<h&&(l=b(g.substring(0,h)),z(c[l])&&(c[l]=b(g.substring(h+1))));return c}}function kg(){this.$get=Pd}function dd(a){function b(d,c){if(D(d)){var e={};r(d,function(a,c){e[c]=b(c,a)});return e}return a.factory(d+"Filter",c)}this.register=b;this.$get=["$injector",function(a){return function(b){return a.get(b+"Filter")}}];b("currency",Qd);b("date",Rd);b("filter",Xg);b("json",Yg);b("limitTo",Zg);b("lowercase",$g);b("number",Sd);b("orderBy",
Td);b("uppercase",ah)}function Xg(){return function(a,b,d,c){if(!ya(a)){if(null==a)return a;throw F("filter")("notarray",a);}c=c||"$";var e;switch(Dc(b)){case "function":break;case "boolean":case "null":case "number":case "string":e=!0;case "object":b=bh(b,d,c,e);break;default:return a}return Array.prototype.filter.call(a,b)}}function bh(a,b,d,c){var e=D(a)&&d in a;!0===b?b=va:B(b)||(b=function(a,b){if(z(a))return!1;if(null===a||null===b)return a===b;if(D(b)||D(a)&&!bc(a))return!1;a=K(""+a);b=K(""+
b);return-1!==a.indexOf(b)});return function(f){return e&&!D(f)?Fa(f,a[d],b,d,!1):Fa(f,a,b,d,c)}}function Fa(a,b,d,c,e,f){var g=Dc(a),k=Dc(b);if("string"===k&&"!"===b.charAt(0))return!Fa(a,b.substring(1),d,c,e);if(H(a))return a.some(function(a){return Fa(a,b,d,c,e)});switch(g){case "object":var h;if(e){for(h in a)if(h.charAt&&"$"!==h.charAt(0)&&Fa(a[h],b,d,c,!0))return!0;return f?!1:Fa(a,b,d,c,!1)}if("object"===k){for(h in b)if(f=b[h],!B(f)&&!z(f)&&(g=h===c,!Fa(g?a:a[h],f,d,c,g,g)))return!1;return!0}return d(a,
b);case "function":return!1;default:return d(a,b)}}function Dc(a){return null===a?"null":typeof a}function Qd(a){var b=a.NUMBER_FORMATS;return function(a,c,e){z(c)&&(c=b.CURRENCY_SYM);z(e)&&(e=b.PATTERNS[1].maxFrac);var f=c?/\u00A4/g:/\s*\u00A4\s*/g;return null==a?a:Ud(a,b.PATTERNS[1],b.GROUP_SEP,b.DECIMAL_SEP,e).replace(f,c)}}function Sd(a){var b=a.NUMBER_FORMATS;return function(a,c){return null==a?a:Ud(a,b.PATTERNS[0],b.GROUP_SEP,b.DECIMAL_SEP,c)}}function ch(a){var b=0,d,c,e,f,g;-1<(c=a.indexOf(Vd))&&
(a=a.replace(Vd,""));0<(e=a.search(/e/i))?(0>c&&(c=e),c+=+a.slice(e+1),a=a.substring(0,e)):0>c&&(c=a.length);for(e=0;a.charAt(e)===Ec;e++);if(e===(g=a.length))d=[0],c=1;else{for(g--;a.charAt(g)===Ec;)g--;c-=e;d=[];for(f=0;e<=g;e++,f++)d[f]=+a.charAt(e)}c>Wd&&(d=d.splice(0,Wd-1),b=c-1,c=1);return{d:d,e:b,i:c}}function dh(a,b,d,c){var e=a.d,f=e.length-a.i;b=z(b)?Math.min(Math.max(d,f),c):+b;d=b+a.i;c=e[d];if(0<d){e.splice(Math.max(a.i,d));for(var g=d;g<e.length;g++)e[g]=0}else for(f=Math.max(0,f),a.i=
1,e.length=Math.max(1,d=b+1),e[0]=0,g=1;g<d;g++)e[g]=0;if(5<=c)if(0>d-1){for(c=0;c>d;c--)e.unshift(0),a.i++;e.unshift(1);a.i++}else e[d-1]++;for(;f<Math.max(0,b);f++)e.push(0);if(b=e.reduceRight(function(a,b,c,d){b+=a;d[c]=b%10;return Math.floor(b/10)},0))e.unshift(b),a.i++}function Ud(a,b,d,c,e){if(!A(a)&&!W(a)||isNaN(a))return"";var f=!isFinite(a),g=!1,k=Math.abs(a)+"",h="";if(f)h="\u221e";else{g=ch(k);dh(g,e,b.minFrac,b.maxFrac);h=g.d;k=g.i;e=g.e;f=[];for(g=h.reduce(function(a,b){return a&&!b},
!0);0>k;)h.unshift(0),k++;0<k?f=h.splice(k,h.length):(f=h,h=[0]);k=[];for(h.length>=b.lgSize&&k.unshift(h.splice(-b.lgSize,h.length).join(""));h.length>b.gSize;)k.unshift(h.splice(-b.gSize,h.length).join(""));h.length&&k.unshift(h.join(""));h=k.join(d);f.length&&(h+=c+f.join(""));e&&(h+="e+"+e)}return 0>a&&!g?b.negPre+h+b.negSuf:b.posPre+h+b.posSuf}function Ob(a,b,d,c){var e="";if(0>a||c&&0>=a)c?a=-a+1:(a=-a,e="-");for(a=""+a;a.length<b;)a=Ec+a;d&&(a=a.substr(a.length-b));return e+a}function ea(a,
b,d,c,e){d=d||0;return function(f){f=f["get"+a]();if(0<d||f>-d)f+=d;0===f&&-12===d&&(f=12);return Ob(f,b,c,e)}}function kb(a,b,d){return function(c,e){var f=c["get"+a](),g=ub((d?"STANDALONE":"")+(b?"SHORT":"")+a);return e[g][f]}}function Xd(a){var b=(new Date(a,0,1)).getDay();return new Date(a,0,(4>=b?5:12)-b)}function Yd(a){return function(b){var d=Xd(b.getFullYear());b=+new Date(b.getFullYear(),b.getMonth(),b.getDate()+(4-b.getDay()))-+d;b=1+Math.round(b/6048E5);return Ob(b,a)}}function Fc(a,b){return 0>=
a.getFullYear()?b.ERAS[0]:b.ERAS[1]}function Rd(a){function b(a){var b;if(b=a.match(d)){a=new Date(0);var f=0,g=0,k=b[8]?a.setUTCFullYear:a.setFullYear,h=b[8]?a.setUTCHours:a.setHours;b[9]&&(f=fa(b[9]+b[10]),g=fa(b[9]+b[11]));k.call(a,fa(b[1]),fa(b[2])-1,fa(b[3]));f=fa(b[4]||0)-f;g=fa(b[5]||0)-g;k=fa(b[6]||0);b=Math.round(1E3*parseFloat("0."+(b[7]||0)));h.call(a,f,g,k,b)}return a}var d=/^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/;return function(c,
d,f){var g="",k=[],h,l;d=d||"mediumDate";d=a.DATETIME_FORMATS[d]||d;A(c)&&(c=eh.test(c)?fa(c):b(c));W(c)&&(c=new Date(c));if(!ha(c)||!isFinite(c.getTime()))return c;for(;d;)(l=fh.exec(d))?(k=db(k,l,1),d=k.pop()):(k.push(d),d=null);var m=c.getTimezoneOffset();f&&(m=ec(f,m),c=fc(c,f,!0));r(k,function(b){h=gh[b];g+=h?h(c,a.DATETIME_FORMATS,m):"''"===b?"'":b.replace(/(^'|'$)/g,"").replace(/''/g,"'")});return g}}function Yg(){return function(a,b){z(b)&&(b=2);return eb(a,b)}}function Zg(){return function(a,
b,d){b=Infinity===Math.abs(Number(b))?Number(b):fa(b);if(X(b))return a;W(a)&&(a=a.toString());if(!ya(a))return a;d=!d||isNaN(d)?0:fa(d);d=0>d?Math.max(0,a.length+d):d;return 0<=b?Gc(a,d,d+b):0===d?Gc(a,b,a.length):Gc(a,Math.max(0,d+b),d)}}function Gc(a,b,d){return A(a)?a.slice(b,d):Ha.call(a,b,d)}function Td(a){function b(b){return b.map(function(b){var c=1,d=Ta;if(B(b))d=b;else if(A(b)){if("+"===b.charAt(0)||"-"===b.charAt(0))c="-"===b.charAt(0)?-1:1,b=b.substring(1);if(""!==b&&(d=a(b),d.constant))var e=
d(),d=function(a){return a[e]}}return{get:d,descending:c}})}function d(a){switch(typeof a){case "number":case "boolean":case "string":return!0;default:return!1}}function c(a,b){var c=0,d=a.type,h=b.type;if(d===h){var h=a.value,l=b.value;"string"===d?(h=h.toLowerCase(),l=l.toLowerCase()):"object"===d&&(D(h)&&(h=a.index),D(l)&&(l=b.index));h!==l&&(c=h<l?-1:1)}else c="undefined"===d?1:"undefined"===h?-1:"null"===d?1:"null"===h?-1:d<h?-1:1;return c}return function(a,f,g,k){if(null==a)return a;if(!ya(a))throw F("orderBy")("notarray",
a);H(f)||(f=[f]);0===f.length&&(f=["+"]);var h=b(f),l=g?-1:1,m=B(k)?k:c;a=Array.prototype.map.call(a,function(a,b){return{value:a,tieBreaker:{value:b,type:"number",index:b},predicateValues:h.map(function(c){var e=c.get(a);c=typeof e;if(null===e)c="null";else if("object"===c)a:{if(B(e.valueOf)&&(e=e.valueOf(),d(e)))break a;bc(e)&&(e=e.toString(),d(e))}return{value:e,type:c,index:b}})}});a.sort(function(a,b){for(var d=0,e=h.length;d<e;d++){var f=m(a.predicateValues[d],b.predicateValues[d]);if(f)return f*
h[d].descending*l}return(m(a.tieBreaker,b.tieBreaker)||c(a.tieBreaker,b.tieBreaker))*l});return a=a.map(function(a){return a.value})}}function Ra(a){B(a)&&(a={link:a});a.restrict=a.restrict||"AC";return ia(a)}function Pb(a,b,d,c,e){this.$$controls=[];this.$error={};this.$$success={};this.$pending=void 0;this.$name=e(b.name||b.ngForm||"")(d);this.$dirty=!1;this.$valid=this.$pristine=!0;this.$submitted=this.$invalid=!1;this.$$parentForm=lb;this.$$element=a;this.$$animate=c;Zd(this)}function Zd(a){a.$$classCache=
{};a.$$classCache[$d]=!(a.$$classCache[mb]=a.$$element.hasClass(mb))}function ae(a){function b(a,b,c){c&&!a.$$classCache[b]?(a.$$animate.addClass(a.$$element,b),a.$$classCache[b]=!0):!c&&a.$$classCache[b]&&(a.$$animate.removeClass(a.$$element,b),a.$$classCache[b]=!1)}function d(a,c,d){c=c?"-"+Vc(c,"-"):"";b(a,mb+c,!0===d);b(a,$d+c,!1===d)}var c=a.set,e=a.unset;a.clazz.prototype.$setValidity=function(a,g,k){z(g)?(this.$pending||(this.$pending={}),c(this.$pending,a,k)):(this.$pending&&e(this.$pending,
a,k),be(this.$pending)&&(this.$pending=void 0));Ga(g)?g?(e(this.$error,a,k),c(this.$$success,a,k)):(c(this.$error,a,k),e(this.$$success,a,k)):(e(this.$error,a,k),e(this.$$success,a,k));this.$pending?(b(this,"ng-pending",!0),this.$valid=this.$invalid=void 0,d(this,"",null)):(b(this,"ng-pending",!1),this.$valid=be(this.$error),this.$invalid=!this.$valid,d(this,"",this.$valid));g=this.$pending&&this.$pending[a]?void 0:this.$error[a]?!1:this.$$success[a]?!0:null;d(this,a,g);this.$$parentForm.$setValidity(a,
g,this)}}function be(a){if(a)for(var b in a)if(a.hasOwnProperty(b))return!1;return!0}function Hc(a){a.$formatters.push(function(b){return a.$isEmpty(b)?b:b.toString()})}function Sa(a,b,d,c,e,f){var g=K(b[0].type);if(!e.android){var k=!1;b.on("compositionstart",function(){k=!0});b.on("compositionupdate",function(a){if(z(a.data)||""===a.data)k=!1});b.on("compositionend",function(){k=!1;l()})}var h,l=function(a){h&&(f.defer.cancel(h),h=null);if(!k){var e=b.val();a=a&&a.type;"password"===g||d.ngTrim&&
"false"===d.ngTrim||(e=U(e));(c.$viewValue!==e||""===e&&c.$$hasNativeValidators)&&c.$setViewValue(e,a)}};if(e.hasEvent("input"))b.on("input",l);else{var m=function(a,b,c){h||(h=f.defer(function(){h=null;b&&b.value===c||l(a)}))};b.on("keydown",function(a){var b=a.keyCode;91===b||15<b&&19>b||37<=b&&40>=b||m(a,this,this.value)});if(e.hasEvent("paste"))b.on("paste cut drop",m)}b.on("change",l);if(ce[g]&&c.$$hasNativeValidators&&g===d.type)b.on("keydown wheel mousedown",function(a){if(!h){var b=this.validity,
c=b.badInput,d=b.typeMismatch;h=f.defer(function(){h=null;b.badInput===c&&b.typeMismatch===d||l(a)})}});c.$render=function(){var a=c.$isEmpty(c.$viewValue)?"":c.$viewValue;b.val()!==a&&b.val(a)}}function Qb(a,b){return function(d,c){var e,f;if(ha(d))return d;if(A(d)){'"'===d.charAt(0)&&'"'===d.charAt(d.length-1)&&(d=d.substring(1,d.length-1));if(hh.test(d))return new Date(d);a.lastIndex=0;if(e=a.exec(d))return e.shift(),f=c?{yyyy:c.getFullYear(),MM:c.getMonth()+1,dd:c.getDate(),HH:c.getHours(),mm:c.getMinutes(),
ss:c.getSeconds(),sss:c.getMilliseconds()/1E3}:{yyyy:1970,MM:1,dd:1,HH:0,mm:0,ss:0,sss:0},r(e,function(a,c){c<b.length&&(f[b[c]]=+a)}),e=new Date(f.yyyy,f.MM-1,f.dd,f.HH,f.mm,f.ss||0,1E3*f.sss||0),100>f.yyyy&&e.setFullYear(f.yyyy),e}return NaN}}function nb(a,b,d,c){return function(e,f,g,k,h,l,m,p){function n(a){return a&&!(a.getTime&&a.getTime()!==a.getTime())}function s(a){return w(a)&&!ha(a)?r(a)||void 0:a}function r(a,b){var c=k.$options.getOption("timezone");v&&v!==c&&(b=Sc(b,ec(v)));var e=d(a,
b);!isNaN(e)&&c&&(e=fc(e,c));return e}Ic(e,f,g,k,a);Sa(e,f,g,k,h,l);var t="time"===a||"datetimelocal"===a,q,v;k.$parsers.push(function(c){if(k.$isEmpty(c))return null;if(b.test(c))return r(c,q);k.$$parserName=a});k.$formatters.push(function(a){if(a&&!ha(a))throw ob("datefmt",a);if(n(a)){q=a;var b=k.$options.getOption("timezone");b&&(v=b,q=fc(q,b,!0));var d=c;t&&A(k.$options.getOption("timeSecondsFormat"))&&(d=c.replace("ss.sss",k.$options.getOption("timeSecondsFormat")).replace(/:$/,""));a=m("date")(a,
d,b);t&&k.$options.getOption("timeStripZeroSeconds")&&(a=a.replace(/(?::00)?(?:\.000)?$/,""));return a}v=q=null;return""});if(w(g.min)||g.ngMin){var x=g.min||p(g.ngMin)(e),B=s(x);k.$validators.min=function(a){return!n(a)||z(B)||d(a)>=B};g.$observe("min",function(a){a!==x&&(B=s(a),x=a,k.$validate())})}if(w(g.max)||g.ngMax){var y=g.max||p(g.ngMax)(e),J=s(y);k.$validators.max=function(a){return!n(a)||z(J)||d(a)<=J};g.$observe("max",function(a){a!==y&&(J=s(a),y=a,k.$validate())})}}}function Ic(a,b,d,
c,e){(c.$$hasNativeValidators=D(b[0].validity))&&c.$parsers.push(function(a){var d=b.prop("validity")||{};if(d.badInput||d.typeMismatch)c.$$parserName=e;else return a})}function de(a){a.$parsers.push(function(b){if(a.$isEmpty(b))return null;if(ih.test(b))return parseFloat(b);a.$$parserName="number"});a.$formatters.push(function(b){if(!a.$isEmpty(b)){if(!W(b))throw ob("numfmt",b);b=b.toString()}return b})}function na(a){w(a)&&!W(a)&&(a=parseFloat(a));return X(a)?void 0:a}function Jc(a){var b=a.toString(),
d=b.indexOf(".");return-1===d?-1<a&&1>a&&(a=/e-(\d+)$/.exec(b))?Number(a[1]):0:b.length-d-1}function ee(a,b,d){a=Number(a);var c=(a|0)!==a,e=(b|0)!==b,f=(d|0)!==d;if(c||e||f){var g=c?Jc(a):0,k=e?Jc(b):0,h=f?Jc(d):0,g=Math.max(g,k,h),g=Math.pow(10,g);a*=g;b*=g;d*=g;c&&(a=Math.round(a));e&&(b=Math.round(b));f&&(d=Math.round(d))}return 0===(a-b)%d}function fe(a,b,d,c,e){if(w(c)){a=a(c);if(!a.constant)throw ob("constexpr",d,c);return a(b)}return e}function Kc(a,b){function d(a,b){if(!a||!a.length)return[];
if(!b||!b.length)return a;var c=[],d=0;a:for(;d<a.length;d++){for(var e=a[d],m=0;m<b.length;m++)if(e===b[m])continue a;c.push(e)}return c}function c(a){if(!a)return a;var b=a;H(a)?b=a.map(c).join(" "):D(a)?b=Object.keys(a).filter(function(b){return a[b]}).join(" "):A(a)||(b=a+"");return b}a="ngClass"+a;var e;return["$parse",function(f){return{restrict:"AC",link:function(g,k,h){function l(a,b){var c=[];r(a,function(a){if(0<b||p[a])p[a]=(p[a]||0)+b,p[a]===+(0<b)&&c.push(a)});return c.join(" ")}function m(a){if(a===
b){var c=s,c=l(c&&c.split(" "),1);h.$addClass(c)}else c=s,c=l(c&&c.split(" "),-1),h.$removeClass(c);n=a}var p=k.data("$classCounts"),n=!0,s;p||(p=T(),k.data("$classCounts",p));"ngClass"!==a&&(e||(e=f("$index",function(a){return a&1})),g.$watch(e,m));g.$watch(f(h[a],c),function(a){if(n===b){var c=s&&s.split(" "),e=a&&a.split(" "),f=d(c,e),c=d(e,c),f=l(f,-1),c=l(c,1);h.$addClass(c);h.$removeClass(f)}s=a})}}}]}function qd(a,b,d,c,e,f){return{restrict:"A",compile:function(g,k){var h=a(k[c]);return function(a,
c){c.on(e,function(c){var e=function(){h(a,{$event:c})};if(b.$$phase)if(f)a.$evalAsync(e);else try{e()}catch(g){d(g)}else a.$apply(e)})}}}}function Rb(a,b,d,c,e,f,g,k,h){this.$modelValue=this.$viewValue=Number.NaN;this.$$rawModelValue=void 0;this.$validators={};this.$asyncValidators={};this.$parsers=[];this.$formatters=[];this.$viewChangeListeners=[];this.$untouched=!0;this.$touched=!1;this.$pristine=!0;this.$dirty=!1;this.$valid=!0;this.$invalid=!1;this.$error={};this.$$success={};this.$pending=
void 0;this.$name=h(d.name||"",!1)(a);this.$$parentForm=lb;this.$options=Sb;this.$$updateEvents="";this.$$updateEventHandler=this.$$updateEventHandler.bind(this);this.$$parsedNgModel=e(d.ngModel);this.$$parsedNgModelAssign=this.$$parsedNgModel.assign;this.$$ngModelGet=this.$$parsedNgModel;this.$$ngModelSet=this.$$parsedNgModelAssign;this.$$pendingDebounce=null;this.$$parserValid=void 0;this.$$parserName="parse";this.$$currentValidationRunId=0;this.$$scope=a;this.$$rootScope=a.$root;this.$$attr=d;
this.$$element=c;this.$$animate=f;this.$$timeout=g;this.$$parse=e;this.$$q=k;this.$$exceptionHandler=b;Zd(this);jh(this)}function jh(a){a.$$scope.$watch(function(b){b=a.$$ngModelGet(b);b===a.$modelValue||a.$modelValue!==a.$modelValue&&b!==b||a.$$setModelValue(b);return b})}function Lc(a){this.$$options=a}function ge(a,b){r(b,function(b,c){w(a[c])||(a[c]=b)})}function Oa(a,b){a.prop("selected",b);a.attr("selected",b)}function he(a,b,d){if(a){A(a)&&(a=new RegExp("^"+a+"$"));if(!a.test)throw F("ngPattern")("noregexp",
b,a,za(d));return a}}function Tb(a){a=fa(a);return X(a)?-1:a}var Wb={objectMaxDepth:5,urlErrorParamsEnabled:!0},ie=/^\/(.+)\/([a-z]*)$/,ta=Object.prototype.hasOwnProperty,K=function(a){return A(a)?a.toLowerCase():a},ub=function(a){return A(a)?a.toUpperCase():a},Ca,x,rb,Ha=[].slice,Fg=[].splice,kh=[].push,la=Object.prototype.toString,Pc=Object.getPrototypeOf,pa=F("ng"),ca=C.angular||(C.angular={}),kc,pb=0;Ca=C.document.documentMode;var X=Number.isNaN||function(a){return a!==a};E.$inject=[];Ta.$inject=
[];var ve=/^\[object (?:Uint8|Uint8Clamped|Uint16|Uint32|Int8|Int16|Int32|Float32|Float64)Array]$/,U=function(a){return A(a)?a.trim():a},Md=function(a){return a.replace(/([-()[\]{}+?*.$^|,:#<!\\])/g,"\\$1").replace(/\x08/g,"\\x08")},Aa=function(){if(!w(Aa.rules)){var a=C.document.querySelector("[ng-csp]")||C.document.querySelector("[data-ng-csp]");if(a){var b=a.getAttribute("ng-csp")||a.getAttribute("data-ng-csp");Aa.rules={noUnsafeEval:!b||-1!==b.indexOf("no-unsafe-eval"),noInlineStyle:!b||-1!==
b.indexOf("no-inline-style")}}else{a=Aa;try{new Function(""),b=!1}catch(d){b=!0}a.rules={noUnsafeEval:b,noInlineStyle:!1}}}return Aa.rules},qb=function(){if(w(qb.name_))return qb.name_;var a,b,d=Qa.length,c,e;for(b=0;b<d;++b)if(c=Qa[b],a=C.document.querySelector("["+c.replace(":","\\:")+"jq]")){e=a.getAttribute(c+"jq");break}return qb.name_=e},xe=/:/g,Qa=["ng-","data-ng-","ng:","x-ng-"],Be=function(a){var b=a.currentScript;if(!b)return!0;if(!(b instanceof C.HTMLScriptElement||b instanceof C.SVGScriptElement))return!1;
b=b.attributes;return[b.getNamedItem("src"),b.getNamedItem("href"),b.getNamedItem("xlink:href")].every(function(b){if(!b)return!0;if(!b.value)return!1;var c=a.createElement("a");c.href=b.value;if(a.location.origin===c.origin)return!0;switch(c.protocol){case "http:":case "https:":case "ftp:":case "blob:":case "file:":case "data:":return!0;default:return!1}})}(C.document),Ee=/[A-Z]/g,Wc=!1,Pa=3,Ke={full:"1.7.9",major:1,minor:7,dot:9,codeName:"pollution-eradication"};Y.expando="ng339";var Ka=Y.cache=
{},pg=1;Y._data=function(a){return this.cache[a[this.expando]]||{}};var lg=/-([a-z])/g,lh=/^-ms-/,Ab={mouseleave:"mouseout",mouseenter:"mouseover"},nc=F("jqLite"),og=/^<([\w-]+)\s*\/?>(?:<\/\1>|)$/,mc=/<|&#?\w+;/,mg=/<([\w:-]+)/,ng=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi,oa={option:[1,'<select multiple="multiple">',"</select>"],thead:[1,"<table>","</table>"],col:[2,"<table><colgroup>","</colgroup></table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>",
"</tr></tbody></table>"],_default:[0,"",""]};oa.optgroup=oa.option;oa.tbody=oa.tfoot=oa.colgroup=oa.caption=oa.thead;oa.th=oa.td;var ug=C.Node.prototype.contains||function(a){return!!(this.compareDocumentPosition(a)&16)},Wa=Y.prototype={ready:fd,toString:function(){var a=[];r(this,function(b){a.push(""+b)});return"["+a.join(", ")+"]"},eq:function(a){return 0<=a?x(this[a]):x(this[this.length+a])},length:0,push:kh,sort:[].sort,splice:[].splice},Gb={};r("multiple selected checked disabled readOnly required open".split(" "),
function(a){Gb[K(a)]=a});var md={};r("input select option textarea button form details".split(" "),function(a){md[a]=!0});var td={ngMinlength:"minlength",ngMaxlength:"maxlength",ngMin:"min",ngMax:"max",ngPattern:"pattern",ngStep:"step"};r({data:rc,removeData:qc,hasData:function(a){for(var b in Ka[a.ng339])return!0;return!1},cleanData:function(a){for(var b=0,d=a.length;b<d;b++)qc(a[b]),id(a[b])}},function(a,b){Y[b]=a});r({data:rc,inheritedData:Eb,scope:function(a){return x.data(a,"$scope")||Eb(a.parentNode||
a,["$isolateScope","$scope"])},isolateScope:function(a){return x.data(a,"$isolateScope")||x.data(a,"$isolateScopeNoTemplate")},controller:jd,injector:function(a){return Eb(a,"$injector")},removeAttr:function(a,b){a.removeAttribute(b)},hasClass:Bb,css:function(a,b,d){b=xb(b.replace(lh,"ms-"));if(w(d))a.style[b]=d;else return a.style[b]},attr:function(a,b,d){var c=a.nodeType;if(c!==Pa&&2!==c&&8!==c&&a.getAttribute){var c=K(b),e=Gb[c];if(w(d))null===d||!1===d&&e?a.removeAttribute(b):a.setAttribute(b,
e?c:d);else return a=a.getAttribute(b),e&&null!==a&&(a=c),null===a?void 0:a}},prop:function(a,b,d){if(w(d))a[b]=d;else return a[b]},text:function(){function a(a,d){if(z(d)){var c=a.nodeType;return 1===c||c===Pa?a.textContent:""}a.textContent=d}a.$dv="";return a}(),val:function(a,b){if(z(b)){if(a.multiple&&"select"===ua(a)){var d=[];r(a.options,function(a){a.selected&&d.push(a.value||a.text)});return d}return a.value}a.value=b},html:function(a,b){if(z(b))return a.innerHTML;yb(a,!0);a.innerHTML=b},
empty:kd},function(a,b){Y.prototype[b]=function(b,c){var e,f,g=this.length;if(a!==kd&&z(2===a.length&&a!==Bb&&a!==jd?b:c)){if(D(b)){for(e=0;e<g;e++)if(a===rc)a(this[e],b);else for(f in b)a(this[e],f,b[f]);return this}e=a.$dv;g=z(e)?Math.min(g,1):g;for(f=0;f<g;f++){var k=a(this[f],b,c);e=e?e+k:k}return e}for(e=0;e<g;e++)a(this[e],b,c);return this}});r({removeData:qc,on:function(a,b,d,c){if(w(c))throw nc("onargs");if(lc(a)){c=zb(a,!0);var e=c.events,f=c.handle;f||(f=c.handle=rg(a,e));c=0<=b.indexOf(" ")?
b.split(" "):[b];for(var g=c.length,k=function(b,c,g){var k=e[b];k||(k=e[b]=[],k.specialHandlerWrapper=c,"$destroy"===b||g||a.addEventListener(b,f));k.push(d)};g--;)b=c[g],Ab[b]?(k(Ab[b],tg),k(b,void 0,!0)):k(b)}},off:id,one:function(a,b,d){a=x(a);a.on(b,function e(){a.off(b,d);a.off(b,e)});a.on(b,d)},replaceWith:function(a,b){var d,c=a.parentNode;yb(a);r(new Y(b),function(b){d?c.insertBefore(b,d.nextSibling):c.replaceChild(b,a);d=b})},children:function(a){var b=[];r(a.childNodes,function(a){1===
a.nodeType&&b.push(a)});return b},contents:function(a){return a.contentDocument||a.childNodes||[]},append:function(a,b){var d=a.nodeType;if(1===d||11===d){b=new Y(b);for(var d=0,c=b.length;d<c;d++)a.appendChild(b[d])}},prepend:function(a,b){if(1===a.nodeType){var d=a.firstChild;r(new Y(b),function(b){a.insertBefore(b,d)})}},wrap:function(a,b){var d=x(b).eq(0).clone()[0],c=a.parentNode;c&&c.replaceChild(d,a);d.appendChild(a)},remove:Fb,detach:function(a){Fb(a,!0)},after:function(a,b){var d=a,c=a.parentNode;
if(c){b=new Y(b);for(var e=0,f=b.length;e<f;e++){var g=b[e];c.insertBefore(g,d.nextSibling);d=g}}},addClass:Db,removeClass:Cb,toggleClass:function(a,b,d){b&&r(b.split(" "),function(b){var e=d;z(e)&&(e=!Bb(a,b));(e?Db:Cb)(a,b)})},parent:function(a){return(a=a.parentNode)&&11!==a.nodeType?a:null},next:function(a){return a.nextElementSibling},find:function(a,b){return a.getElementsByTagName?a.getElementsByTagName(b):[]},clone:pc,triggerHandler:function(a,b,d){var c,e,f=b.type||b,g=zb(a);if(g=(g=g&&g.events)&&
g[f])c={preventDefault:function(){this.defaultPrevented=!0},isDefaultPrevented:function(){return!0===this.defaultPrevented},stopImmediatePropagation:function(){this.immediatePropagationStopped=!0},isImmediatePropagationStopped:function(){return!0===this.immediatePropagationStopped},stopPropagation:E,type:f,target:a},b.type&&(c=S(c,b)),b=ja(g),e=d?[c].concat(d):[c],r(b,function(b){c.isImmediatePropagationStopped()||b.apply(a,e)})}},function(a,b){Y.prototype[b]=function(b,c,e){for(var f,g=0,k=this.length;g<
k;g++)z(f)?(f=a(this[g],b,c,e),w(f)&&(f=x(f))):oc(f,a(this[g],b,c,e));return w(f)?f:this}});Y.prototype.bind=Y.prototype.on;Y.prototype.unbind=Y.prototype.off;var mh=Object.create(null);nd.prototype={_idx:function(a){a!==this._lastKey&&(this._lastKey=a,this._lastIndex=this._keys.indexOf(a));return this._lastIndex},_transformKey:function(a){return X(a)?mh:a},get:function(a){a=this._transformKey(a);a=this._idx(a);if(-1!==a)return this._values[a]},has:function(a){a=this._transformKey(a);return-1!==this._idx(a)},
set:function(a,b){a=this._transformKey(a);var d=this._idx(a);-1===d&&(d=this._lastIndex=this._keys.length);this._keys[d]=a;this._values[d]=b},delete:function(a){a=this._transformKey(a);a=this._idx(a);if(-1===a)return!1;this._keys.splice(a,1);this._values.splice(a,1);this._lastKey=NaN;this._lastIndex=-1;return!0}};var Hb=nd,jg=[function(){this.$get=[function(){return Hb}]}],wg=/^([^(]+?)=>/,xg=/^[^(]*\(\s*([^)]*)\)/m,nh=/,/,oh=/^\s*(_?)(\S+?)\1\s*$/,vg=/((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg,Ba=F("$injector");
fb.$$annotate=function(a,b,d){var c;if("function"===typeof a){if(!(c=a.$inject)){c=[];if(a.length){if(b)throw A(d)&&d||(d=a.name||yg(a)),Ba("strictdi",d);b=od(a);r(b[1].split(nh),function(a){a.replace(oh,function(a,b,d){c.push(d)})})}a.$inject=c}}else H(a)?(b=a.length-1,sb(a[b],"fn"),c=a.slice(0,b)):sb(a,"fn",!0);return c};var je=F("$animate"),zf=function(){this.$get=E},Af=function(){var a=new Hb,b=[];this.$get=["$$AnimateRunner","$rootScope",function(d,c){function e(a,b,c){var d=!1;b&&(b=A(b)?b.split(" "):
H(b)?b:[],r(b,function(b){b&&(d=!0,a[b]=c)}));return d}function f(){r(b,function(b){var c=a.get(b);if(c){var d=zg(b.attr("class")),e="",f="";r(c,function(a,b){a!==!!d[b]&&(a?e+=(e.length?" ":"")+b:f+=(f.length?" ":"")+b)});r(b,function(a){e&&Db(a,e);f&&Cb(a,f)});a.delete(b)}});b.length=0}return{enabled:E,on:E,off:E,pin:E,push:function(g,k,h,l){l&&l();h=h||{};h.from&&g.css(h.from);h.to&&g.css(h.to);if(h.addClass||h.removeClass)if(k=h.addClass,l=h.removeClass,h=a.get(g)||{},k=e(h,k,!0),l=e(h,l,!1),
k||l)a.set(g,h),b.push(g),1===b.length&&c.$$postDigest(f);g=new d;g.complete();return g}}}]},xf=["$provide",function(a){var b=this,d=null,c=null;this.$$registeredAnimations=Object.create(null);this.register=function(c,d){if(c&&"."!==c.charAt(0))throw je("notcsel",c);var g=c+"-animation";b.$$registeredAnimations[c.substr(1)]=g;a.factory(g,d)};this.customFilter=function(a){1===arguments.length&&(c=B(a)?a:null);return c};this.classNameFilter=function(a){if(1===arguments.length&&(d=a instanceof RegExp?
a:null)&&/[(\s|\/)]ng-animate[(\s|\/)]/.test(d.toString()))throw d=null,je("nongcls","ng-animate");return d};this.$get=["$$animateQueue",function(a){function b(a,c,d){if(d){var e;a:{for(e=0;e<d.length;e++){var f=d[e];if(1===f.nodeType){e=f;break a}}e=void 0}!e||e.parentNode||e.previousElementSibling||(d=null)}d?d.after(a):c.prepend(a)}return{on:a.on,off:a.off,pin:a.pin,enabled:a.enabled,cancel:function(a){a.cancel&&a.cancel()},enter:function(c,d,h,l){d=d&&x(d);h=h&&x(h);d=d||h.parent();b(c,d,h);return a.push(c,
"enter",ra(l))},move:function(c,d,h,l){d=d&&x(d);h=h&&x(h);d=d||h.parent();b(c,d,h);return a.push(c,"move",ra(l))},leave:function(b,c){return a.push(b,"leave",ra(c),function(){b.remove()})},addClass:function(b,c,d){d=ra(d);d.addClass=hb(d.addclass,c);return a.push(b,"addClass",d)},removeClass:function(b,c,d){d=ra(d);d.removeClass=hb(d.removeClass,c);return a.push(b,"removeClass",d)},setClass:function(b,c,d,f){f=ra(f);f.addClass=hb(f.addClass,c);f.removeClass=hb(f.removeClass,d);return a.push(b,"setClass",
f)},animate:function(b,c,d,f,m){m=ra(m);m.from=m.from?S(m.from,c):c;m.to=m.to?S(m.to,d):d;m.tempClasses=hb(m.tempClasses,f||"ng-inline-animate");return a.push(b,"animate",m)}}}]}],Cf=function(){this.$get=["$$rAF",function(a){function b(b){d.push(b);1<d.length||a(function(){for(var a=0;a<d.length;a++)d[a]();d=[]})}var d=[];return function(){var a=!1;b(function(){a=!0});return function(d){a?d():b(d)}}}]},Bf=function(){this.$get=["$q","$sniffer","$$animateAsyncRun","$$isDocumentHidden","$timeout",function(a,
b,d,c,e){function f(a){this.setHost(a);var b=d();this._doneCallbacks=[];this._tick=function(a){c()?e(a,0,!1):b(a)};this._state=0}f.chain=function(a,b){function c(){if(d===a.length)b(!0);else a[d](function(a){!1===a?b(!1):(d++,c())})}var d=0;c()};f.all=function(a,b){function c(f){e=e&&f;++d===a.length&&b(e)}var d=0,e=!0;r(a,function(a){a.done(c)})};f.prototype={setHost:function(a){this.host=a||{}},done:function(a){2===this._state?a():this._doneCallbacks.push(a)},progress:E,getPromise:function(){if(!this.promise){var b=
this;this.promise=a(function(a,c){b.done(function(b){!1===b?c():a()})})}return this.promise},then:function(a,b){return this.getPromise().then(a,b)},"catch":function(a){return this.getPromise()["catch"](a)},"finally":function(a){return this.getPromise()["finally"](a)},pause:function(){this.host.pause&&this.host.pause()},resume:function(){this.host.resume&&this.host.resume()},end:function(){this.host.end&&this.host.end();this._resolve(!0)},cancel:function(){this.host.cancel&&this.host.cancel();this._resolve(!1)},
complete:function(a){var b=this;0===b._state&&(b._state=1,b._tick(function(){b._resolve(a)}))},_resolve:function(a){2!==this._state&&(r(this._doneCallbacks,function(b){b(a)}),this._doneCallbacks.length=0,this._state=2)}};return f}]},yf=function(){this.$get=["$$rAF","$q","$$AnimateRunner",function(a,b,d){return function(b,e){function f(){a(function(){g.addClass&&(b.addClass(g.addClass),g.addClass=null);g.removeClass&&(b.removeClass(g.removeClass),g.removeClass=null);g.to&&(b.css(g.to),g.to=null);k||
h.complete();k=!0});return h}var g=e||{};g.$$prepared||(g=Ia(g));g.cleanupStyles&&(g.from=g.to=null);g.from&&(b.css(g.from),g.from=null);var k,h=new d;return{start:f,end:f}}}]},$=F("$compile"),tc=new function(){};Xc.$inject=["$provide","$$sanitizeUriProvider"];Jb.prototype.isFirstChange=function(){return this.previousValue===tc};var pd=/^((?:x|data)[:\-_])/i,Eg=/[:\-_]+(.)/g,vd=F("$controller"),ud=/^(\S+)(\s+as\s+([\w$]+))?$/,Jf=function(){this.$get=["$document",function(a){return function(b){b?!b.nodeType&&
b instanceof x&&(b=b[0]):b=a[0].body;return b.offsetWidth+1}}]},wd="application/json",wc={"Content-Type":wd+";charset=utf-8"},Hg=/^\[|^\{(?!\{)/,Ig={"[":/]$/,"{":/}$/},Gg=/^\)]\}',?\n/,Kb=F("$http"),Ma=ca.$interpolateMinErr=F("$interpolate");Ma.throwNoconcat=function(a){throw Ma("noconcat",a);};Ma.interr=function(a,b){return Ma("interr",a,b.toString())};var Lg=F("$interval"),Sf=function(){this.$get=function(){function a(a){var b=function(a){b.data=a;b.called=!0};b.id=a;return b}var b=ca.callbacks,
d={};return{createCallback:function(c){c="_"+(b.$$counter++).toString(36);var e="angular.callbacks."+c,f=a(c);d[e]=b[c]=f;return e},wasCalled:function(a){return d[a].called},getResponse:function(a){return d[a].data},removeCallback:function(a){delete b[d[a].id];delete d[a]}}}},ph=/^([^?#]*)(\?([^#]*))?(#(.*))?$/,Mg={http:80,https:443,ftp:21},jb=F("$location"),Ng=/^\s*[\\/]{2,}/,qh={$$absUrl:"",$$html5:!1,$$replace:!1,$$compose:function(){for(var a=this.$$path,b=this.$$hash,d=ye(this.$$search),b=b?
"#"+hc(b):"",a=a.split("/"),c=a.length;c--;)a[c]=hc(a[c].replace(/%2F/g,"/"));this.$$url=a.join("/")+(d?"?"+d:"")+b;this.$$absUrl=this.$$normalizeUrl(this.$$url);this.$$urlUpdatedByLocation=!0},absUrl:Lb("$$absUrl"),url:function(a){if(z(a))return this.$$url;var b=ph.exec(a);(b[1]||""===a)&&this.path(decodeURIComponent(b[1]));(b[2]||b[1]||""===a)&&this.search(b[3]||"");this.hash(b[5]||"");return this},protocol:Lb("$$protocol"),host:Lb("$$host"),port:Lb("$$port"),path:Dd("$$path",function(a){a=null!==
a?a.toString():"";return"/"===a.charAt(0)?a:"/"+a}),search:function(a,b){switch(arguments.length){case 0:return this.$$search;case 1:if(A(a)||W(a))a=a.toString(),this.$$search=gc(a);else if(D(a))a=Ia(a,{}),r(a,function(b,c){null==b&&delete a[c]}),this.$$search=a;else throw jb("isrcharg");break;default:z(b)||null===b?delete this.$$search[a]:this.$$search[a]=b}this.$$compose();return this},hash:Dd("$$hash",function(a){return null!==a?a.toString():""}),replace:function(){this.$$replace=!0;return this}};
r([Cd,zc,yc],function(a){a.prototype=Object.create(qh);a.prototype.state=function(b){if(!arguments.length)return this.$$state;if(a!==yc||!this.$$html5)throw jb("nostate");this.$$state=z(b)?null:b;this.$$urlUpdatedByLocation=!0;return this}});var Ya=F("$parse"),Rg={}.constructor.prototype.valueOf,Ub=T();r("+ - * / % === !== == != < > <= >= && || ! = |".split(" "),function(a){Ub[a]=!0});var rh={n:"\n",f:"\f",r:"\r",t:"\t",v:"\v","'":"'",'"':'"'},Nb=function(a){this.options=a};Nb.prototype={constructor:Nb,
lex:function(a){this.text=a;this.index=0;for(this.tokens=[];this.index<this.text.length;)if(a=this.text.charAt(this.index),'"'===a||"'"===a)this.readString(a);else if(this.isNumber(a)||"."===a&&this.isNumber(this.peek()))this.readNumber();else if(this.isIdentifierStart(this.peekMultichar()))this.readIdent();else if(this.is(a,"(){}[].,;:?"))this.tokens.push({index:this.index,text:a}),this.index++;else if(this.isWhitespace(a))this.index++;else{var b=a+this.peek(),d=b+this.peek(2),c=Ub[b],e=Ub[d];Ub[a]||
c||e?(a=e?d:c?b:a,this.tokens.push({index:this.index,text:a,operator:!0}),this.index+=a.length):this.throwError("Unexpected next character ",this.index,this.index+1)}return this.tokens},is:function(a,b){return-1!==b.indexOf(a)},peek:function(a){a=a||1;return this.index+a<this.text.length?this.text.charAt(this.index+a):!1},isNumber:function(a){return"0"<=a&&"9">=a&&"string"===typeof a},isWhitespace:function(a){return" "===a||"\r"===a||"\t"===a||"\n"===a||"\v"===a||"\u00a0"===a},isIdentifierStart:function(a){return this.options.isIdentifierStart?
this.options.isIdentifierStart(a,this.codePointAt(a)):this.isValidIdentifierStart(a)},isValidIdentifierStart:function(a){return"a"<=a&&"z">=a||"A"<=a&&"Z">=a||"_"===a||"$"===a},isIdentifierContinue:function(a){return this.options.isIdentifierContinue?this.options.isIdentifierContinue(a,this.codePointAt(a)):this.isValidIdentifierContinue(a)},isValidIdentifierContinue:function(a,b){return this.isValidIdentifierStart(a,b)||this.isNumber(a)},codePointAt:function(a){return 1===a.length?a.charCodeAt(0):
(a.charCodeAt(0)<<10)+a.charCodeAt(1)-56613888},peekMultichar:function(){var a=this.text.charAt(this.index),b=this.peek();if(!b)return a;var d=a.charCodeAt(0),c=b.charCodeAt(0);return 55296<=d&&56319>=d&&56320<=c&&57343>=c?a+b:a},isExpOperator:function(a){return"-"===a||"+"===a||this.isNumber(a)},throwError:function(a,b,d){d=d||this.index;b=w(b)?"s "+b+"-"+this.index+" ["+this.text.substring(b,d)+"]":" "+d;throw Ya("lexerr",a,b,this.text);},readNumber:function(){for(var a="",b=this.index;this.index<
this.text.length;){var d=K(this.text.charAt(this.index));if("."===d||this.isNumber(d))a+=d;else{var c=this.peek();if("e"===d&&this.isExpOperator(c))a+=d;else if(this.isExpOperator(d)&&c&&this.isNumber(c)&&"e"===a.charAt(a.length-1))a+=d;else if(!this.isExpOperator(d)||c&&this.isNumber(c)||"e"!==a.charAt(a.length-1))break;else this.throwError("Invalid exponent")}this.index++}this.tokens.push({index:b,text:a,constant:!0,value:Number(a)})},readIdent:function(){var a=this.index;for(this.index+=this.peekMultichar().length;this.index<
this.text.length;){var b=this.peekMultichar();if(!this.isIdentifierContinue(b))break;this.index+=b.length}this.tokens.push({index:a,text:this.text.slice(a,this.index),identifier:!0})},readString:function(a){var b=this.index;this.index++;for(var d="",c=a,e=!1;this.index<this.text.length;){var f=this.text.charAt(this.index),c=c+f;if(e)"u"===f?(e=this.text.substring(this.index+1,this.index+5),e.match(/[\da-f]{4}/i)||this.throwError("Invalid unicode escape [\\u"+e+"]"),this.index+=4,d+=String.fromCharCode(parseInt(e,
16))):d+=rh[f]||f,e=!1;else if("\\"===f)e=!0;else{if(f===a){this.index++;this.tokens.push({index:b,text:c,constant:!0,value:d});return}d+=f}this.index++}this.throwError("Unterminated quote",b)}};var q=function(a,b){this.lexer=a;this.options=b};q.Program="Program";q.ExpressionStatement="ExpressionStatement";q.AssignmentExpression="AssignmentExpression";q.ConditionalExpression="ConditionalExpression";q.LogicalExpression="LogicalExpression";q.BinaryExpression="BinaryExpression";q.UnaryExpression="UnaryExpression";
q.CallExpression="CallExpression";q.MemberExpression="MemberExpression";q.Identifier="Identifier";q.Literal="Literal";q.ArrayExpression="ArrayExpression";q.Property="Property";q.ObjectExpression="ObjectExpression";q.ThisExpression="ThisExpression";q.LocalsExpression="LocalsExpression";q.NGValueParameter="NGValueParameter";q.prototype={ast:function(a){this.text=a;this.tokens=this.lexer.lex(a);a=this.program();0!==this.tokens.length&&this.throwError("is an unexpected token",this.tokens[0]);return a},
program:function(){for(var a=[];;)if(0<this.tokens.length&&!this.peek("}",")",";","]")&&a.push(this.expressionStatement()),!this.expect(";"))return{type:q.Program,body:a}},expressionStatement:function(){return{type:q.ExpressionStatement,expression:this.filterChain()}},filterChain:function(){for(var a=this.expression();this.expect("|");)a=this.filter(a);return a},expression:function(){return this.assignment()},assignment:function(){var a=this.ternary();if(this.expect("=")){if(!Hd(a))throw Ya("lval");
a={type:q.AssignmentExpression,left:a,right:this.assignment(),operator:"="}}return a},ternary:function(){var a=this.logicalOR(),b,d;return this.expect("?")&&(b=this.expression(),this.consume(":"))?(d=this.expression(),{type:q.ConditionalExpression,test:a,alternate:b,consequent:d}):a},logicalOR:function(){for(var a=this.logicalAND();this.expect("||");)a={type:q.LogicalExpression,operator:"||",left:a,right:this.logicalAND()};return a},logicalAND:function(){for(var a=this.equality();this.expect("&&");)a=
{type:q.LogicalExpression,operator:"&&",left:a,right:this.equality()};return a},equality:function(){for(var a=this.relational(),b;b=this.expect("==","!=","===","!==");)a={type:q.BinaryExpression,operator:b.text,left:a,right:this.relational()};return a},relational:function(){for(var a=this.additive(),b;b=this.expect("<",">","<=",">=");)a={type:q.BinaryExpression,operator:b.text,left:a,right:this.additive()};return a},additive:function(){for(var a=this.multiplicative(),b;b=this.expect("+","-");)a={type:q.BinaryExpression,
operator:b.text,left:a,right:this.multiplicative()};return a},multiplicative:function(){for(var a=this.unary(),b;b=this.expect("*","/","%");)a={type:q.BinaryExpression,operator:b.text,left:a,right:this.unary()};return a},unary:function(){var a;return(a=this.expect("+","-","!"))?{type:q.UnaryExpression,operator:a.text,prefix:!0,argument:this.unary()}:this.primary()},primary:function(){var a;this.expect("(")?(a=this.filterChain(),this.consume(")")):this.expect("[")?a=this.arrayDeclaration():this.expect("{")?
a=this.object():this.selfReferential.hasOwnProperty(this.peek().text)?a=Ia(this.selfReferential[this.consume().text]):this.options.literals.hasOwnProperty(this.peek().text)?a={type:q.Literal,value:this.options.literals[this.consume().text]}:this.peek().identifier?a=this.identifier():this.peek().constant?a=this.constant():this.throwError("not a primary expression",this.peek());for(var b;b=this.expect("(","[",".");)"("===b.text?(a={type:q.CallExpression,callee:a,arguments:this.parseArguments()},this.consume(")")):
"["===b.text?(a={type:q.MemberExpression,object:a,property:this.expression(),computed:!0},this.consume("]")):"."===b.text?a={type:q.MemberExpression,object:a,property:this.identifier(),computed:!1}:this.throwError("IMPOSSIBLE");return a},filter:function(a){a=[a];for(var b={type:q.CallExpression,callee:this.identifier(),arguments:a,filter:!0};this.expect(":");)a.push(this.expression());return b},parseArguments:function(){var a=[];if(")"!==this.peekToken().text){do a.push(this.filterChain());while(this.expect(","))
}return a},identifier:function(){var a=this.consume();a.identifier||this.throwError("is not a valid identifier",a);return{type:q.Identifier,name:a.text}},constant:function(){return{type:q.Literal,value:this.consume().value}},arrayDeclaration:function(){var a=[];if("]"!==this.peekToken().text){do{if(this.peek("]"))break;a.push(this.expression())}while(this.expect(","))}this.consume("]");return{type:q.ArrayExpression,elements:a}},object:function(){var a=[],b;if("}"!==this.peekToken().text){do{if(this.peek("}"))break;
b={type:q.Property,kind:"init"};this.peek().constant?(b.key=this.constant(),b.computed=!1,this.consume(":"),b.value=this.expression()):this.peek().identifier?(b.key=this.identifier(),b.computed=!1,this.peek(":")?(this.consume(":"),b.value=this.expression()):b.value=b.key):this.peek("[")?(this.consume("["),b.key=this.expression(),this.consume("]"),b.computed=!0,this.consume(":"),b.value=this.expression()):this.throwError("invalid key",this.peek());a.push(b)}while(this.expect(","))}this.consume("}");
return{type:q.ObjectExpression,properties:a}},throwError:function(a,b){throw Ya("syntax",b.text,a,b.index+1,this.text,this.text.substring(b.index));},consume:function(a){if(0===this.tokens.length)throw Ya("ueoe",this.text);var b=this.expect(a);b||this.throwError("is unexpected, expecting ["+a+"]",this.peek());return b},peekToken:function(){if(0===this.tokens.length)throw Ya("ueoe",this.text);return this.tokens[0]},peek:function(a,b,d,c){return this.peekAhead(0,a,b,d,c)},peekAhead:function(a,b,d,c,
e){if(this.tokens.length>a){a=this.tokens[a];var f=a.text;if(f===b||f===d||f===c||f===e||!(b||d||c||e))return a}return!1},expect:function(a,b,d,c){return(a=this.peek(a,b,d,c))?(this.tokens.shift(),a):!1},selfReferential:{"this":{type:q.ThisExpression},$locals:{type:q.LocalsExpression}}};var Fd=2;Jd.prototype={compile:function(a){var b=this;this.state={nextId:0,filters:{},fn:{vars:[],body:[],own:{}},assign:{vars:[],body:[],own:{}},inputs:[]};Z(a,b.$filter);var d="",c;this.stage="assign";if(c=Id(a))this.state.computing=
"assign",d=this.nextId(),this.recurse(c,d),this.return_(d),d="fn.assign="+this.generateFunction("assign","s,v,l");c=Gd(a.body);b.stage="inputs";r(c,function(a,c){var d="fn"+c;b.state[d]={vars:[],body:[],own:{}};b.state.computing=d;var k=b.nextId();b.recurse(a,k);b.return_(k);b.state.inputs.push({name:d,isPure:a.isPure});a.watchId=c});this.state.computing="fn";this.stage="main";this.recurse(a);a='"'+this.USE+" "+this.STRICT+'";\n'+this.filterPrefix()+"var fn="+this.generateFunction("fn","s,l,a,i")+
d+this.watchFns()+"return fn;";a=(new Function("$filter","getStringValue","ifDefined","plus",a))(this.$filter,Og,Pg,Ed);this.state=this.stage=void 0;return a},USE:"use",STRICT:"strict",watchFns:function(){var a=[],b=this.state.inputs,d=this;r(b,function(b){a.push("var "+b.name+"="+d.generateFunction(b.name,"s"));b.isPure&&a.push(b.name,".isPure="+JSON.stringify(b.isPure)+";")});b.length&&a.push("fn.inputs=["+b.map(function(a){return a.name}).join(",")+"];");return a.join("")},generateFunction:function(a,
b){return"function("+b+"){"+this.varsPrefix(a)+this.body(a)+"};"},filterPrefix:function(){var a=[],b=this;r(this.state.filters,function(d,c){a.push(d+"=$filter("+b.escape(c)+")")});return a.length?"var "+a.join(",")+";":""},varsPrefix:function(a){return this.state[a].vars.length?"var "+this.state[a].vars.join(",")+";":""},body:function(a){return this.state[a].body.join("")},recurse:function(a,b,d,c,e,f){var g,k,h=this,l,m,p;c=c||E;if(!f&&w(a.watchId))b=b||this.nextId(),this.if_("i",this.lazyAssign(b,
this.computedMember("i",a.watchId)),this.lazyRecurse(a,b,d,c,e,!0));else switch(a.type){case q.Program:r(a.body,function(b,c){h.recurse(b.expression,void 0,void 0,function(a){k=a});c!==a.body.length-1?h.current().body.push(k,";"):h.return_(k)});break;case q.Literal:m=this.escape(a.value);this.assign(b,m);c(b||m);break;case q.UnaryExpression:this.recurse(a.argument,void 0,void 0,function(a){k=a});m=a.operator+"("+this.ifDefined(k,0)+")";this.assign(b,m);c(m);break;case q.BinaryExpression:this.recurse(a.left,
void 0,void 0,function(a){g=a});this.recurse(a.right,void 0,void 0,function(a){k=a});m="+"===a.operator?this.plus(g,k):"-"===a.operator?this.ifDefined(g,0)+a.operator+this.ifDefined(k,0):"("+g+")"+a.operator+"("+k+")";this.assign(b,m);c(m);break;case q.LogicalExpression:b=b||this.nextId();h.recurse(a.left,b);h.if_("&&"===a.operator?b:h.not(b),h.lazyRecurse(a.right,b));c(b);break;case q.ConditionalExpression:b=b||this.nextId();h.recurse(a.test,b);h.if_(b,h.lazyRecurse(a.alternate,b),h.lazyRecurse(a.consequent,
b));c(b);break;case q.Identifier:b=b||this.nextId();d&&(d.context="inputs"===h.stage?"s":this.assign(this.nextId(),this.getHasOwnProperty("l",a.name)+"?l:s"),d.computed=!1,d.name=a.name);h.if_("inputs"===h.stage||h.not(h.getHasOwnProperty("l",a.name)),function(){h.if_("inputs"===h.stage||"s",function(){e&&1!==e&&h.if_(h.isNull(h.nonComputedMember("s",a.name)),h.lazyAssign(h.nonComputedMember("s",a.name),"{}"));h.assign(b,h.nonComputedMember("s",a.name))})},b&&h.lazyAssign(b,h.nonComputedMember("l",
a.name)));c(b);break;case q.MemberExpression:g=d&&(d.context=this.nextId())||this.nextId();b=b||this.nextId();h.recurse(a.object,g,void 0,function(){h.if_(h.notNull(g),function(){a.computed?(k=h.nextId(),h.recurse(a.property,k),h.getStringValue(k),e&&1!==e&&h.if_(h.not(h.computedMember(g,k)),h.lazyAssign(h.computedMember(g,k),"{}")),m=h.computedMember(g,k),h.assign(b,m),d&&(d.computed=!0,d.name=k)):(e&&1!==e&&h.if_(h.isNull(h.nonComputedMember(g,a.property.name)),h.lazyAssign(h.nonComputedMember(g,
a.property.name),"{}")),m=h.nonComputedMember(g,a.property.name),h.assign(b,m),d&&(d.computed=!1,d.name=a.property.name))},function(){h.assign(b,"undefined")});c(b)},!!e);break;case q.CallExpression:b=b||this.nextId();a.filter?(k=h.filter(a.callee.name),l=[],r(a.arguments,function(a){var b=h.nextId();h.recurse(a,b);l.push(b)}),m=k+"("+l.join(",")+")",h.assign(b,m),c(b)):(k=h.nextId(),g={},l=[],h.recurse(a.callee,k,g,function(){h.if_(h.notNull(k),function(){r(a.arguments,function(b){h.recurse(b,a.constant?
void 0:h.nextId(),void 0,function(a){l.push(a)})});m=g.name?h.member(g.context,g.name,g.computed)+"("+l.join(",")+")":k+"("+l.join(",")+")";h.assign(b,m)},function(){h.assign(b,"undefined")});c(b)}));break;case q.AssignmentExpression:k=this.nextId();g={};this.recurse(a.left,void 0,g,function(){h.if_(h.notNull(g.context),function(){h.recurse(a.right,k);m=h.member(g.context,g.name,g.computed)+a.operator+k;h.assign(b,m);c(b||m)})},1);break;case q.ArrayExpression:l=[];r(a.elements,function(b){h.recurse(b,
a.constant?void 0:h.nextId(),void 0,function(a){l.push(a)})});m="["+l.join(",")+"]";this.assign(b,m);c(b||m);break;case q.ObjectExpression:l=[];p=!1;r(a.properties,function(a){a.computed&&(p=!0)});p?(b=b||this.nextId(),this.assign(b,"{}"),r(a.properties,function(a){a.computed?(g=h.nextId(),h.recurse(a.key,g)):g=a.key.type===q.Identifier?a.key.name:""+a.key.value;k=h.nextId();h.recurse(a.value,k);h.assign(h.member(b,g,a.computed),k)})):(r(a.properties,function(b){h.recurse(b.value,a.constant?void 0:
h.nextId(),void 0,function(a){l.push(h.escape(b.key.type===q.Identifier?b.key.name:""+b.key.value)+":"+a)})}),m="{"+l.join(",")+"}",this.assign(b,m));c(b||m);break;case q.ThisExpression:this.assign(b,"s");c(b||"s");break;case q.LocalsExpression:this.assign(b,"l");c(b||"l");break;case q.NGValueParameter:this.assign(b,"v"),c(b||"v")}},getHasOwnProperty:function(a,b){var d=a+"."+b,c=this.current().own;c.hasOwnProperty(d)||(c[d]=this.nextId(!1,a+"&&("+this.escape(b)+" in "+a+")"));return c[d]},assign:function(a,
b){if(a)return this.current().body.push(a,"=",b,";"),a},filter:function(a){this.state.filters.hasOwnProperty(a)||(this.state.filters[a]=this.nextId(!0));return this.state.filters[a]},ifDefined:function(a,b){return"ifDefined("+a+","+this.escape(b)+")"},plus:function(a,b){return"plus("+a+","+b+")"},return_:function(a){this.current().body.push("return ",a,";")},if_:function(a,b,d){if(!0===a)b();else{var c=this.current().body;c.push("if(",a,"){");b();c.push("}");d&&(c.push("else{"),d(),c.push("}"))}},
not:function(a){return"!("+a+")"},isNull:function(a){return a+"==null"},notNull:function(a){return a+"!=null"},nonComputedMember:function(a,b){var d=/[^$_a-zA-Z0-9]/g;return/^[$_a-zA-Z][$_a-zA-Z0-9]*$/.test(b)?a+"."+b:a+'["'+b.replace(d,this.stringEscapeFn)+'"]'},computedMember:function(a,b){return a+"["+b+"]"},member:function(a,b,d){return d?this.computedMember(a,b):this.nonComputedMember(a,b)},getStringValue:function(a){this.assign(a,"getStringValue("+a+")")},lazyRecurse:function(a,b,d,c,e,f){var g=
this;return function(){g.recurse(a,b,d,c,e,f)}},lazyAssign:function(a,b){var d=this;return function(){d.assign(a,b)}},stringEscapeRegex:/[^ a-zA-Z0-9]/g,stringEscapeFn:function(a){return"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)},escape:function(a){if(A(a))return"'"+a.replace(this.stringEscapeRegex,this.stringEscapeFn)+"'";if(W(a))return a.toString();if(!0===a)return"true";if(!1===a)return"false";if(null===a)return"null";if("undefined"===typeof a)return"undefined";throw Ya("esc");},nextId:function(a,
b){var d="v"+this.state.nextId++;a||this.current().vars.push(d+(b?"="+b:""));return d},current:function(){return this.state[this.state.computing]}};Kd.prototype={compile:function(a){var b=this;Z(a,b.$filter);var d,c;if(d=Id(a))c=this.recurse(d);d=Gd(a.body);var e;d&&(e=[],r(d,function(a,c){var d=b.recurse(a);d.isPure=a.isPure;a.input=d;e.push(d);a.watchId=c}));var f=[];r(a.body,function(a){f.push(b.recurse(a.expression))});a=0===a.body.length?E:1===a.body.length?f[0]:function(a,b){var c;r(f,function(d){c=
d(a,b)});return c};c&&(a.assign=function(a,b,d){return c(a,d,b)});e&&(a.inputs=e);return a},recurse:function(a,b,d){var c,e,f=this,g;if(a.input)return this.inputs(a.input,a.watchId);switch(a.type){case q.Literal:return this.value(a.value,b);case q.UnaryExpression:return e=this.recurse(a.argument),this["unary"+a.operator](e,b);case q.BinaryExpression:return c=this.recurse(a.left),e=this.recurse(a.right),this["binary"+a.operator](c,e,b);case q.LogicalExpression:return c=this.recurse(a.left),e=this.recurse(a.right),
this["binary"+a.operator](c,e,b);case q.ConditionalExpression:return this["ternary?:"](this.recurse(a.test),this.recurse(a.alternate),this.recurse(a.consequent),b);case q.Identifier:return f.identifier(a.name,b,d);case q.MemberExpression:return c=this.recurse(a.object,!1,!!d),a.computed||(e=a.property.name),a.computed&&(e=this.recurse(a.property)),a.computed?this.computedMember(c,e,b,d):this.nonComputedMember(c,e,b,d);case q.CallExpression:return g=[],r(a.arguments,function(a){g.push(f.recurse(a))}),
a.filter&&(e=this.$filter(a.callee.name)),a.filter||(e=this.recurse(a.callee,!0)),a.filter?function(a,c,d,f){for(var p=[],n=0;n<g.length;++n)p.push(g[n](a,c,d,f));a=e.apply(void 0,p,f);return b?{context:void 0,name:void 0,value:a}:a}:function(a,c,d,f){var p=e(a,c,d,f),n;if(null!=p.value){n=[];for(var s=0;s<g.length;++s)n.push(g[s](a,c,d,f));n=p.value.apply(p.context,n)}return b?{value:n}:n};case q.AssignmentExpression:return c=this.recurse(a.left,!0,1),e=this.recurse(a.right),function(a,d,f,g){var p=
c(a,d,f,g);a=e(a,d,f,g);p.context[p.name]=a;return b?{value:a}:a};case q.ArrayExpression:return g=[],r(a.elements,function(a){g.push(f.recurse(a))}),function(a,c,d,e){for(var f=[],n=0;n<g.length;++n)f.push(g[n](a,c,d,e));return b?{value:f}:f};case q.ObjectExpression:return g=[],r(a.properties,function(a){a.computed?g.push({key:f.recurse(a.key),computed:!0,value:f.recurse(a.value)}):g.push({key:a.key.type===q.Identifier?a.key.name:""+a.key.value,computed:!1,value:f.recurse(a.value)})}),function(a,
c,d,e){for(var f={},n=0;n<g.length;++n)g[n].computed?f[g[n].key(a,c,d,e)]=g[n].value(a,c,d,e):f[g[n].key]=g[n].value(a,c,d,e);return b?{value:f}:f};case q.ThisExpression:return function(a){return b?{value:a}:a};case q.LocalsExpression:return function(a,c){return b?{value:c}:c};case q.NGValueParameter:return function(a,c,d){return b?{value:d}:d}}},"unary+":function(a,b){return function(d,c,e,f){d=a(d,c,e,f);d=w(d)?+d:0;return b?{value:d}:d}},"unary-":function(a,b){return function(d,c,e,f){d=a(d,c,
e,f);d=w(d)?-d:-0;return b?{value:d}:d}},"unary!":function(a,b){return function(d,c,e,f){d=!a(d,c,e,f);return b?{value:d}:d}},"binary+":function(a,b,d){return function(c,e,f,g){var k=a(c,e,f,g);c=b(c,e,f,g);k=Ed(k,c);return d?{value:k}:k}},"binary-":function(a,b,d){return function(c,e,f,g){var k=a(c,e,f,g);c=b(c,e,f,g);k=(w(k)?k:0)-(w(c)?c:0);return d?{value:k}:k}},"binary*":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)*b(c,e,f,g);return d?{value:c}:c}},"binary/":function(a,b,d){return function(c,
e,f,g){c=a(c,e,f,g)/b(c,e,f,g);return d?{value:c}:c}},"binary%":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)%b(c,e,f,g);return d?{value:c}:c}},"binary===":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)===b(c,e,f,g);return d?{value:c}:c}},"binary!==":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)!==b(c,e,f,g);return d?{value:c}:c}},"binary==":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)==b(c,e,f,g);return d?{value:c}:c}},"binary!=":function(a,b,d){return function(c,
e,f,g){c=a(c,e,f,g)!=b(c,e,f,g);return d?{value:c}:c}},"binary<":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)<b(c,e,f,g);return d?{value:c}:c}},"binary>":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)>b(c,e,f,g);return d?{value:c}:c}},"binary<=":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)<=b(c,e,f,g);return d?{value:c}:c}},"binary>=":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)>=b(c,e,f,g);return d?{value:c}:c}},"binary&&":function(a,b,d){return function(c,e,f,g){c=
a(c,e,f,g)&&b(c,e,f,g);return d?{value:c}:c}},"binary||":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)||b(c,e,f,g);return d?{value:c}:c}},"ternary?:":function(a,b,d,c){return function(e,f,g,k){e=a(e,f,g,k)?b(e,f,g,k):d(e,f,g,k);return c?{value:e}:e}},value:function(a,b){return function(){return b?{context:void 0,name:void 0,value:a}:a}},identifier:function(a,b,d){return function(c,e,f,g){c=e&&a in e?e:c;d&&1!==d&&c&&null==c[a]&&(c[a]={});e=c?c[a]:void 0;return b?{context:c,name:a,value:e}:
e}},computedMember:function(a,b,d,c){return function(e,f,g,k){var h=a(e,f,g,k),l,m;null!=h&&(l=b(e,f,g,k),l+="",c&&1!==c&&h&&!h[l]&&(h[l]={}),m=h[l]);return d?{context:h,name:l,value:m}:m}},nonComputedMember:function(a,b,d,c){return function(e,f,g,k){e=a(e,f,g,k);c&&1!==c&&e&&null==e[b]&&(e[b]={});f=null!=e?e[b]:void 0;return d?{context:e,name:b,value:f}:f}},inputs:function(a,b){return function(d,c,e,f){return f?f[b]:a(d,c,e)}}};Mb.prototype={constructor:Mb,parse:function(a){a=this.getAst(a);var b=
this.astCompiler.compile(a.ast),d=a.ast;b.literal=0===d.body.length||1===d.body.length&&(d.body[0].expression.type===q.Literal||d.body[0].expression.type===q.ArrayExpression||d.body[0].expression.type===q.ObjectExpression);b.constant=a.ast.constant;b.oneTime=a.oneTime;return b},getAst:function(a){var b=!1;a=a.trim();":"===a.charAt(0)&&":"===a.charAt(1)&&(b=!0,a=a.substring(2));return{ast:this.ast.ast(a),oneTime:b}}};var Ea=F("$sce"),V={HTML:"html",CSS:"css",MEDIA_URL:"mediaUrl",URL:"url",RESOURCE_URL:"resourceUrl",
JS:"js"},Cc=/_([a-z])/g,Ug=F("$templateRequest"),Vg=F("$timeout"),aa=C.document.createElement("a"),Od=ga(C.location.href),Na;aa.href="http://[::1]";var Wg="[::1]"===aa.hostname;Pd.$inject=["$document"];dd.$inject=["$provide"];var Wd=22,Vd=".",Ec="0";Qd.$inject=["$locale"];Sd.$inject=["$locale"];var gh={yyyy:ea("FullYear",4,0,!1,!0),yy:ea("FullYear",2,0,!0,!0),y:ea("FullYear",1,0,!1,!0),MMMM:kb("Month"),MMM:kb("Month",!0),MM:ea("Month",2,1),M:ea("Month",1,1),LLLL:kb("Month",!1,!0),dd:ea("Date",2),
d:ea("Date",1),HH:ea("Hours",2),H:ea("Hours",1),hh:ea("Hours",2,-12),h:ea("Hours",1,-12),mm:ea("Minutes",2),m:ea("Minutes",1),ss:ea("Seconds",2),s:ea("Seconds",1),sss:ea("Milliseconds",3),EEEE:kb("Day"),EEE:kb("Day",!0),a:function(a,b){return 12>a.getHours()?b.AMPMS[0]:b.AMPMS[1]},Z:function(a,b,d){a=-1*d;return a=(0<=a?"+":"")+(Ob(Math[0<a?"floor":"ceil"](a/60),2)+Ob(Math.abs(a%60),2))},ww:Yd(2),w:Yd(1),G:Fc,GG:Fc,GGG:Fc,GGGG:function(a,b){return 0>=a.getFullYear()?b.ERANAMES[0]:b.ERANAMES[1]}},
fh=/((?:[^yMLdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|L+|d+|H+|h+|m+|s+|a|Z|G+|w+))([\s\S]*)/,eh=/^-?\d+$/;Rd.$inject=["$locale"];var $g=ia(K),ah=ia(ub);Td.$inject=["$parse"];var Me=ia({restrict:"E",compile:function(a,b){if(!b.href&&!b.xlinkHref)return function(a,b){if("a"===b[0].nodeName.toLowerCase()){var e="[object SVGAnimatedString]"===la.call(b.prop("href"))?"xlink:href":"href";b.on("click",function(a){b.attr(e)||a.preventDefault()})}}}}),vb={};r(Gb,function(a,b){function d(a,d,e){a.$watch(e[c],
function(a){e.$set(b,!!a)})}if("multiple"!==a){var c=wa("ng-"+b),e=d;"checked"===a&&(e=function(a,b,e){e.ngModel!==e[c]&&d(a,b,e)});vb[c]=function(){return{restrict:"A",priority:100,link:e}}}});r(td,function(a,b){vb[b]=function(){return{priority:100,link:function(a,c,e){if("ngPattern"===b&&"/"===e.ngPattern.charAt(0)&&(c=e.ngPattern.match(ie))){e.$set("ngPattern",new RegExp(c[1],c[2]));return}a.$watch(e[b],function(a){e.$set(b,a)})}}}});r(["src","srcset","href"],function(a){var b=wa("ng-"+a);vb[b]=
["$sce",function(d){return{priority:99,link:function(c,e,f){var g=a,k=a;"href"===a&&"[object SVGAnimatedString]"===la.call(e.prop("href"))&&(k="xlinkHref",f.$attr[k]="xlink:href",g=null);f.$set(b,d.getTrustedMediaUrl(f[b]));f.$observe(b,function(b){b?(f.$set(k,b),Ca&&g&&e.prop(g,f[k])):"href"===a&&f.$set(k,null)})}}}]});var lb={$addControl:E,$getControls:ia([]),$$renameControl:function(a,b){a.$name=b},$removeControl:E,$setValidity:E,$setDirty:E,$setPristine:E,$setSubmitted:E,$$setSubmitted:E};Pb.$inject=
["$element","$attrs","$scope","$animate","$interpolate"];Pb.prototype={$rollbackViewValue:function(){r(this.$$controls,function(a){a.$rollbackViewValue()})},$commitViewValue:function(){r(this.$$controls,function(a){a.$commitViewValue()})},$addControl:function(a){Ja(a.$name,"input");this.$$controls.push(a);a.$name&&(this[a.$name]=a);a.$$parentForm=this},$getControls:function(){return ja(this.$$controls)},$$renameControl:function(a,b){var d=a.$name;this[d]===a&&delete this[d];this[b]=a;a.$name=b},$removeControl:function(a){a.$name&&
this[a.$name]===a&&delete this[a.$name];r(this.$pending,function(b,d){this.$setValidity(d,null,a)},this);r(this.$error,function(b,d){this.$setValidity(d,null,a)},this);r(this.$$success,function(b,d){this.$setValidity(d,null,a)},this);cb(this.$$controls,a);a.$$parentForm=lb},$setDirty:function(){this.$$animate.removeClass(this.$$element,Za);this.$$animate.addClass(this.$$element,Vb);this.$dirty=!0;this.$pristine=!1;this.$$parentForm.$setDirty()},$setPristine:function(){this.$$animate.setClass(this.$$element,
Za,Vb+" ng-submitted");this.$dirty=!1;this.$pristine=!0;this.$submitted=!1;r(this.$$controls,function(a){a.$setPristine()})},$setUntouched:function(){r(this.$$controls,function(a){a.$setUntouched()})},$setSubmitted:function(){for(var a=this;a.$$parentForm&&a.$$parentForm!==lb;)a=a.$$parentForm;a.$$setSubmitted()},$$setSubmitted:function(){this.$$animate.addClass(this.$$element,"ng-submitted");this.$submitted=!0;r(this.$$controls,function(a){a.$$setSubmitted&&a.$$setSubmitted()})}};ae({clazz:Pb,set:function(a,
b,d){var c=a[b];c?-1===c.indexOf(d)&&c.push(d):a[b]=[d]},unset:function(a,b,d){var c=a[b];c&&(cb(c,d),0===c.length&&delete a[b])}});var ke=function(a){return["$timeout","$parse",function(b,d){function c(a){return""===a?d('this[""]').assign:d(a).assign||E}return{name:"form",restrict:a?"EAC":"E",require:["form","^^?form"],controller:Pb,compile:function(d,f){d.addClass(Za).addClass(mb);var g=f.name?"name":a&&f.ngForm?"ngForm":!1;return{pre:function(a,d,e,f){var p=f[0];if(!("action"in e)){var n=function(b){a.$apply(function(){p.$commitViewValue();
p.$setSubmitted()});b.preventDefault()};d[0].addEventListener("submit",n);d.on("$destroy",function(){b(function(){d[0].removeEventListener("submit",n)},0,!1)})}(f[1]||p.$$parentForm).$addControl(p);var s=g?c(p.$name):E;g&&(s(a,p),e.$observe(g,function(b){p.$name!==b&&(s(a,void 0),p.$$parentForm.$$renameControl(p,b),s=c(p.$name),s(a,p))}));d.on("$destroy",function(){p.$$parentForm.$removeControl(p);s(a,void 0);S(p,lb)})}}}}}]},Ne=ke(),Ze=ke(!0),hh=/^\d{4,}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+(?:[+-][0-2]\d:[0-5]\d|Z)$/,
sh=/^[a-z][a-z\d.+-]*:\/*(?:[^:@]+(?::[^@]+)?@)?(?:[^\s:/?#]+|\[[a-f\d:]+])(?::\d+)?(?:\/[^?#]*)?(?:\?[^#]*)?(?:#.*)?$/i,th=/^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/,ih=/^\s*(-|\+)?(\d+|(\d*(\.\d*)))([eE][+-]?\d+)?\s*$/,le=/^(\d{4,})-(\d{2})-(\d{2})$/,me=/^(\d{4,})-(\d\d)-(\d\d)T(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/,Mc=/^(\d{4,})-W(\d\d)$/,ne=/^(\d{4,})-(\d\d)$/,
oe=/^(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/,ce=T();r(["date","datetime-local","month","time","week"],function(a){ce[a]=!0});var pe={text:function(a,b,d,c,e,f){Sa(a,b,d,c,e,f);Hc(c)},date:nb("date",le,Qb(le,["yyyy","MM","dd"]),"yyyy-MM-dd"),"datetime-local":nb("datetimelocal",me,Qb(me,"yyyy MM dd HH mm ss sss".split(" ")),"yyyy-MM-ddTHH:mm:ss.sss"),time:nb("time",oe,Qb(oe,["HH","mm","ss","sss"]),"HH:mm:ss.sss"),week:nb("week",Mc,function(a,b){if(ha(a))return a;if(A(a)){Mc.lastIndex=0;var d=Mc.exec(a);
if(d){var c=+d[1],e=+d[2],f=d=0,g=0,k=0,h=Xd(c),e=7*(e-1);b&&(d=b.getHours(),f=b.getMinutes(),g=b.getSeconds(),k=b.getMilliseconds());return new Date(c,0,h.getDate()+e,d,f,g,k)}}return NaN},"yyyy-Www"),month:nb("month",ne,Qb(ne,["yyyy","MM"]),"yyyy-MM"),number:function(a,b,d,c,e,f,g,k){Ic(a,b,d,c,"number");de(c);Sa(a,b,d,c,e,f);var h;if(w(d.min)||d.ngMin){var l=d.min||k(d.ngMin)(a);h=na(l);c.$validators.min=function(a,b){return c.$isEmpty(b)||z(h)||b>=h};d.$observe("min",function(a){a!==l&&(h=na(a),
l=a,c.$validate())})}if(w(d.max)||d.ngMax){var m=d.max||k(d.ngMax)(a),p=na(m);c.$validators.max=function(a,b){return c.$isEmpty(b)||z(p)||b<=p};d.$observe("max",function(a){a!==m&&(p=na(a),m=a,c.$validate())})}if(w(d.step)||d.ngStep){var n=d.step||k(d.ngStep)(a),s=na(n);c.$validators.step=function(a,b){return c.$isEmpty(b)||z(s)||ee(b,h||0,s)};d.$observe("step",function(a){a!==n&&(s=na(a),n=a,c.$validate())})}},url:function(a,b,d,c,e,f){Sa(a,b,d,c,e,f);Hc(c);c.$validators.url=function(a,b){var d=
a||b;return c.$isEmpty(d)||sh.test(d)}},email:function(a,b,d,c,e,f){Sa(a,b,d,c,e,f);Hc(c);c.$validators.email=function(a,b){var d=a||b;return c.$isEmpty(d)||th.test(d)}},radio:function(a,b,d,c){var e=!d.ngTrim||"false"!==U(d.ngTrim);z(d.name)&&b.attr("name",++pb);b.on("change",function(a){var g;b[0].checked&&(g=d.value,e&&(g=U(g)),c.$setViewValue(g,a&&a.type))});c.$render=function(){var a=d.value;e&&(a=U(a));b[0].checked=a===c.$viewValue};d.$observe("value",c.$render)},range:function(a,b,d,c,e,f){function g(a,
c){b.attr(a,d[a]);var e=d[a];d.$observe(a,function(a){a!==e&&(e=a,c(a))})}function k(a){p=na(a);X(c.$modelValue)||(m?(a=b.val(),p>a&&(a=p,b.val(a)),c.$setViewValue(a)):c.$validate())}function h(a){n=na(a);X(c.$modelValue)||(m?(a=b.val(),n<a&&(b.val(n),a=n<p?p:n),c.$setViewValue(a)):c.$validate())}function l(a){s=na(a);X(c.$modelValue)||(m?c.$viewValue!==b.val()&&c.$setViewValue(b.val()):c.$validate())}Ic(a,b,d,c,"range");de(c);Sa(a,b,d,c,e,f);var m=c.$$hasNativeValidators&&"range"===b[0].type,p=m?
0:void 0,n=m?100:void 0,s=m?1:void 0,r=b[0].validity;a=w(d.min);e=w(d.max);f=w(d.step);var q=c.$render;c.$render=m&&w(r.rangeUnderflow)&&w(r.rangeOverflow)?function(){q();c.$setViewValue(b.val())}:q;a&&(p=na(d.min),c.$validators.min=m?function(){return!0}:function(a,b){return c.$isEmpty(b)||z(p)||b>=p},g("min",k));e&&(n=na(d.max),c.$validators.max=m?function(){return!0}:function(a,b){return c.$isEmpty(b)||z(n)||b<=n},g("max",h));f&&(s=na(d.step),c.$validators.step=m?function(){return!r.stepMismatch}:
function(a,b){return c.$isEmpty(b)||z(s)||ee(b,p||0,s)},g("step",l))},checkbox:function(a,b,d,c,e,f,g,k){var h=fe(k,a,"ngTrueValue",d.ngTrueValue,!0),l=fe(k,a,"ngFalseValue",d.ngFalseValue,!1);b.on("change",function(a){c.$setViewValue(b[0].checked,a&&a.type)});c.$render=function(){b[0].checked=c.$viewValue};c.$isEmpty=function(a){return!1===a};c.$formatters.push(function(a){return va(a,h)});c.$parsers.push(function(a){return a?h:l})},hidden:E,button:E,submit:E,reset:E,file:E},Yc=["$browser","$sniffer",
"$filter","$parse",function(a,b,d,c){return{restrict:"E",require:["?ngModel"],link:{pre:function(e,f,g,k){k[0]&&(pe[K(g.type)]||pe.text)(e,f,g,k[0],b,a,d,c)}}}}],vf=function(){var a={configurable:!0,enumerable:!1,get:function(){return this.getAttribute("value")||""},set:function(a){this.setAttribute("value",a)}};return{restrict:"E",priority:200,compile:function(b,d){if("hidden"===K(d.type))return{pre:function(b,d,f,g){b=d[0];b.parentNode&&b.parentNode.insertBefore(b,b.nextSibling);Object.defineProperty&&
Object.defineProperty(b,"value",a)}}}}},uh=/^(true|false|\d+)$/,sf=function(){function a(a,d,c){var e=w(c)?c:9===Ca?"":null;a.prop("value",e);d.$set("value",c)}return{restrict:"A",priority:100,compile:function(b,d){return uh.test(d.ngValue)?function(b,d,f){b=b.$eval(f.ngValue);a(d,f,b)}:function(b,d,f){b.$watch(f.ngValue,function(b){a(d,f,b)})}}}},Re=["$compile",function(a){return{restrict:"AC",compile:function(b){a.$$addBindingClass(b);return function(b,c,e){a.$$addBindingInfo(c,e.ngBind);c=c[0];
b.$watch(e.ngBind,function(a){c.textContent=ic(a)})}}}}],Te=["$interpolate","$compile",function(a,b){return{compile:function(d){b.$$addBindingClass(d);return function(c,d,f){c=a(d.attr(f.$attr.ngBindTemplate));b.$$addBindingInfo(d,c.expressions);d=d[0];f.$observe("ngBindTemplate",function(a){d.textContent=z(a)?"":a})}}}}],Se=["$sce","$parse","$compile",function(a,b,d){return{restrict:"A",compile:function(c,e){var f=b(e.ngBindHtml),g=b(e.ngBindHtml,function(b){return a.valueOf(b)});d.$$addBindingClass(c);
return function(b,c,e){d.$$addBindingInfo(c,e.ngBindHtml);b.$watch(g,function(){var d=f(b);c.html(a.getTrustedHtml(d)||"")})}}}}],rf=ia({restrict:"A",require:"ngModel",link:function(a,b,d,c){c.$viewChangeListeners.push(function(){a.$eval(d.ngChange)})}}),Ue=Kc("",!0),We=Kc("Odd",0),Ve=Kc("Even",1),Xe=Ra({compile:function(a,b){b.$set("ngCloak",void 0);a.removeClass("ng-cloak")}}),Ye=[function(){return{restrict:"A",scope:!0,controller:"@",priority:500}}],cd={},vh={blur:!0,focus:!0};r("click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste".split(" "),
function(a){var b=wa("ng-"+a);cd[b]=["$parse","$rootScope","$exceptionHandler",function(d,c,e){return qd(d,c,e,b,a,vh[a])}]});var af=["$animate","$compile",function(a,b){return{multiElement:!0,transclude:"element",priority:600,terminal:!0,restrict:"A",$$tlb:!0,link:function(d,c,e,f,g){var k,h,l;d.$watch(e.ngIf,function(d){d?h||g(function(d,f){h=f;d[d.length++]=b.$$createComment("end ngIf",e.ngIf);k={clone:d};a.enter(d,c.parent(),c)}):(l&&(l.remove(),l=null),h&&(h.$destroy(),h=null),k&&(l=tb(k.clone),
a.leave(l).done(function(a){!1!==a&&(l=null)}),k=null))})}}}],bf=["$templateRequest","$anchorScroll","$animate",function(a,b,d){return{restrict:"ECA",priority:400,terminal:!0,transclude:"element",controller:ca.noop,compile:function(c,e){var f=e.ngInclude||e.src,g=e.onload||"",k=e.autoscroll;return function(c,e,m,p,n){var r=0,q,t,x,v=function(){t&&(t.remove(),t=null);q&&(q.$destroy(),q=null);x&&(d.leave(x).done(function(a){!1!==a&&(t=null)}),t=x,x=null)};c.$watch(f,function(f){var m=function(a){!1===
a||!w(k)||k&&!c.$eval(k)||b()},t=++r;f?(a(f,!0).then(function(a){if(!c.$$destroyed&&t===r){var b=c.$new();p.template=a;a=n(b,function(a){v();d.enter(a,null,e).done(m)});q=b;x=a;q.$emit("$includeContentLoaded",f);c.$eval(g)}},function(){c.$$destroyed||t!==r||(v(),c.$emit("$includeContentError",f))}),c.$emit("$includeContentRequested",f)):(v(),p.template=null)})}}}}],uf=["$compile",function(a){return{restrict:"ECA",priority:-400,require:"ngInclude",link:function(b,d,c,e){la.call(d[0]).match(/SVG/)?
(d.empty(),a(ed(e.template,C.document).childNodes)(b,function(a){d.append(a)},{futureParentElement:d})):(d.html(e.template),a(d.contents())(b))}}}],cf=Ra({priority:450,compile:function(){return{pre:function(a,b,d){a.$eval(d.ngInit)}}}}),qf=function(){return{restrict:"A",priority:100,require:"ngModel",link:function(a,b,d,c){var e=d.ngList||", ",f="false"!==d.ngTrim,g=f?U(e):e;c.$parsers.push(function(a){if(!z(a)){var b=[];a&&r(a.split(g),function(a){a&&b.push(f?U(a):a)});return b}});c.$formatters.push(function(a){if(H(a))return a.join(e)});
c.$isEmpty=function(a){return!a||!a.length}}}},mb="ng-valid",$d="ng-invalid",Za="ng-pristine",Vb="ng-dirty",ob=F("ngModel");Rb.$inject="$scope $exceptionHandler $attrs $element $parse $animate $timeout $q $interpolate".split(" ");Rb.prototype={$$initGetterSetters:function(){if(this.$options.getOption("getterSetter")){var a=this.$$parse(this.$$attr.ngModel+"()"),b=this.$$parse(this.$$attr.ngModel+"($$$p)");this.$$ngModelGet=function(b){var c=this.$$parsedNgModel(b);B(c)&&(c=a(b));return c};this.$$ngModelSet=
function(a,c){B(this.$$parsedNgModel(a))?b(a,{$$$p:c}):this.$$parsedNgModelAssign(a,c)}}else if(!this.$$parsedNgModel.assign)throw ob("nonassign",this.$$attr.ngModel,za(this.$$element));},$render:E,$isEmpty:function(a){return z(a)||""===a||null===a||a!==a},$$updateEmptyClasses:function(a){this.$isEmpty(a)?(this.$$animate.removeClass(this.$$element,"ng-not-empty"),this.$$animate.addClass(this.$$element,"ng-empty")):(this.$$animate.removeClass(this.$$element,"ng-empty"),this.$$animate.addClass(this.$$element,
"ng-not-empty"))},$setPristine:function(){this.$dirty=!1;this.$pristine=!0;this.$$animate.removeClass(this.$$element,Vb);this.$$animate.addClass(this.$$element,Za)},$setDirty:function(){this.$dirty=!0;this.$pristine=!1;this.$$animate.removeClass(this.$$element,Za);this.$$animate.addClass(this.$$element,Vb);this.$$parentForm.$setDirty()},$setUntouched:function(){this.$touched=!1;this.$untouched=!0;this.$$animate.setClass(this.$$element,"ng-untouched","ng-touched")},$setTouched:function(){this.$touched=
!0;this.$untouched=!1;this.$$animate.setClass(this.$$element,"ng-touched","ng-untouched")},$rollbackViewValue:function(){this.$$timeout.cancel(this.$$pendingDebounce);this.$viewValue=this.$$lastCommittedViewValue;this.$render()},$validate:function(){if(!X(this.$modelValue)){var a=this.$$lastCommittedViewValue,b=this.$$rawModelValue,d=this.$valid,c=this.$modelValue,e=this.$options.getOption("allowInvalid"),f=this;this.$$runValidators(b,a,function(a){e||d===a||(f.$modelValue=a?b:void 0,f.$modelValue!==
c&&f.$$writeModelToScope())})}},$$runValidators:function(a,b,d){function c(){var c=!0;r(h.$validators,function(d,e){var g=Boolean(d(a,b));c=c&&g;f(e,g)});return c?!0:(r(h.$asyncValidators,function(a,b){f(b,null)}),!1)}function e(){var c=[],d=!0;r(h.$asyncValidators,function(e,g){var h=e(a,b);if(!h||!B(h.then))throw ob("nopromise",h);f(g,void 0);c.push(h.then(function(){f(g,!0)},function(){d=!1;f(g,!1)}))});c.length?h.$$q.all(c).then(function(){g(d)},E):g(!0)}function f(a,b){k===h.$$currentValidationRunId&&
h.$setValidity(a,b)}function g(a){k===h.$$currentValidationRunId&&d(a)}this.$$currentValidationRunId++;var k=this.$$currentValidationRunId,h=this;(function(){var a=h.$$parserName;if(z(h.$$parserValid))f(a,null);else return h.$$parserValid||(r(h.$validators,function(a,b){f(b,null)}),r(h.$asyncValidators,function(a,b){f(b,null)})),f(a,h.$$parserValid),h.$$parserValid;return!0})()?c()?e():g(!1):g(!1)},$commitViewValue:function(){var a=this.$viewValue;this.$$timeout.cancel(this.$$pendingDebounce);if(this.$$lastCommittedViewValue!==
a||""===a&&this.$$hasNativeValidators)this.$$updateEmptyClasses(a),this.$$lastCommittedViewValue=a,this.$pristine&&this.$setDirty(),this.$$parseAndValidate()},$$parseAndValidate:function(){var a=this.$$lastCommittedViewValue,b=this;this.$$parserValid=z(a)?void 0:!0;this.$setValidity(this.$$parserName,null);this.$$parserName="parse";if(this.$$parserValid)for(var d=0;d<this.$parsers.length;d++)if(a=this.$parsers[d](a),z(a)){this.$$parserValid=!1;break}X(this.$modelValue)&&(this.$modelValue=this.$$ngModelGet(this.$$scope));
var c=this.$modelValue,e=this.$options.getOption("allowInvalid");this.$$rawModelValue=a;e&&(this.$modelValue=a,b.$modelValue!==c&&b.$$writeModelToScope());this.$$runValidators(a,this.$$lastCommittedViewValue,function(d){e||(b.$modelValue=d?a:void 0,b.$modelValue!==c&&b.$$writeModelToScope())})},$$writeModelToScope:function(){this.$$ngModelSet(this.$$scope,this.$modelValue);r(this.$viewChangeListeners,function(a){try{a()}catch(b){this.$$exceptionHandler(b)}},this)},$setViewValue:function(a,b){this.$viewValue=
a;this.$options.getOption("updateOnDefault")&&this.$$debounceViewValueCommit(b)},$$debounceViewValueCommit:function(a){var b=this.$options.getOption("debounce");W(b[a])?b=b[a]:W(b["default"])&&-1===this.$options.getOption("updateOn").indexOf(a)?b=b["default"]:W(b["*"])&&(b=b["*"]);this.$$timeout.cancel(this.$$pendingDebounce);var d=this;0<b?this.$$pendingDebounce=this.$$timeout(function(){d.$commitViewValue()},b):this.$$rootScope.$$phase?this.$commitViewValue():this.$$scope.$apply(function(){d.$commitViewValue()})},
$overrideModelOptions:function(a){this.$options=this.$options.createChild(a);this.$$setUpdateOnEvents()},$processModelValue:function(){var a=this.$$format();this.$viewValue!==a&&(this.$$updateEmptyClasses(a),this.$viewValue=this.$$lastCommittedViewValue=a,this.$render(),this.$$runValidators(this.$modelValue,this.$viewValue,E))},$$format:function(){for(var a=this.$formatters,b=a.length,d=this.$modelValue;b--;)d=a[b](d);return d},$$setModelValue:function(a){this.$modelValue=this.$$rawModelValue=a;this.$$parserValid=
void 0;this.$processModelValue()},$$setUpdateOnEvents:function(){this.$$updateEvents&&this.$$element.off(this.$$updateEvents,this.$$updateEventHandler);if(this.$$updateEvents=this.$options.getOption("updateOn"))this.$$element.on(this.$$updateEvents,this.$$updateEventHandler)},$$updateEventHandler:function(a){this.$$debounceViewValueCommit(a&&a.type)}};ae({clazz:Rb,set:function(a,b){a[b]=!0},unset:function(a,b){delete a[b]}});var pf=["$rootScope",function(a){return{restrict:"A",require:["ngModel",
"^?form","^?ngModelOptions"],controller:Rb,priority:1,compile:function(b){b.addClass(Za).addClass("ng-untouched").addClass(mb);return{pre:function(a,b,e,f){var g=f[0];b=f[1]||g.$$parentForm;if(f=f[2])g.$options=f.$options;g.$$initGetterSetters();b.$addControl(g);e.$observe("name",function(a){g.$name!==a&&g.$$parentForm.$$renameControl(g,a)});a.$on("$destroy",function(){g.$$parentForm.$removeControl(g)})},post:function(b,c,e,f){function g(){k.$setTouched()}var k=f[0];k.$$setUpdateOnEvents();c.on("blur",
function(){k.$touched||(a.$$phase?b.$evalAsync(g):b.$apply(g))})}}}}}],Sb,wh=/(\s+|^)default(\s+|$)/;Lc.prototype={getOption:function(a){return this.$$options[a]},createChild:function(a){var b=!1;a=S({},a);r(a,function(d,c){"$inherit"===d?"*"===c?b=!0:(a[c]=this.$$options[c],"updateOn"===c&&(a.updateOnDefault=this.$$options.updateOnDefault)):"updateOn"===c&&(a.updateOnDefault=!1,a[c]=U(d.replace(wh,function(){a.updateOnDefault=!0;return" "})))},this);b&&(delete a["*"],ge(a,this.$$options));ge(a,Sb.$$options);
return new Lc(a)}};Sb=new Lc({updateOn:"",updateOnDefault:!0,debounce:0,getterSetter:!1,allowInvalid:!1,timezone:null});var tf=function(){function a(a,d){this.$$attrs=a;this.$$scope=d}a.$inject=["$attrs","$scope"];a.prototype={$onInit:function(){var a=this.parentCtrl?this.parentCtrl.$options:Sb,d=this.$$scope.$eval(this.$$attrs.ngModelOptions);this.$options=a.createChild(d)}};return{restrict:"A",priority:10,require:{parentCtrl:"?^^ngModelOptions"},bindToController:!0,controller:a}},df=Ra({terminal:!0,
priority:1E3}),xh=F("ngOptions"),yh=/^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?(?:\s+disable\s+when\s+([\s\S]+?))?\s+for\s+(?:([$\w][$\w]*)|(?:\(\s*([$\w][$\w]*)\s*,\s*([$\w][$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/,nf=["$compile","$document","$parse",function(a,b,d){function c(a,b,c){function e(a,b,c,d,f){this.selectValue=a;this.viewValue=b;this.label=c;this.group=d;this.disabled=f}function f(a){var b;if(!r&&ya(a))b=a;else{b=[];for(var c in a)a.hasOwnProperty(c)&&
"$"!==c.charAt(0)&&b.push(c)}return b}var p=a.match(yh);if(!p)throw xh("iexp",a,za(b));var n=p[5]||p[7],r=p[6];a=/ as /.test(p[0])&&p[1];var q=p[9];b=d(p[2]?p[1]:n);var t=a&&d(a)||b,w=q&&d(q),v=q?function(a,b){return w(c,b)}:function(a){return La(a)},x=function(a,b){return v(a,A(a,b))},z=d(p[2]||p[1]),y=d(p[3]||""),J=d(p[4]||""),I=d(p[8]),B={},A=r?function(a,b){B[r]=b;B[n]=a;return B}:function(a){B[n]=a;return B};return{trackBy:q,getTrackByValue:x,getWatchables:d(I,function(a){var b=[];a=a||[];for(var d=
f(a),e=d.length,g=0;g<e;g++){var k=a===d?g:d[g],l=a[k],k=A(l,k),l=v(l,k);b.push(l);if(p[2]||p[1])l=z(c,k),b.push(l);p[4]&&(k=J(c,k),b.push(k))}return b}),getOptions:function(){for(var a=[],b={},d=I(c)||[],g=f(d),k=g.length,n=0;n<k;n++){var p=d===g?n:g[n],r=A(d[p],p),s=t(c,r),p=v(s,r),w=z(c,r),B=y(c,r),r=J(c,r),s=new e(p,s,w,B,r);a.push(s);b[p]=s}return{items:a,selectValueMap:b,getOptionFromViewValue:function(a){return b[x(a)]},getViewValueFromOption:function(a){return q?Ia(a.viewValue):a.viewValue}}}}}
var e=C.document.createElement("option"),f=C.document.createElement("optgroup");return{restrict:"A",terminal:!0,require:["select","ngModel"],link:{pre:function(a,b,c,d){d[0].registerOption=E},post:function(d,k,h,l){function m(a){var b=(a=v.getOptionFromViewValue(a))&&a.element;b&&!b.selected&&(b.selected=!0);return a}function p(a,b){a.element=b;b.disabled=a.disabled;a.label!==b.label&&(b.label=a.label,b.textContent=a.label);b.value=a.selectValue}var n=l[0],q=l[1],z=h.multiple;l=0;for(var t=k.children(),
B=t.length;l<B;l++)if(""===t[l].value){n.hasEmptyOption=!0;n.emptyOption=t.eq(l);break}k.empty();l=!!n.emptyOption;x(e.cloneNode(!1)).val("?");var v,A=c(h.ngOptions,k,d),C=b[0].createDocumentFragment();n.generateUnknownOptionValue=function(a){return"?"};z?(n.writeValue=function(a){if(v){var b=a&&a.map(m)||[];v.items.forEach(function(a){a.element.selected&&-1===Array.prototype.indexOf.call(b,a)&&(a.element.selected=!1)})}},n.readValue=function(){var a=k.val()||[],b=[];r(a,function(a){(a=v.selectValueMap[a])&&
!a.disabled&&b.push(v.getViewValueFromOption(a))});return b},A.trackBy&&d.$watchCollection(function(){if(H(q.$viewValue))return q.$viewValue.map(function(a){return A.getTrackByValue(a)})},function(){q.$render()})):(n.writeValue=function(a){if(v){var b=k[0].options[k[0].selectedIndex],c=v.getOptionFromViewValue(a);b&&b.removeAttribute("selected");c?(k[0].value!==c.selectValue&&(n.removeUnknownOption(),k[0].value=c.selectValue,c.element.selected=!0),c.element.setAttribute("selected","selected")):n.selectUnknownOrEmptyOption(a)}},
n.readValue=function(){var a=v.selectValueMap[k.val()];return a&&!a.disabled?(n.unselectEmptyOption(),n.removeUnknownOption(),v.getViewValueFromOption(a)):null},A.trackBy&&d.$watch(function(){return A.getTrackByValue(q.$viewValue)},function(){q.$render()}));l&&(a(n.emptyOption)(d),k.prepend(n.emptyOption),8===n.emptyOption[0].nodeType?(n.hasEmptyOption=!1,n.registerOption=function(a,b){""===b.val()&&(n.hasEmptyOption=!0,n.emptyOption=b,n.emptyOption.removeClass("ng-scope"),q.$render(),b.on("$destroy",
function(){var a=n.$isEmptyOptionSelected();n.hasEmptyOption=!1;n.emptyOption=void 0;a&&q.$render()}))}):n.emptyOption.removeClass("ng-scope"));d.$watchCollection(A.getWatchables,function(){var a=v&&n.readValue();if(v)for(var b=v.items.length-1;0<=b;b--){var c=v.items[b];w(c.group)?Fb(c.element.parentNode):Fb(c.element)}v=A.getOptions();var d={};v.items.forEach(function(a){var b;if(w(a.group)){b=d[a.group];b||(b=f.cloneNode(!1),C.appendChild(b),b.label=null===a.group?"null":a.group,d[a.group]=b);
var c=e.cloneNode(!1);b.appendChild(c);p(a,c)}else b=e.cloneNode(!1),C.appendChild(b),p(a,b)});k[0].appendChild(C);q.$render();q.$isEmpty(a)||(b=n.readValue(),(A.trackBy||z?va(a,b):a===b)||(q.$setViewValue(b),q.$render()))})}}}}],ef=["$locale","$interpolate","$log",function(a,b,d){var c=/{}/g,e=/^when(Minus)?(.+)$/;return{link:function(f,g,k){function h(a){g.text(a||"")}var l=k.count,m=k.$attr.when&&g.attr(k.$attr.when),p=k.offset||0,n=f.$eval(m)||{},q={},w=b.startSymbol(),t=b.endSymbol(),x=w+l+"-"+
p+t,v=ca.noop,A;r(k,function(a,b){var c=e.exec(b);c&&(c=(c[1]?"-":"")+K(c[2]),n[c]=g.attr(k.$attr[b]))});r(n,function(a,d){q[d]=b(a.replace(c,x))});f.$watch(l,function(b){var c=parseFloat(b),e=X(c);e||c in n||(c=a.pluralCat(c-p));c===A||e&&X(A)||(v(),e=q[c],z(e)?(null!=b&&d.debug("ngPluralize: no rule defined for '"+c+"' in "+m),v=E,h()):v=f.$watch(e,h),A=c)})}}}],qe=F("ngRef"),ff=["$parse",function(a){return{priority:-1,restrict:"A",compile:function(b,d){var c=wa(ua(b)),e=a(d.ngRef),f=e.assign||
function(){throw qe("nonassign",d.ngRef);};return function(a,b,h){var l;if(h.hasOwnProperty("ngRefRead"))if("$element"===h.ngRefRead)l=b;else{if(l=b.data("$"+h.ngRefRead+"Controller"),!l)throw qe("noctrl",h.ngRefRead,d.ngRef);}else l=b.data("$"+c+"Controller");l=l||b;f(a,l);b.on("$destroy",function(){e(a)===l&&f(a,null)})}}}}],gf=["$parse","$animate","$compile",function(a,b,d){var c=F("ngRepeat"),e=function(a,b,c,d,e,f,g){a[c]=d;e&&(a[e]=f);a.$index=b;a.$first=0===b;a.$last=b===g-1;a.$middle=!(a.$first||
a.$last);a.$odd=!(a.$even=0===(b&1))},f=function(a,b,c){return La(c)},g=function(a,b){return b};return{restrict:"A",multiElement:!0,transclude:"element",priority:1E3,terminal:!0,$$tlb:!0,compile:function(k,h){var l=h.ngRepeat,m=d.$$createComment("end ngRepeat",l),p=l.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/);if(!p)throw c("iexp",l);var n=p[1],q=p[2],w=p[3],t=p[4],p=n.match(/^(?:(\s*[$\w]+)|\(\s*([$\w]+)\s*,\s*([$\w]+)\s*\))$/);if(!p)throw c("iidexp",
n);var x=p[3]||p[1],v=p[2];if(w&&(!/^[$a-zA-Z_][$a-zA-Z0-9_]*$/.test(w)||/^(null|undefined|this|\$index|\$first|\$middle|\$last|\$even|\$odd|\$parent|\$root|\$id)$/.test(w)))throw c("badident",w);var z;if(t){var A={$id:La},y=a(t);z=function(a,b,c,d){v&&(A[v]=b);A[x]=c;A.$index=d;return y(a,A)}}return function(a,d,h,k,n){var p=T();a.$watchCollection(q,function(h){var k,q,t=d[0],s,y=T(),B,C,E,D,H,F,K;w&&(a[w]=h);if(ya(h))H=h,q=z||f;else for(K in q=z||g,H=[],h)ta.call(h,K)&&"$"!==K.charAt(0)&&H.push(K);
B=H.length;K=Array(B);for(k=0;k<B;k++)if(C=h===H?k:H[k],E=h[C],D=q(a,C,E,k),p[D])F=p[D],delete p[D],y[D]=F,K[k]=F;else{if(y[D])throw r(K,function(a){a&&a.scope&&(p[a.id]=a)}),c("dupes",l,D,E);K[k]={id:D,scope:void 0,clone:void 0};y[D]=!0}A&&(A[x]=void 0);for(s in p){F=p[s];D=tb(F.clone);b.leave(D);if(D[0].parentNode)for(k=0,q=D.length;k<q;k++)D[k].$$NG_REMOVED=!0;F.scope.$destroy()}for(k=0;k<B;k++)if(C=h===H?k:H[k],E=h[C],F=K[k],F.scope){s=t;do s=s.nextSibling;while(s&&s.$$NG_REMOVED);F.clone[0]!==
s&&b.move(tb(F.clone),null,t);t=F.clone[F.clone.length-1];e(F.scope,k,x,E,v,C,B)}else n(function(a,c){F.scope=c;var d=m.cloneNode(!1);a[a.length++]=d;b.enter(a,null,t);t=d;F.clone=a;y[F.id]=F;e(F.scope,k,x,E,v,C,B)});p=y})}}}}],hf=["$animate",function(a){return{restrict:"A",multiElement:!0,link:function(b,d,c){b.$watch(c.ngShow,function(b){a[b?"removeClass":"addClass"](d,"ng-hide",{tempClasses:"ng-hide-animate"})})}}}],$e=["$animate",function(a){return{restrict:"A",multiElement:!0,link:function(b,
d,c){b.$watch(c.ngHide,function(b){a[b?"addClass":"removeClass"](d,"ng-hide",{tempClasses:"ng-hide-animate"})})}}}],jf=Ra(function(a,b,d){a.$watchCollection(d.ngStyle,function(a,d){d&&a!==d&&r(d,function(a,c){b.css(c,"")});a&&b.css(a)})}),kf=["$animate","$compile",function(a,b){return{require:"ngSwitch",controller:["$scope",function(){this.cases={}}],link:function(d,c,e,f){var g=[],k=[],h=[],l=[],m=function(a,b){return function(c){!1!==c&&a.splice(b,1)}};d.$watch(e.ngSwitch||e.on,function(c){for(var d,
e;h.length;)a.cancel(h.pop());d=0;for(e=l.length;d<e;++d){var q=tb(k[d].clone);l[d].$destroy();(h[d]=a.leave(q)).done(m(h,d))}k.length=0;l.length=0;(g=f.cases["!"+c]||f.cases["?"])&&r(g,function(c){c.transclude(function(d,e){l.push(e);var f=c.element;d[d.length++]=b.$$createComment("end ngSwitchWhen");k.push({clone:d});a.enter(d,f.parent(),f)})})})}}}],lf=Ra({transclude:"element",priority:1200,require:"^ngSwitch",multiElement:!0,link:function(a,b,d,c,e){a=d.ngSwitchWhen.split(d.ngSwitchWhenSeparator).sort().filter(function(a,
b,c){return c[b-1]!==a});r(a,function(a){c.cases["!"+a]=c.cases["!"+a]||[];c.cases["!"+a].push({transclude:e,element:b})})}}),mf=Ra({transclude:"element",priority:1200,require:"^ngSwitch",multiElement:!0,link:function(a,b,d,c,e){c.cases["?"]=c.cases["?"]||[];c.cases["?"].push({transclude:e,element:b})}}),zh=F("ngTransclude"),of=["$compile",function(a){return{restrict:"EAC",compile:function(b){var d=a(b.contents());b.empty();return function(a,b,f,g,k){function h(){d(a,function(a){b.append(a)})}if(!k)throw zh("orphan",
za(b));f.ngTransclude===f.$attr.ngTransclude&&(f.ngTransclude="");f=f.ngTransclude||f.ngTranscludeSlot;k(function(a,c){var d;if(d=a.length)a:{d=0;for(var f=a.length;d<f;d++){var g=a[d];if(g.nodeType!==Pa||g.nodeValue.trim()){d=!0;break a}}d=void 0}d?b.append(a):(h(),c.$destroy())},null,f);f&&!k.isSlotFilled(f)&&h()}}}}],Oe=["$templateCache",function(a){return{restrict:"E",terminal:!0,compile:function(b,d){"text/ng-template"===d.type&&a.put(d.id,b[0].text)}}}],Ah={$setViewValue:E,$render:E},Bh=["$element",
"$scope",function(a,b){function d(){g||(g=!0,b.$$postDigest(function(){g=!1;e.ngModelCtrl.$render()}))}function c(a){k||(k=!0,b.$$postDigest(function(){b.$$destroyed||(k=!1,e.ngModelCtrl.$setViewValue(e.readValue()),a&&e.ngModelCtrl.$render())}))}var e=this,f=new Hb;e.selectValueMap={};e.ngModelCtrl=Ah;e.multiple=!1;e.unknownOption=x(C.document.createElement("option"));e.hasEmptyOption=!1;e.emptyOption=void 0;e.renderUnknownOption=function(b){b=e.generateUnknownOptionValue(b);e.unknownOption.val(b);
a.prepend(e.unknownOption);Oa(e.unknownOption,!0);a.val(b)};e.updateUnknownOption=function(b){b=e.generateUnknownOptionValue(b);e.unknownOption.val(b);Oa(e.unknownOption,!0);a.val(b)};e.generateUnknownOptionValue=function(a){return"? "+La(a)+" ?"};e.removeUnknownOption=function(){e.unknownOption.parent()&&e.unknownOption.remove()};e.selectEmptyOption=function(){e.emptyOption&&(a.val(""),Oa(e.emptyOption,!0))};e.unselectEmptyOption=function(){e.hasEmptyOption&&Oa(e.emptyOption,!1)};b.$on("$destroy",
function(){e.renderUnknownOption=E});e.readValue=function(){var b=a.val(),b=b in e.selectValueMap?e.selectValueMap[b]:b;return e.hasOption(b)?b:null};e.writeValue=function(b){var c=a[0].options[a[0].selectedIndex];c&&Oa(x(c),!1);e.hasOption(b)?(e.removeUnknownOption(),c=La(b),a.val(c in e.selectValueMap?c:b),Oa(x(a[0].options[a[0].selectedIndex]),!0)):e.selectUnknownOrEmptyOption(b)};e.addOption=function(a,b){if(8!==b[0].nodeType){Ja(a,'"option value"');""===a&&(e.hasEmptyOption=!0,e.emptyOption=
b);var c=f.get(a)||0;f.set(a,c+1);d()}};e.removeOption=function(a){var b=f.get(a);b&&(1===b?(f.delete(a),""===a&&(e.hasEmptyOption=!1,e.emptyOption=void 0)):f.set(a,b-1))};e.hasOption=function(a){return!!f.get(a)};e.$hasEmptyOption=function(){return e.hasEmptyOption};e.$isUnknownOptionSelected=function(){return a[0].options[0]===e.unknownOption[0]};e.$isEmptyOptionSelected=function(){return e.hasEmptyOption&&a[0].options[a[0].selectedIndex]===e.emptyOption[0]};e.selectUnknownOrEmptyOption=function(a){null==
a&&e.emptyOption?(e.removeUnknownOption(),e.selectEmptyOption()):e.unknownOption.parent().length?e.updateUnknownOption(a):e.renderUnknownOption(a)};var g=!1,k=!1;e.registerOption=function(a,b,f,g,k){if(f.$attr.ngValue){var q,r;f.$observe("value",function(a){var d,f=b.prop("selected");w(r)&&(e.removeOption(q),delete e.selectValueMap[r],d=!0);r=La(a);q=a;e.selectValueMap[r]=a;e.addOption(a,b);b.attr("value",r);d&&f&&c()})}else g?f.$observe("value",function(a){e.readValue();var d,f=b.prop("selected");
w(q)&&(e.removeOption(q),d=!0);q=a;e.addOption(a,b);d&&f&&c()}):k?a.$watch(k,function(a,d){f.$set("value",a);var g=b.prop("selected");d!==a&&e.removeOption(d);e.addOption(a,b);d&&g&&c()}):e.addOption(f.value,b);f.$observe("disabled",function(a){if("true"===a||a&&b.prop("selected"))e.multiple?c(!0):(e.ngModelCtrl.$setViewValue(null),e.ngModelCtrl.$render())});b.on("$destroy",function(){var a=e.readValue(),b=f.value;e.removeOption(b);d();(e.multiple&&a&&-1!==a.indexOf(b)||a===b)&&c(!0)})}}],Pe=function(){return{restrict:"E",
require:["select","?ngModel"],controller:Bh,priority:1,link:{pre:function(a,b,d,c){var e=c[0],f=c[1];if(f){if(e.ngModelCtrl=f,b.on("change",function(){e.removeUnknownOption();a.$apply(function(){f.$setViewValue(e.readValue())})}),d.multiple){e.multiple=!0;e.readValue=function(){var a=[];r(b.find("option"),function(b){b.selected&&!b.disabled&&(b=b.value,a.push(b in e.selectValueMap?e.selectValueMap[b]:b))});return a};e.writeValue=function(a){r(b.find("option"),function(b){var c=!!a&&(-1!==Array.prototype.indexOf.call(a,
b.value)||-1!==Array.prototype.indexOf.call(a,e.selectValueMap[b.value]));c!==b.selected&&Oa(x(b),c)})};var g,k=NaN;a.$watch(function(){k!==f.$viewValue||va(g,f.$viewValue)||(g=ja(f.$viewValue),f.$render());k=f.$viewValue});f.$isEmpty=function(a){return!a||0===a.length}}}else e.registerOption=E},post:function(a,b,d,c){var e=c[1];if(e){var f=c[0];e.$render=function(){f.writeValue(e.$viewValue)}}}}}},Qe=["$interpolate",function(a){return{restrict:"E",priority:100,compile:function(b,d){var c,e;w(d.ngValue)||
(w(d.value)?c=a(d.value,!0):(e=a(b.text(),!0))||d.$set("value",b.text()));return function(a,b,d){var h=b.parent();(h=h.data("$selectController")||h.parent().data("$selectController"))&&h.registerOption(a,b,d,c,e)}}}}],$c=["$parse",function(a){return{restrict:"A",require:"?ngModel",link:function(b,d,c,e){if(e){var f=c.hasOwnProperty("required")||a(c.ngRequired)(b);c.ngRequired||(c.required=!0);e.$validators.required=function(a,b){return!f||!e.$isEmpty(b)};c.$observe("required",function(a){f!==a&&(f=
a,e.$validate())})}}}}],Zc=["$parse",function(a){return{restrict:"A",require:"?ngModel",compile:function(b,d){var c,e;d.ngPattern&&(c=d.ngPattern,e="/"===d.ngPattern.charAt(0)&&ie.test(d.ngPattern)?function(){return d.ngPattern}:a(d.ngPattern));return function(a,b,d,h){if(h){var l=d.pattern;d.ngPattern?l=e(a):c=d.pattern;var m=he(l,c,b);d.$observe("pattern",function(a){var d=m;m=he(a,c,b);(d&&d.toString())!==(m&&m.toString())&&h.$validate()});h.$validators.pattern=function(a,b){return h.$isEmpty(b)||
z(m)||m.test(b)}}}}}}],bd=["$parse",function(a){return{restrict:"A",require:"?ngModel",link:function(b,d,c,e){if(e){var f=c.maxlength||a(c.ngMaxlength)(b),g=Tb(f);c.$observe("maxlength",function(a){f!==a&&(g=Tb(a),f=a,e.$validate())});e.$validators.maxlength=function(a,b){return 0>g||e.$isEmpty(b)||b.length<=g}}}}}],ad=["$parse",function(a){return{restrict:"A",require:"?ngModel",link:function(b,d,c,e){if(e){var f=c.minlength||a(c.ngMinlength)(b),g=Tb(f)||-1;c.$observe("minlength",function(a){f!==
a&&(g=Tb(a)||-1,f=a,e.$validate())});e.$validators.minlength=function(a,b){return e.$isEmpty(b)||b.length>=g}}}}}];C.angular.bootstrap?C.console&&console.log("WARNING: Tried to load AngularJS more than once."):(Fe(),Je(ca),ca.module("ngLocale",[],["$provide",function(a){function b(a){a+="";var b=a.indexOf(".");return-1==b?0:a.length-b-1}a.value("$locale",{DATETIME_FORMATS:{AMPMS:["AM","PM"],DAY:"Sunday Monday Tuesday Wednesday Thursday Friday Saturday".split(" "),ERANAMES:["Before Christ","Anno Domini"],
ERAS:["BC","AD"],FIRSTDAYOFWEEK:6,MONTH:"January February March April May June July August September October November December".split(" "),SHORTDAY:"Sun Mon Tue Wed Thu Fri Sat".split(" "),SHORTMONTH:"Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(" "),STANDALONEMONTH:"January February March April May June July August September October November December".split(" "),WEEKENDRANGE:[5,6],fullDate:"EEEE, MMMM d, y",longDate:"MMMM d, y",medium:"MMM d, y h:mm:ss a",mediumDate:"MMM d, y",mediumTime:"h:mm:ss a",
"short":"M/d/yy h:mm a",shortDate:"M/d/yy",shortTime:"h:mm a"},NUMBER_FORMATS:{CURRENCY_SYM:"$",DECIMAL_SEP:".",GROUP_SEP:",",PATTERNS:[{gSize:3,lgSize:3,maxFrac:3,minFrac:0,minInt:1,negPre:"-",negSuf:"",posPre:"",posSuf:""},{gSize:3,lgSize:3,maxFrac:2,minFrac:2,minInt:1,negPre:"-\u00a4",negSuf:"",posPre:"\u00a4",posSuf:""}]},id:"en-us",localeID:"en_US",pluralCat:function(a,c){var e=a|0,f=c;void 0===f&&(f=Math.min(b(a),3));Math.pow(10,f);return 1==e&&0==f?"one":"other"}})}]),x(function(){Ae(C.document,
Uc)}))})(window);!window.angular.$$csp().noInlineStyle&&window.angular.element(document.head).prepend('<style type="text/css">@charset "UTF-8";[ng\\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak,.ng-hide:not(.ng-hide-animate){display:none !important;}ng\\:form{display:block;}.ng-animate-shim{visibility:hidden;}.ng-anchor{position:absolute;}</style>');
//# sourceMappingURL=angular.min.js.map

File diff suppressed because it is too large Load diff

View file

@ -1 +0,0 @@
../../../frontend/static/js/bootstrap.min.js

7
admin/static/js/bootstrap.min.js vendored Normal file

File diff suppressed because one or more lines are too long

355
admin/static/js/common.js Normal file
View file

@ -0,0 +1,355 @@
var alertNbLines = true;
function treatFlagKey(flag) {
if (flag.values !== undefined) {
if (flag.separator) {
for (var i = flag.values.length - 1; i >= 0; i--) {
if (flag.nb_lines && (flag.values[i] == undefined || !flag.values[i].length)) {
if (alertNbLines) {
alertNbLines = false;
if (!confirm("Lorsque plusieurs flags sont attendus pour une même question, ceux-ci ne sont pas validés un par un. Ils ne sont validés qu'une fois tous les champs remplis correctement. (Sauf mention contraire, l'ordre n'importe pas)"))
console.log(flag.values[9999].length); // Launch exception here to avoid form validation
}
}
else if (!flag.values[i].length) {
flag.values.splice(i, 1);
}
}
if (flag.ignore_order)
flag.value = flag.values.slice().sort().join(flag.separator) + flag.separator;
else
flag.value = flag.values.join(flag.separator) + flag.separator;
if (flag.values.length == 0)
flag.values = [""];
}
else
flag.value = flag.values[0];
}
if (flag.found == null && flag.soluce !== undefined) {
if (flag.value && flag.soluce) {
if (flag.ignore_case)
flag.value = flag.value.toLowerCase();
if (flag.capture_regexp) {
var re = new RegExp(flag.capture_regexp, flag.ignore_case?'ui':'u');
var match = re.exec(flag.value);
match.shift();
flag.value = match.join("+");
}
if (flag.soluce == b2sum(flag.value))
flag.found = new Date();
}
}
return flag.found !== undefined && flag.found !== false;
}
String.prototype.capitalize = function() {
return this
.toLowerCase()
.replace(
/(^|\s|-)([a-z])/g,
function(m,p1,p2) { return p1+p2.toUpperCase(); }
);
}
Array.prototype.inArray = function(v) {
return this.reduce(function(presence, current) {
return presence || current == v;
}, false);
}
angular.module("FICApp")
.directive('autofocus', ['$timeout', function($timeout) {
return {
restrict: 'A',
link : function($scope, $element) {
$timeout(function() {
$element[0].focus();
});
}
}
}])
.directive('autocarousel', ['$timeout', function($timeout) {
return {
restrict: 'A',
link : function($scope, $element) {
$timeout(function() {
$($element[0]).carousel();
});
}
}
}])
.directive('fileModel', ['$parse', function ($parse) {
return {
restrict: 'A',
link: function($scope, element, attrs) {
var model = $parse(attrs.fileModel);
var modelSetter = model.assign;
element.bind('change', function(){
$scope.$apply(function(){
modelSetter($scope, element[0].files[0]);
$scope.uploadFile();
});
});
}
};
}]);
angular.module("FICApp")
.filter("escapeURL", function() {
return function(input) {
return encodeURIComponent(input);
}
})
.filter("stripHTML", function() {
return function(input) {
if (!input)
return input;
return input.replace(
/(<([^>]+)>)/ig,
""
);
}
})
.filter("capitalize", function() {
return function(input) {
return input.capitalize();
}
})
.filter("rankTitle", function() {
var itms = {
"rank": "Rang",
"name": "Équipe",
"score": "Score",
};
return function(input) {
if (itms[input] != undefined) {
return itms[input];
} else {
return input;
}
}
})
.filter("time", function() {
return function(input) {
input = Math.floor(input);
if (input == undefined) {
return "--";
} else if (input >= 10) {
return input;
} else {
return "0" + input;
}
}
})
.filter("timer", function() {
return function(input) {
input = Math.floor(input / 1000);
var res = ""
if (input >= 3600) {
res += Math.floor(input / 3600) + ":";
input = input % 3600;
}
if (res || input >= 60) {
if (res && Math.floor(input / 60) <= 9)
res += "0";
res += Math.floor(input / 60) + "'";
input = input % 60;
}
return res + (input>9?input:"0"+input) + '"';
}
})
.filter("since", function() {
return function(passed) {
if (passed < 120000) {
return "Il y a " + Math.floor(passed/1000) + " secondes";
} else {
return "Il y a " + Math.floor(passed/60000) + " minutes";
}
}
})
.filter("size", function() {
var units = [
"o",
"kio",
"Mio",
"Gio",
"Tio",
"Pio",
"Eio",
"Zio",
"Yio",
]
return function(input) {
var res = input;
var unit = 0;
while (res > 1024) {
unit += 1;
res = res / 1024;
}
return (Math.round(res * 100) / 100) + " " + units[unit];
}
})
.filter("coeff", function() {
return function(input) {
if (input > 1) {
return "+" + Math.floor((input - 1) * 100) + " %"
} else if (input < 1) {
return "-" + Math.floor((1 - input) * 100) + " %"
} else {
return "";
}
}
})
.filter("objectLength", function() {
return function(input) {
if (input !== undefined)
return Object.keys(input).length;
else
return "";
}
})
.filter("bto16", function() {
return function(input) {
const raw = atob(input);
let result = '';
for (let i = 0; i < raw.length; i++) {
const hex = raw.charCodeAt(i).toString(16);
result += (hex.length === 2 ? hex : '0' + hex);
}
return result;
}
});
angular.module("FICApp")
.component('flagKey', {
bindings: {
kid: '=',
key: '=',
settings: '=',
wantchoices: '=',
},
controller: function() {
this.additem = function(key) {
this.key.values.push("");
};
},
template: `
<div class="form-group">
<label for="sol_{{ $ctrl.kid }}_0" ng-class="{'text-light': !$ctrl.key.found}">{{ $ctrl.key.label }}&nbsp;:</label>
<span ng-if="$ctrl.key.found && $ctrl.key.value" ng-bind="$ctrl.key.value"></span>
<div class="input-group" ng-repeat="v in $ctrl.key.values track by $index" ng-class="{'mt-1': !$first}" ng-if="!$ctrl.key.found">
<input type="text" class="form-control flag" id="sol_{{ $ctrl.kid }}_{{ $index }}" autocomplete="off" name="sol_{{ $ctrl.kid }}_{{ $index }}" ng-model="$ctrl.key.values[$index]" ng-if="!$ctrl.key.choices && !$ctrl.key.multiline" placeholder="{{ $ctrl.key.placeholder }}" title="{{ $ctrl.key.placeholder }}">
<textarea class="form-control flag" id="sol_{{ $ctrl.kid }}_{{ $index }}" autocomplete="off" name="sol_{{ $ctrl.kid }}_{{ $index }}" ng-model="$ctrl.key.values[$index]" ng-if="!$ctrl.key.choices && $ctrl.key.multiline" placeholder="{{ $ctrl.key.placeholder }}" title="{{ $ctrl.key.placeholder }}"></textarea>
<select class="custom-select" id="sol_{{ $ctrl.kid }}" name="sol_{{ $ctrl.kid }}" ng-model="$ctrl.key.values[$index]" ng-if="$ctrl.key.choices" ng-options="l as v for (l, v) in $ctrl.key.choices"></select>
<div class="input-group-append" ng-if="$ctrl.key.choices_cost">
<button class="btn btn-success" type="button" ng-click="$ctrl.wantchoices($ctrl.kid)" ng-class="{disabled: $ctrl.key.wcsubmitted}" title="Cliquez pour échanger ce champ de texte par une liste de choix. L'opération vous coûtera {{ $ctrl.key.choices_cost * $ctrl.settings.wchoiceCurrentCoefficient }} points.">
<span class="glyphicon glyphicon-tasks" aria-hidden="true"></span>
Liste de propositions (<ng-pluralize count="$ctrl.key.choices_cost * $ctrl.settings.wchoiceCurrentCoefficient" when="{'one': '{} point', 'other': '{} points'}"></ng-pluralize>)
</button>
</div>
<div class="input-group-append" ng-if="$ctrl.key.separator && !$ctrl.key.nb_lines && $last">
<button class="btn btn-success" type="button" ng-click="$ctrl.additem(key)" title="Ajouter un élément.">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
</button>
</div>
</div>
<small class="form-text text-muted" ng-if="!$ctrl.key.found && $ctrl.key.help.length > 0" ng-bind-html="$ctrl.key.help"></small>
<span class="glyphicon glyphicon-ok form-control-feedback text-success" aria-hidden="true" ng-if="$ctrl.key.found" title="Flag trouvé à {{ $ctrl.key.found | date:'mediumTime'}}"></span>
</div>
`
});
angular.module("FICApp")
.run(function($rootScope) {
$rootScope.recvTime = function(response) {
time = {
"cu": Math.floor(response.headers("x-fic-time") * 1000),
"he": (new Date()).getTime(),
};
sessionStorage.time = angular.toJson(time);
return time;
}
})
.controller("CountdownController", function($scope, $rootScope, $interval) {
var time;
if (sessionStorage.time)
time = angular.fromJson(sessionStorage.time);
$scope.time = {};
$rootScope.getSrvTime = function() {
if (time && time.cu && time.he)
return new Date(Date.now() + (time.cu - time.he));
else
return undefined;
}
function updTime() {
if (time && $rootScope.settings && $rootScope.settings.end) {
var srv_cur = new Date(Date.now() + (time.cu - time.he));
// Refresh on start/activate time reached
if (Math.floor($rootScope.settings.start / 1000) == Math.floor(srv_cur / 1000) ||Math.floor($rootScope.settings.activateTime / 1000) == Math.floor(srv_cur / 1000))
$rootScope.refresh(true, true);
var remain = 0;
if ($rootScope.settings.start === undefined || $rootScope.settings.start == 0) {
$scope.time = {};
return
} else if ($rootScope.settings.start > srv_cur) {
$scope.startIn = Math.floor(($rootScope.settings.start - srv_cur) / 1000);
remain = $rootScope.settings.end - $rootScope.settings.start;
} else if ($rootScope.settings.end > srv_cur) {
$scope.startIn = 0;
remain = $rootScope.settings.end - srv_cur;
}
$rootScope.timeProgression = 1 - remain / ($rootScope.settings.end - $rootScope.settings.start);
$rootScope.timeRemaining = remain;
if ($rootScope.settings.activateTime) {
var now = new Date();
var actTime = new Date($rootScope.settings.activateTime);
if (actTime > now)
$rootScope.activateTimeCountDown = actTime - now;
else
$rootScope.activateTimeCountDown = null;
} else {
$rootScope.activateTimeCountDown = null;
}
remain = remain / 1000;
if (remain < 0) {
remain = 0;
$scope.time.end = true;
$scope.time.expired = true;
} else if (remain < 60) {
$scope.time.end = false;
$scope.time.expired = true;
} else {
$scope.time.end = false;
$scope.time.expired = false;
}
$scope.time.remaining = remain;
$scope.time.hours = Math.floor(remain / 3600);
$scope.time.minutes = Math.floor((remain % 3600) / 60);
$scope.time.seconds = Math.floor(remain % 60);
}
}
updTime();
$interval(updTime, 1000);
})

Some files were not shown because too many files have changed in this diff Show more