Compare commits

..

2 commits

Author SHA1 Message Date
9c50bc37d9 dex-update-passwd: Initial commit
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-19 11:59:56 +02:00
e23377329a admin: Can use GRPC to manage password 2024-04-19 11:58:59 +02:00
165 changed files with 5547 additions and 9177 deletions

View file

@ -20,7 +20,7 @@ steps:
- mkdir deploy
- name: build qa ui
image: node:23-alpine
image: node:21-alpine
commands:
- cd qa/ui
- npm install --network-timeout=100000
@ -31,15 +31,15 @@ steps:
image: golang:alpine
commands:
- apk --no-cache add build-base
- go vet -buildvcs=false -tags gitgo ./...
- go vet -buildvcs=false ./...
- go vet -v -buildvcs=false -tags gitgo ./...
- go vet -v -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
- go build -v -buildvcs=false -tags gitgo -o deploy/admin-gitgo-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/admin
- go build -v -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
@ -51,7 +51,7 @@ steps:
- 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
- go build -v -buildvcs=false -o deploy/checker-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/checker
environment:
CGO_ENABLED: 0
when:
@ -62,7 +62,7 @@ steps:
- 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
- go build -v -buildvcs=false -o deploy/evdist-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/evdist
environment:
CGO_ENABLED: 0
when:
@ -73,7 +73,7 @@ steps:
- 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
- go build -v -buildvcs=false -o deploy/generator-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/generator
environment:
CGO_ENABLED: 0
when:
@ -84,7 +84,7 @@ steps:
- 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
- go build -v -buildvcs=false -o deploy/receiver-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/receiver
environment:
CGO_ENABLED: 0
when:
@ -93,7 +93,7 @@ steps:
- master
- name: build frontend fic ui
image: node:23-alpine
image: node:21-alpine
commands:
- cd frontend/fic
- npm install --network-timeout=100000
@ -107,7 +107,7 @@ steps:
- 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
- go build -v -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
@ -120,12 +120,12 @@ steps:
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
- go build -buildvcs=false --tags checkupdate -v -o deploy/repochecker-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/repochecker
- go build -buildvcs=false -buildmode=plugin -v -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 -v -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 -v -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 -v -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 -v -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:
@ -135,7 +135,7 @@ steps:
- 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
- go build -v -buildvcs=false -o deploy/qa-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/qa
environment:
CGO_ENABLED: 0
when:
@ -378,14 +378,14 @@ steps:
image: golang:alpine
commands:
- apk --no-cache add git
- go get -d ./...
- go get -v -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
- go build -v -buildvcs=false -o deploy/admin-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/admin
environment:
CGO_ENABLED: 0
when:
@ -396,7 +396,7 @@ steps:
- 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
- go build -v -buildvcs=false -o deploy/checker-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/checker
environment:
CGO_ENABLED: 0
when:
@ -407,7 +407,7 @@ steps:
- 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
- go build -v -buildvcs=false -o deploy/evdist-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/evdist
environment:
CGO_ENABLED: 0
when:
@ -418,7 +418,7 @@ steps:
- 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
- go build -v -buildvcs=false -o deploy/generator-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/generator
environment:
CGO_ENABLED: 0
when:
@ -429,7 +429,7 @@ steps:
- 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
- go build -v -buildvcs=false -o deploy/receiver-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/receiver
environment:
CGO_ENABLED: 0
when:
@ -438,7 +438,7 @@ steps:
- master
- name: build frontend fic ui
image: node:23-alpine
image: node:21-alpine
commands:
- cd frontend/fic
- npm install --network-timeout=100000
@ -451,7 +451,7 @@ steps:
- 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
- go build -v -buildvcs=false -o deploy/dashboard-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/dashboard
environment:
CGO_ENABLED: 0
when:
@ -463,7 +463,7 @@ steps:
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 --tags checkupdate -v -o deploy/repochecker-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/repochecker
environment:
CGO_ENABLED: 0
when:
@ -475,7 +475,7 @@ steps:
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
- go build -buildvcs=false --tags checkupdate -v -o deploy/repochecker-darwin-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/repochecker
environment:
CGO_ENABLED: 0
GOOS: darwin
@ -486,7 +486,7 @@ steps:
- master
- name: build qa ui
image: node:23-alpine
image: node:21-alpine
commands:
- cd qa/ui
- npm install --network-timeout=100000
@ -500,7 +500,7 @@ steps:
- 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
- go build -v -buildvcs=false -o deploy/qa-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/qa
environment:
CGO_ENABLED: 0
when:

View file

@ -62,7 +62,7 @@ dependency_scanning:
get-deps:
stage: deps
image: golang:1-alpine
image: golang:alpine3.18
before_script:
- export GOPATH="$CI_PROJECT_DIR/.go"
- mkdir -p .go
@ -75,7 +75,7 @@ vet:
needs: ["build-qa-ui"]
dependencies:
- build-qa-ui
image: golang:1-alpine
image: golang:alpine3.18
before_script:
- export GOPATH="$CI_PROJECT_DIR/.go"
- mkdir -p .go

View file

@ -1,4 +1,4 @@
FROM golang:1-alpine AS gobuild
FROM golang:1-alpine as gobuild
RUN apk add --no-cache git
@ -20,7 +20,7 @@ RUN go get -d -v ./admin && \
go build -v -buildmode=plugin -o repochecker/videos-rules.so ./repochecker/videos
FROM alpine:3.21
FROM alpine:3.19
RUN apk add --no-cache \
ca-certificates \

View file

@ -1,4 +1,4 @@
FROM golang:1-alpine AS gobuild
FROM golang:1-alpine as gobuild
RUN apk add --no-cache git
@ -13,7 +13,7 @@ RUN go get -d -v ./checker && \
go build -v -buildvcs=false -o checker/checker ./checker
FROM alpine:3.21
FROM alpine:3.19
WORKDIR /srv

View file

@ -1,4 +1,4 @@
FROM golang:1-alpine AS gobuild
FROM golang:1-alpine as gobuild
RUN apk add --no-cache git
@ -13,7 +13,7 @@ RUN go get -d -v ./dashboard && \
go build -v -buildvcs=false -o dashboard/dashboard ./dashboard
FROM alpine:3.21
FROM alpine:3.19
EXPOSE 8082
@ -27,6 +27,6 @@ COPY --from=gobuild /go/src/srs.epita.fr/fic-server/dashboard/dashboard /srv/das
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 frontend/fic/static/img/ 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/

View file

@ -1,4 +1,4 @@
FROM alpine:3.21
FROM alpine:3.19
EXPOSE 67/udp
EXPOSE 69/udp

View file

@ -1,4 +1,4 @@
FROM golang:1-alpine AS gobuild
FROM golang:1-alpine as gobuild
RUN apk add --no-cache git
@ -12,7 +12,7 @@ RUN go get -d -v ./evdist && \
go build -v -buildvcs=false -o evdist/evdist ./evdist
FROM alpine:3.21
FROM alpine:3.19
WORKDIR /srv

View file

@ -1,4 +1,4 @@
FROM node:23-alpine AS nodebuild
FROM node:21-alpine as nodebuild
WORKDIR /ui

View file

@ -1,4 +1,4 @@
FROM golang:1-alpine AS gobuild
FROM golang:1-alpine as gobuild
RUN apk add --no-cache git
@ -13,7 +13,7 @@ RUN go get -d -v ./generator && \
go build -v -buildvcs=false -o generator/generator ./generator
FROM alpine:3.21
FROM alpine:3.19
WORKDIR /srv

View file

@ -1,4 +1,4 @@
FROM golang:1-alpine AS gobuild
FROM golang:1-alpine as gobuild
RUN apk add --no-cache git
@ -15,7 +15,7 @@ RUN go get -d -v ./admin && \
go build -v -o get-remote-files ./admin/get-remote-files
FROM alpine:3.21
FROM alpine:3.19
RUN apk add --no-cache \
ca-certificates

View file

@ -1,4 +1,4 @@
FROM node:23-alpine AS nodebuild
FROM node:21-alpine as nodebuild
WORKDIR /ui
@ -10,16 +10,9 @@ RUN npm install --network-timeout=100000 && \
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
ENV FIC_BASEURL=/ \
HOST_RECEIVER=receiver:8080 HOST_ADMIN=admin:8081 HOST_DASHBOARD=dashboard:8082 HOST_QA=qa:8083 \
PATH_FILES=/srv/FILES PATH_STARTINGBLOCK=/srv/STARTINGBLOCK PATH_STATIC=/srv/htdocs-frontend PATH_SETTINGS=/srv/SETTINGSDIST PATH_TEAMS=/srv/TEAMS
EXPOSE 80

View file

@ -1,4 +1,4 @@
FROM node:23-alpine AS nodebuild
FROM node:21-alpine as nodebuild
WORKDIR /ui
@ -8,7 +8,7 @@ RUN npm install --network-timeout=100000 && \
npm run build
FROM golang:1-alpine AS gobuild
FROM golang:1-alpine as gobuild
RUN apk add --no-cache git
@ -25,7 +25,7 @@ RUN go get -d -v ./qa && \
go build -v -buildvcs=false -o qa/qa ./qa
FROM alpine:3.21
FROM alpine:3.19
EXPOSE 8083

View file

@ -1,4 +1,4 @@
FROM golang:1-alpine AS gobuild
FROM golang:1-alpine as gobuild
RUN apk add --no-cache git
@ -13,7 +13,7 @@ RUN go get -d -v ./receiver && \
go build -v -buildvcs=false -o ./receiver/receiver ./receiver
FROM alpine:3.21
FROM alpine:3.19
EXPOSE 8080

View file

@ -1,4 +1,4 @@
FROM golang:1-alpine AS gobuild
FROM golang:1-alpine as gobuild
RUN apk add --no-cache git
@ -13,7 +13,7 @@ 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
FROM alpine:3.19
RUN apk add --no-cache openssl ca-certificates

View file

@ -1,4 +1,4 @@
FROM golang:1-alpine AS gobuild
FROM golang:1-alpine as gobuild
RUN apk add --no-cache git
@ -13,7 +13,7 @@ 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
FROM alpine:3.19
RUN apk add --no-cache openssl ca-certificates

View file

@ -1,4 +1,4 @@
FROM golang:1-alpine AS gobuild
FROM golang:1-alpine as gobuild
RUN apk add --no-cache git
@ -23,7 +23,7 @@ RUN go get -d -v ./repochecker && \
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
ADD 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

View file

@ -6,32 +6,6 @@ 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 :

View file

@ -1,11 +1,9 @@
package api
import (
"bytes"
"fmt"
"log"
"net/http"
"path"
"reflect"
"strconv"
"strings"
@ -20,7 +18,6 @@ import (
func declareGlobalExercicesRoutes(router *gin.RouterGroup) {
router.GET("/resolutions.json", exportResolutionMovies)
router.GET("/exercices_stats.json", getExercicesStats)
router.GET("/exercices_forge_bindings.json", getExercicesForgeLinks)
router.GET("/tags", listTags)
}
@ -35,18 +32,9 @@ func declareExercicesRoutes(router *gin.RouterGroup) {
apiExercicesRoutes.PATCH("", partUpdateExercice)
apiExercicesRoutes.DELETE("", deleteExercice)
apiExercicesRoutes.POST("/diff-sync", APIDiffExerciceWithRemote)
apiExercicesRoutes.GET("/history.json", getExerciceHistory)
apiExercicesRoutes.GET("/stats.json", getExerciceStats)
apiExercicesRoutes.GET("/tries", listTries)
apiTriesRoutes := apiExercicesRoutes.Group("/tries/:trid")
apiTriesRoutes.Use(ExerciceTryHandler)
apiTriesRoutes.GET("", getExerciceTry)
apiTriesRoutes.DELETE("", deleteExerciceTry)
apiExercicesRoutes.GET("/history.json", getExerciceHistory)
apiHistoryRoutes := apiExercicesRoutes.Group("/history.json")
apiHistoryRoutes.Use(AssigneeCookieHandler)
@ -74,8 +62,6 @@ func declareExercicesRoutes(router *gin.RouterGroup) {
apiFlagsRoutes.POST("/try", tryExerciceFlag)
apiFlagsRoutes.DELETE("/", deleteExerciceFlag)
apiFlagsRoutes.GET("/dependancies", showExerciceFlagDeps)
apiFlagsRoutes.GET("/statistics", showExerciceFlagStats)
apiFlagsRoutes.DELETE("/tries", deleteExerciceFlagTries)
apiFlagsRoutes.GET("/choices/", listFlagChoices)
apiFlagsChoicesRoutes := apiExercicesRoutes.Group("/choices/:cid")
apiFlagsChoicesRoutes.Use(FlagChoiceHandler)
@ -91,8 +77,6 @@ func declareExercicesRoutes(router *gin.RouterGroup) {
apiQuizRoutes.PUT("", updateExerciceQuiz)
apiQuizRoutes.DELETE("", deleteExerciceQuiz)
apiQuizRoutes.GET("/dependancies", showExerciceQuizDeps)
apiQuizRoutes.GET("/statistics", showExerciceQuizStats)
apiQuizRoutes.DELETE("/tries", deleteExerciceQuizTries)
apiExercicesRoutes.GET("/tags", listExerciceTags)
apiExercicesRoutes.POST("/tags", addExerciceTag)
@ -103,8 +87,8 @@ func declareExercicesRoutes(router *gin.RouterGroup) {
// Remote
router.GET("/remote/themes/:thid/exercices/:exid", sync.ApiGetRemoteExercice)
router.GET("/remote/themes/:thid/exercices/:exid/flags", sync.ApiGetRemoteExerciceFlags)
router.GET("/remote/themes/:thid/exercices/:exid/hints", sync.ApiGetRemoteExerciceHints)
router.GET("/remote/themes/:thid/exercices/:exid/flags", sync.ApiGetRemoteExerciceFlags)
}
type Exercice struct {
@ -142,7 +126,7 @@ func ExerciceHandler(c *gin.Context) {
c.Set("theme", theme)
} else {
c.Set("theme", &fic.StandaloneExercicesTheme)
c.Set("theme", &fic.Theme{Path: sync.StandaloneExercicesDirectory})
}
}
@ -488,71 +472,6 @@ func getExercicesStats(c *gin.Context) {
c.JSON(http.StatusOK, ret)
}
type themeForgeBinding struct {
ThemeName string `json:"name"`
ThemePath string `json:"path"`
ForgeLink string `json:"forge_link"`
Exercices []exerciceForgeBinding `json:"exercices"`
}
type exerciceForgeBinding struct {
ExerciceName string `json:"name"`
ExercicePath string `json:"path"`
ForgeLink string `json:"forge_link"`
}
func getExercicesForgeLinks(c *gin.Context) {
themes, err := fic.GetThemesExtended()
if err != nil {
log.Println("Unable to listThemes:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during themes listing."})
return
}
fli, ok := sync.GlobalImporter.(sync.ForgeLinkedImporter)
if !ok {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Current importer is not compatible with ForgeLinkedImporter"})
return
}
ret := []themeForgeBinding{}
for _, theme := range themes {
exercices, err := theme.GetExercices()
if err != nil {
log.Println("Unable to listExercices:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during exercice listing."})
return
}
var exlinks []exerciceForgeBinding
for _, exercice := range exercices {
var forgelink string
if u, _ := fli.GetExerciceLink(exercice); u != nil {
forgelink = u.String()
}
exlinks = append(exlinks, exerciceForgeBinding{
ExerciceName: exercice.Title,
ExercicePath: exercice.Path,
ForgeLink: forgelink,
})
}
var forgelink string
if u, _ := fli.GetThemeLink(theme); u != nil {
forgelink = u.String()
}
ret = append(ret, themeForgeBinding{
ThemeName: theme.Name,
ThemePath: theme.Path,
ForgeLink: forgelink,
Exercices: exlinks,
})
}
c.JSON(http.StatusOK, ret)
}
func AssigneeCookieHandler(c *gin.Context) {
myassignee, err := c.Cookie("myassignee")
if err != nil {
@ -933,60 +852,6 @@ func showExerciceFlagDeps(c *gin.Context) {
c.JSON(http.StatusOK, deps)
}
func showExerciceFlagStats(c *gin.Context) {
exercice := c.MustGet("exercice").(*fic.Exercice)
flag := c.MustGet("flag-key").(*fic.FlagKey)
history, err := exercice.GetHistory()
if err != nil {
log.Println("Unable to getExerciceHistory:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when retrieving exercice history"})
return
}
var completed int64
for _, hline := range history {
if hline["kind"].(string) == "flag_found" {
if int(*hline["secondary"].(*int64)) == flag.Id {
completed += 1
}
}
}
tries, err := flag.NbTries()
if err != nil {
log.Println("Unable to nbTries:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when retrieving flag tries"})
return
}
teams, err := flag.TeamsOnIt()
if err != nil {
log.Println("Unable to teamsOnIt:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when retrieving flag related teams"})
return
}
c.JSON(http.StatusOK, gin.H{
"completed": completed,
"tries": tries,
"teams": teams,
})
}
func deleteExerciceFlagTries(c *gin.Context) {
flag := c.MustGet("flag-key").(*fic.FlagKey)
err := flag.DeleteTries()
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
c.AbortWithStatusJSON(http.StatusOK, true)
}
func tryExerciceFlag(c *gin.Context) {
flag := c.MustGet("flag-key").(*fic.FlagKey)
@ -1030,23 +895,6 @@ func updateExerciceFlag(c *gin.Context) {
flag.Help = uk.Help
flag.IgnoreCase = uk.IgnoreCase
flag.Multiline = uk.Multiline
flag.ChoicesCost = uk.ChoicesCost
flag.BonusGain = uk.BonusGain
if uk.CaptureRe != nil && len(*uk.CaptureRe) > 0 {
if flag.CaptureRegexp != uk.CaptureRe && uk.Flag == "" {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Pour changer la capture_regexp, vous devez rentrer la réponse attendue à nouveau, car le flag doit être recalculé."})
return
}
flag.CaptureRegexp = uk.CaptureRe
} else {
if flag.CaptureRegexp != nil && uk.Flag == "" {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Pour changer la capture_regexp, vous devez rentrer la réponse attendue à nouveau, car le flag doit être recalculé."})
return
}
flag.CaptureRegexp = nil
}
if len(uk.Flag) > 0 {
var err error
flag.Checksum, err = flag.ComputeChecksum([]byte(uk.Flag))
@ -1058,6 +906,14 @@ func updateExerciceFlag(c *gin.Context) {
} else {
flag.Checksum = uk.Value
}
flag.ChoicesCost = uk.ChoicesCost
flag.BonusGain = uk.BonusGain
if uk.CaptureRe != nil && len(*uk.CaptureRe) > 0 {
flag.CaptureRegexp = uk.CaptureRe
} else {
flag.CaptureRegexp = nil
}
if _, err := flag.Update(); err != nil {
log.Println("Unable to updateExerciceFlag:", err.Error())
@ -1166,60 +1022,6 @@ func showExerciceQuizDeps(c *gin.Context) {
c.JSON(http.StatusOK, deps)
}
func showExerciceQuizStats(c *gin.Context) {
exercice := c.MustGet("exercice").(*fic.Exercice)
quiz := c.MustGet("flag-quiz").(*fic.MCQ)
history, err := exercice.GetHistory()
if err != nil {
log.Println("Unable to getExerciceHistory:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when retrieving exercice history"})
return
}
var completed int64
for _, hline := range history {
if hline["kind"].(string) == "mcq_found" {
if *hline["secondary"].(*int) == quiz.Id {
completed += 1
}
}
}
tries, err := quiz.NbTries()
if err != nil {
log.Println("Unable to nbTries:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when retrieving flag tries"})
return
}
teams, err := quiz.TeamsOnIt()
if err != nil {
log.Println("Unable to teamsOnIt:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when retrieving flag related teams"})
return
}
c.JSON(http.StatusOK, gin.H{
"completed": completed,
"tries": tries,
"teams": teams,
})
}
func deleteExerciceQuizTries(c *gin.Context) {
quiz := c.MustGet("flag-quiz").(*fic.MCQ)
err := quiz.DeleteTries()
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
c.AbortWithStatusJSON(http.StatusOK, true)
}
func updateExerciceQuiz(c *gin.Context) {
quiz := c.MustGet("flag-quiz").(*fic.MCQ)
@ -1352,398 +1154,3 @@ func updateExerciceTags(c *gin.Context) {
exercice.WipeTags()
addExerciceTag(c)
}
type syncDiff struct {
Field string `json:"field"`
Link string `json:"link"`
Before interface{} `json:"be"`
After interface{} `json:"af"`
}
func diffExerciceWithRemote(exercice *fic.Exercice, theme *fic.Theme) ([]syncDiff, error) {
var diffs []syncDiff
// Compare exercice attributes
thid := exercice.Path[:strings.Index(exercice.Path, "/")]
exid := exercice.Path[strings.Index(exercice.Path, "/")+1:]
exercice_remote, err := sync.GetRemoteExercice(thid, exid, theme)
if err != nil {
return nil, err
}
for _, field := range reflect.VisibleFields(reflect.TypeOf(*exercice)) {
if ((field.Name == "Image") && path.Base(reflect.ValueOf(*exercice_remote).FieldByName(field.Name).String()) != path.Base(reflect.ValueOf(*exercice).FieldByName(field.Name).String())) || ((field.Name == "Depend") && (((exercice_remote.Depend == nil || exercice.Depend == nil) && exercice.Depend != exercice_remote.Depend) || (exercice_remote.Depend != nil && exercice.Depend != nil && *exercice.Depend != *exercice_remote.Depend))) || (field.Name != "Image" && field.Name != "Depend" && !reflect.ValueOf(*exercice_remote).FieldByName(field.Name).Equal(reflect.ValueOf(*exercice).FieldByName(field.Name))) {
if !field.IsExported() || field.Name == "Id" || field.Name == "IdTheme" || field.Name == "IssueKind" || field.Name == "Coefficient" || field.Name == "BackgroundColor" {
continue
}
diffs = append(diffs, syncDiff{
Field: field.Name,
Link: fmt.Sprintf("exercices/%d", exercice.Id),
Before: reflect.ValueOf(*exercice).FieldByName(field.Name).Interface(),
After: reflect.ValueOf(*exercice_remote).FieldByName(field.Name).Interface(),
})
}
}
// Compare files
files, err := exercice.GetFiles()
if err != nil {
return nil, fmt.Errorf("Unable to GetFiles: %w", err)
}
files_remote, err := sync.GetRemoteExerciceFiles(thid, exid)
if err != nil {
return nil, fmt.Errorf("Unable to GetRemoteFiles: %w", err)
}
for i, file_remote := range files_remote {
if len(files) <= i {
diffs = append(diffs, syncDiff{
Field: fmt.Sprintf("files[%d]", i),
Link: fmt.Sprintf("exercices/%d", exercice.Id),
Before: nil,
After: file_remote,
})
continue
}
for _, field := range reflect.VisibleFields(reflect.TypeOf(*file_remote)) {
if !field.IsExported() || field.Name == "Id" || field.Name == "IdExercice" {
continue
}
if ((field.Name == "Path") && path.Base(reflect.ValueOf(*file_remote).FieldByName(field.Name).String()) != path.Base(reflect.ValueOf(*files[i]).FieldByName(field.Name).String())) || ((field.Name == "Checksum" || field.Name == "ChecksumShown") && !bytes.Equal(reflect.ValueOf(*file_remote).FieldByName(field.Name).Bytes(), reflect.ValueOf(*files[i]).FieldByName(field.Name).Bytes())) || (field.Name != "Checksum" && field.Name != "ChecksumShown" && field.Name != "Path" && !reflect.ValueOf(*file_remote).FieldByName(field.Name).Equal(reflect.ValueOf(*files[i]).FieldByName(field.Name))) {
diffs = append(diffs, syncDiff{
Field: fmt.Sprintf("files[%d].%s", i, field.Name),
Link: fmt.Sprintf("exercices/%d", exercice.Id),
Before: reflect.ValueOf(*files[i]).FieldByName(field.Name).Interface(),
After: reflect.ValueOf(*file_remote).FieldByName(field.Name).Interface(),
})
}
}
}
// Compare flags
flags, err := exercice.GetFlags()
if err != nil {
return nil, fmt.Errorf("Unable to GetFlags: %w", err)
}
flags_remote, err := sync.GetRemoteExerciceFlags(thid, exid)
if err != nil {
return nil, fmt.Errorf("Unable to GetRemoteFlags: %w", err)
}
var flags_not_found []interface{}
var flags_extra_found []interface{}
for i, flag_remote := range flags_remote {
if key_remote, ok := flag_remote.(*fic.FlagKey); ok {
found := false
for _, flag := range flags {
if key, ok := flag.(*fic.FlagKey); ok && (key.Label == key_remote.Label || key.Order == key_remote.Order) {
found = true
// Parse flag label
if len(key.Label) > 3 && key.Label[0] == '%' {
spl := strings.Split(key.Label, "%")
key.Label = strings.Join(spl[2:], "%")
}
for _, field := range reflect.VisibleFields(reflect.TypeOf(*key_remote)) {
if !field.IsExported() || field.Name == "Id" || field.Name == "IdExercice" {
continue
}
if (field.Name == "Checksum" && !bytes.Equal(key.Checksum, key_remote.Checksum)) || (field.Name == "CaptureRegexp" && ((key.CaptureRegexp == nil || key_remote.CaptureRegexp == nil) && key.CaptureRegexp != key_remote.CaptureRegexp) || (key.CaptureRegexp != nil && key_remote.CaptureRegexp != nil && *key.CaptureRegexp != *key_remote.CaptureRegexp)) || (field.Name != "Checksum" && field.Name != "CaptureRegexp" && !reflect.ValueOf(*key_remote).FieldByName(field.Name).Equal(reflect.ValueOf(*key).FieldByName(field.Name))) {
diffs = append(diffs, syncDiff{
Field: fmt.Sprintf("flags[%d].%s", i, field.Name),
Link: fmt.Sprintf("exercices/%d/flags#flag-%d", exercice.Id, key.Id),
Before: reflect.ValueOf(*key).FieldByName(field.Name).Interface(),
After: reflect.ValueOf(*key_remote).FieldByName(field.Name).Interface(),
})
}
}
break
}
}
if !found {
flags_not_found = append(flags_not_found, key_remote)
}
} else if mcq_remote, ok := flag_remote.(*fic.MCQ); ok {
found := false
for _, flag := range flags {
if mcq, ok := flag.(*fic.MCQ); ok && (mcq.Title == mcq_remote.Title || mcq.Order == mcq_remote.Order) {
found = true
for _, field := range reflect.VisibleFields(reflect.TypeOf(*mcq_remote)) {
if !field.IsExported() || field.Name == "Id" || field.Name == "IdExercice" {
continue
}
if field.Name == "Entries" {
var not_found []*fic.MCQ_entry
var extra_found []*fic.MCQ_entry
for i, entry_remote := range mcq_remote.Entries {
found := false
for j, entry := range mcq.Entries {
if entry.Label == entry_remote.Label {
for _, field := range reflect.VisibleFields(reflect.TypeOf(*entry_remote)) {
if field.Name == "Id" {
continue
}
if !reflect.ValueOf(*entry_remote).FieldByName(field.Name).Equal(reflect.ValueOf(*entry).FieldByName(field.Name)) {
diffs = append(diffs, syncDiff{
Field: fmt.Sprintf("flags[%d].entries[%d].%s", i, j, field.Name),
Link: fmt.Sprintf("exercices/%d/flags#quiz-%d", exercice.Id, mcq.Id),
Before: reflect.ValueOf(*mcq.Entries[j]).FieldByName(field.Name).Interface(),
After: reflect.ValueOf(*entry_remote).FieldByName(field.Name).Interface(),
})
}
}
found = true
break
}
}
if !found {
not_found = append(not_found, entry_remote)
}
}
for _, entry := range mcq.Entries {
found := false
for _, entry_remote := range mcq_remote.Entries {
if entry.Label == entry_remote.Label {
found = true
break
}
}
if !found {
extra_found = append(extra_found, entry)
}
}
if len(not_found) > 0 || len(extra_found) > 0 {
diffs = append(diffs, syncDiff{
Field: fmt.Sprintf("flags[%d].entries", i),
Link: fmt.Sprintf("exercices/%d/flags", exercice.Id),
Before: extra_found,
After: not_found,
})
}
} else if !reflect.ValueOf(*mcq_remote).FieldByName(field.Name).Equal(reflect.ValueOf(*mcq).FieldByName(field.Name)) {
diffs = append(diffs, syncDiff{
Field: fmt.Sprintf("flags[%d].%s", i, field.Name),
Link: fmt.Sprintf("exercices/%d/flags", exercice.Id),
Before: reflect.ValueOf(*mcq).FieldByName(field.Name).Interface(),
After: reflect.ValueOf(*mcq_remote).FieldByName(field.Name).Interface(),
})
}
}
break
}
}
if !found {
flags_not_found = append(flags_not_found, mcq_remote)
}
} else if label_remote, ok := flag_remote.(*fic.FlagLabel); ok {
found := false
for _, flag := range flags {
if label, ok := flag.(*fic.FlagLabel); ok && (label.Label == label_remote.Label || label.Order == label_remote.Order) {
found = true
for _, field := range reflect.VisibleFields(reflect.TypeOf(*label_remote)) {
if !field.IsExported() || field.Name == "Id" || field.Name == "IdExercice" {
continue
}
if !reflect.ValueOf(*label_remote).FieldByName(field.Name).Equal(reflect.ValueOf(*label).FieldByName(field.Name)) {
diffs = append(diffs, syncDiff{
Field: fmt.Sprintf("flags[%d].%s", i, field.Name),
Link: fmt.Sprintf("exercices/%d/flags#flag-%d", exercice.Id, label.Id),
Before: reflect.ValueOf(*label).FieldByName(field.Name).Interface(),
After: reflect.ValueOf(*label_remote).FieldByName(field.Name).Interface(),
})
}
}
break
}
}
if !found {
flags_not_found = append(flags_not_found, label_remote)
}
} else {
log.Printf("unknown flag type: %T", flag_remote)
}
}
for _, flag := range flags {
if key, ok := flag.(*fic.FlagKey); ok {
found := false
for _, flag_remote := range flags_remote {
if key_remote, ok := flag_remote.(*fic.FlagKey); ok && (key.Label == key_remote.Label || key.Order == key_remote.Order) {
found = true
break
}
}
if !found {
flags_extra_found = append(flags_extra_found, flag)
}
} else if mcq, ok := flag.(*fic.MCQ); ok {
found := false
for _, flag_remote := range flags_remote {
if mcq_remote, ok := flag_remote.(*fic.MCQ); ok && (mcq.Title == mcq_remote.Title || mcq.Order == mcq_remote.Order) {
found = true
break
}
}
if !found {
flags_extra_found = append(flags_extra_found, flag)
}
} else if label, ok := flag.(*fic.FlagLabel); ok {
found := false
for _, flag_remote := range flags_remote {
if label_remote, ok := flag_remote.(*fic.FlagLabel); ok && (label.Label == label_remote.Label || label.Order == label_remote.Order) {
found = true
break
}
}
if !found {
flags_extra_found = append(flags_extra_found, flag)
}
}
}
if len(flags_not_found) > 0 || len(flags_extra_found) > 0 {
diffs = append(diffs, syncDiff{
Field: "flags",
Link: fmt.Sprintf("exercices/%d/flags", exercice.Id),
Before: flags_extra_found,
After: flags_not_found,
})
}
// Compare hints
hints, err := exercice.GetHints()
if err != nil {
return nil, fmt.Errorf("Unable to GetHints: %w", err)
}
hints_remote, err := sync.GetRemoteExerciceHints(thid, exid)
if err != nil {
return nil, fmt.Errorf("Unable to GetRemoteHints: %w", err)
}
for i, hint_remote := range hints_remote {
hint_remote.Hint.TreatHintContent()
for _, field := range reflect.VisibleFields(reflect.TypeOf(*hint_remote.Hint)) {
if !field.IsExported() || field.Name == "Id" || field.Name == "IdExercice" {
continue
}
if len(hints) <= i {
diffs = append(diffs, syncDiff{
Field: fmt.Sprintf("hints[%d].%s", i, field.Name),
Link: fmt.Sprintf("exercices/%d", exercice.Id),
Before: nil,
After: reflect.ValueOf(*hint_remote.Hint).FieldByName(field.Name).Interface(),
})
} else if !reflect.ValueOf(*hint_remote.Hint).FieldByName(field.Name).Equal(reflect.ValueOf(*hints[i]).FieldByName(field.Name)) {
diffs = append(diffs, syncDiff{
Field: fmt.Sprintf("hints[%d].%s", i, field.Name),
Link: fmt.Sprintf("exercices/%d", exercice.Id),
Before: reflect.ValueOf(*hints[i]).FieldByName(field.Name).Interface(),
After: reflect.ValueOf(*hint_remote.Hint).FieldByName(field.Name).Interface(),
})
}
}
}
return diffs, err
}
func APIDiffExerciceWithRemote(c *gin.Context) {
theme := c.MustGet("theme").(*fic.Theme)
exercice := c.MustGet("exercice").(*fic.Exercice)
diffs, err := diffExerciceWithRemote(exercice, theme)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.JSON(http.StatusOK, diffs)
}
func listTries(c *gin.Context) {
exercice := c.MustGet("exercice").(*fic.Exercice)
tries, err := exercice.TriesList()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.JSON(http.StatusOK, tries)
}
func ExerciceTryHandler(c *gin.Context) {
trid, err := strconv.ParseInt(string(c.Params.ByName("trid")), 10, 32)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid try identifier"})
return
}
exercice := c.MustGet("exercice").(*fic.Exercice)
try, err := exercice.GetTry(trid)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Try not found"})
return
}
c.Set("try", try)
c.Next()
}
func getExerciceTry(c *gin.Context) {
try := c.MustGet("try").(*fic.ExerciceTry)
err := try.FillDetails()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.JSON(http.StatusOK, try)
}
func deleteExerciceTry(c *gin.Context) {
try := c.MustGet("try").(*fic.ExerciceTry)
_, err := try.Delete()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.Status(http.StatusNoContent)
}

View file

@ -3,12 +3,9 @@ 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"
@ -34,9 +31,6 @@ func declareExportRoutes(router *gin.RouterGroup) {
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 {
@ -62,41 +56,6 @@ func declareExportRoutes(router *gin.RouterGroup) {
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 {

View file

@ -5,7 +5,6 @@ import (
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"strconv"
@ -144,27 +143,12 @@ func listFiles(c *gin.Context) {
}
func clearFiles(c *gin.Context) {
err := os.RemoveAll(fic.FilesDir)
_, err := fic.ClearFiles()
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)
}

View file

@ -8,6 +8,7 @@ import (
"net/http"
"os"
"path"
"strings"
"text/template"
"unicode"
@ -33,13 +34,9 @@ func declarePasswordRoutes(router *gin.RouterGroup) {
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()
_, staticpassword := c.Request.URL.Query()["staticpassword"]
cfg, err := genDexConfig(staticpassword)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
@ -48,7 +45,8 @@ func declarePasswordRoutes(router *gin.RouterGroup) {
c.String(http.StatusOK, string(cfg))
})
router.POST("/dex.yaml", func(c *gin.Context) {
if dexcfg, err := genDexConfig(); err != nil {
_, staticpassword := c.Request.URL.Query()["staticpassword"]
if dexcfg, err := genDexConfig(staticpassword); 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 {
@ -139,8 +137,12 @@ storage:
file: /var/dex/dex.db
web:
http: 0.0.0.0:5556
{{ if .GRPC }}
grpc:
addr: 127.0.0.1:5557
{{ end }}
frontend:
issuer: {{ .Name }}
issuer: Challenge forensic
logoURL: {{ .LogoPath }}
dir: /srv/dex/web/
oauth2:
@ -214,16 +216,23 @@ type dexConfig struct {
Clients []dexConfigClient
Teams []*fic.Team
LogoPath string
GRPC bool
}
func genDexConfig() ([]byte, error) {
func genDexConfig(withTeams bool) ([]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
var teams []*fic.Team
var err error
// Should teams be included as static passwords, instead of being managed by GRPC
if withTeams {
teams, err = fic.GetTeams()
if err != nil {
return nil, err
}
}
b := bytes.NewBufferString("")
@ -241,7 +250,7 @@ func genDexConfig() ([]byte, error) {
logoPath := ""
if len(challengeInfo.MainLogo) > 0 {
logoPath = path.Join("../../files", "logo", path.Base(challengeInfo.MainLogo[len(challengeInfo.MainLogo)-1]))
logoPath = strings.Replace(challengeInfo.MainLogo[len(challengeInfo.MainLogo)-1], "$FILES$", fic.FilesDir, -1)
}
dexTmpl, err := template.New("dexcfg").Parse(dexcfgtpl)
@ -262,12 +271,20 @@ func genDexConfig() ([]byte, error) {
},
Teams: teams,
LogoPath: logoPath,
GRPC: !withTeams,
})
if err != nil {
return nil, fmt.Errorf("An error occurs during template execution: %w", err)
}
// Also generate team associations
if !withTeams {
teams, err = fic.GetTeams()
if err != nil {
return nil, err
}
}
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 {
@ -285,11 +302,6 @@ func genDexConfig() ([]byte, error) {
}
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 {
@ -299,7 +311,6 @@ func genDexPasswordTpl() ([]byte, error) {
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 {

View file

@ -41,27 +41,5 @@ func declareRepositoriesRoutes(router *gin.RouterGroup) {
}
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)
})
}
}

View file

@ -8,6 +8,7 @@ import (
"net/http"
"os"
"path"
"reflect"
"strconv"
"time"
@ -25,6 +26,7 @@ func declareSettingsRoutes(router *gin.RouterGroup) {
router.GET("/challenge.json", getChallengeInfo)
router.PUT("/challenge.json", saveChallengeInfo)
router.GET("/settings-ro.json", getROSettings)
router.GET("/settings.json", getSettings)
router.PUT("/settings.json", saveSettings)
router.DELETE("/settings.json", func(c *gin.Context) {
@ -96,6 +98,24 @@ func fullGeneration(c *gin.Context) {
})
}
func getROSettings(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,
})
}
func GetChallengeInfo() (*settings.ChallengeInfo, error) {
var challengeinfo string
var err error
@ -310,7 +330,6 @@ func ApplySettings(config *settings.Settings) {
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
@ -330,7 +349,6 @@ func ResetSettings() error {
WChoiceCurCoefficient: 1,
GlobalScoreCoefficient: 1,
DiscountedFactor: 0,
QuestionGainRatio: 0,
UnlockedStandaloneExercices: 10,
UnlockedStandaloneExercicesByThemeStepValidation: 1,
UnlockedStandaloneExercicesByStandaloneExerciceValidation: 0,

View file

@ -7,7 +7,6 @@ import (
"net/url"
"os"
"path"
"reflect"
"strings"
"srs.epita.fr/fic-server/admin/generation"
@ -18,8 +17,6 @@ import (
"go.uber.org/multierr"
)
var lastSyncError = ""
func flatifySyncErrors(errs error) (ret []string) {
for _, err := range multierr.Errors(errs) {
ret = append(ret, err.Error())
@ -30,37 +27,12 @@ func flatifySyncErrors(errs error) (ret []string) {
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)
}
})
@ -73,25 +45,27 @@ func declareSyncRoutes(router *gin.RouterGroup) {
})
// 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.GET("/deep", func(c *gin.Context) {
if sync.DeepSyncProgress == 0 {
c.AbortWithStatusJSON(http.StatusTooEarly, gin.H{"errmsg": "Pas de synchronisation en cours"})
return
}
c.JSON(http.StatusOK, gin.H{"progress": sync.DeepSyncProgress})
})
apiSyncRoutes.POST("/deep", func(c *gin.Context) {
c.JSON(http.StatusOK, sync.SyncDeep(sync.GlobalImporter))
})
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)) {
for _, se := range multierr.Errors(sync.SyncThemeDeep(sync.GlobalImporter, &fic.Theme{Path: sync.StandaloneExercicesDirectory}, 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) {
@ -105,7 +79,6 @@ func declareSyncRoutes(router *gin.RouterGroup) {
}
sync.EditDeepReport(&sync.SyncReport{Themes: map[string][]string{theme.Name: st}}, false)
sync.DeepSyncProgress = 255
lastSyncError = ""
c.JSON(http.StatusOK, st)
})
@ -117,7 +90,6 @@ func declareSyncRoutes(router *gin.RouterGroup) {
apiSyncRoutes.POST("/themes", func(c *gin.Context) {
_, errs := sync.SyncThemes(sync.GlobalImporter)
lastSyncError = ""
c.JSON(http.StatusOK, flatifySyncErrors(errs))
})
@ -241,7 +213,7 @@ func declareSyncExercicesRoutes(router *gin.RouterGroup) {
exceptions := sync.LoadExerciceException(sync.GlobalImporter, theme, exercice, nil)
c.JSON(http.StatusOK, flatifySyncErrors(sync.ImportExerciceFiles(sync.GlobalImporter, exercice, exceptions)))
c.JSON(http.StatusOK, flatifySyncErrors(sync.SyncExerciceFiles(sync.GlobalImporter, exercice, exceptions)))
})
apiSyncExercicesRoutes.POST("/fixurlid", func(c *gin.Context) {
exercice := c.MustGet("exercice").(*fic.Exercice)
@ -286,12 +258,10 @@ func autoSync(c *gin.Context) {
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()
@ -380,32 +350,3 @@ func autoSync(c *gin.Context) {
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)
}

View file

@ -1,9 +1,9 @@
package api
import (
"encoding/json"
"fmt"
"log"
"math/rand"
"net/http"
"strconv"
"strings"
@ -186,9 +186,6 @@ func declareTeamsRoutes(router *gin.RouterGroup) {
declareTeamsPasswordRoutes(apiTeamsRoutes)
declareTeamClaimsRoutes(apiTeamsRoutes)
declareTeamCertificateRoutes(apiTeamsRoutes)
// Import teams from cyberrange
router.POST("/cyberrange-teams.json", importTeamsFromCyberrange)
}
func TeamHandler(c *gin.Context) {
@ -295,11 +292,6 @@ func bindingTeams(c *gin.Context) {
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 {
@ -308,7 +300,7 @@ func allAssociations(c *gin.Context) {
return
}
var ret []teamAssociation
var ret []string
for _, team := range teams {
assocs, err := pki.GetTeamAssociations(TeamsDir, team.Id)
@ -318,84 +310,13 @@ func allAssociations(c *gin.Context) {
}
for _, a := range assocs {
ret = append(ret, teamAssociation{a, team.Id})
ret = append(ret, a)
}
}
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)
@ -405,7 +326,11 @@ func createTeam(c *gin.Context) {
}
if ut.Color == 0 {
ut.Color = fic.RandomColor().ToRGB()
ut.Color = fic.HSL{
H: rand.Float64(),
S: 1,
L: 0.5,
}.ToRGB()
}
team, err := fic.CreateTeam(strings.TrimSpace(ut.Name), ut.Color, ut.ExternalId)

View file

@ -5,9 +5,7 @@ import (
"log"
"net/http"
"path"
"reflect"
"strconv"
"strings"
"srs.epita.fr/fic-server/admin/sync"
"srs.epita.fr/fic-server/libfic"
@ -50,8 +48,6 @@ func declareThemesRoutes(router *gin.RouterGroup) {
apiThemesRoutes.PUT("", updateTheme)
apiThemesRoutes.DELETE("", deleteTheme)
apiThemesRoutes.POST("/diff-sync", APIDiffThemeWithRemote)
apiThemesRoutes.GET("/exercices_stats.json", getThemedExercicesStats)
declareExercicesRoutes(apiThemesRoutes)
@ -74,18 +70,14 @@ func ThemeHandler(c *gin.Context) {
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)
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()
}
@ -135,10 +127,6 @@ func listThemes(c *gin.Context) {
return
}
if has, _ := fic.HasStandaloneExercice(); has {
themes = append([]*fic.Theme{&fic.StandaloneExercicesTheme}, themes...)
}
c.JSON(http.StatusOK, themes)
}
@ -267,110 +255,3 @@ func getThemedExercicesStats(c *gin.Context) {
}
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)
}

View file

@ -73,7 +73,7 @@ func main() {
func treatDir(p string) {
var expath string
for _, f := range []string{"challenge.toml", "challenge.txt"} {
for _, f := range []string{"challenge.txt", "challenge.toml"} {
if sync.GlobalImporter.Exists(path.Join(p, f)) {
expath = p
break
@ -108,7 +108,7 @@ func treatExercice(expath string) {
paramsFiles, err := sync.GetExerciceFilesParams(sync.GlobalImporter, exercice)
if err != nil {
log.Printf("Unable to read challenge.toml %q: %s", expath, err.Error())
log.Printf("Unable to read challenge.txt %q: %s", expath, err.Error())
return
}

View file

@ -4,7 +4,7 @@ const indextpl = `<!DOCTYPE html>
<html ng-app="FICApp">
<head>
<meta charset="utf-8">
<title>{{ .title }} - Administration</title>
<title>Challenge Forensic - 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>
@ -86,7 +86,7 @@ const indextpl = `<!DOCTYPE html>
<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">
<img alt="FIC" src="img/fic.png" 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>
@ -95,7 +95,7 @@ const indextpl = `<!DOCTYPE html>
<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('/pki')}"><a class="nav-link" href="pki">PKI</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>
@ -124,7 +124,7 @@ const indextpl = `<!DOCTYPE html>
</div>
<span id="clock" class="navbar-text" ng-controller="CountdownController" ng-cloak>
<div style="pointer-events: none; position: absolute;">
<div style="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>

View file

@ -199,10 +199,10 @@ func main() {
}
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) {
// Update distributed challenge.json
if _, err := os.Stat(path.Join(settings.SettingsDir, settings.ChallengeFile)); os.IsNotExist(err) {
challengeinfo, err := sync.GetFileContent(sync.GlobalImporter, settings.ChallengeFile)
if err == nil {
if fd, err := os.Create(path.Join(settings.SettingsDir, settings.ChallengeFile)); err != nil {
log.Fatal("Unable to open SETTINGS/challenge.json:", err)
} else {
@ -213,10 +213,6 @@ func main() {
}
}
}
if ci, err := settings.ReadChallengeInfo(challengeinfo); err == nil {
fic.StandaloneExercicesTheme.Authors = ci.Authors
}
}
}

View file

@ -6,7 +6,6 @@ import (
"errors"
"log"
"net/http"
"os"
"path"
"strings"
"text/template"
@ -26,24 +25,10 @@ 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 {
} else if err = indexTmpl.Execute(b, map[string]string{"urlbase": path.Clean(path.Join(baseURL+"/", "nuke"))[:len(path.Clean(path.Join(baseURL+"/", "nuke")))-4]}); err != nil {
log.Fatal("An error occurs during template execution:", err)
} else {
indexPage = b.Bytes()
@ -65,9 +50,6 @@ func declareStaticRoutes(router *gin.RouterGroup, cfg *settings.Settings, baseUR
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)
})
@ -80,9 +62,6 @@ func declareStaticRoutes(router *gin.RouterGroup, cfg *settings.Settings, baseUR
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)
})
@ -125,20 +104,8 @@ func declareStaticRoutes(router *gin.RouterGroup, cfg *settings.Settings, baseUR
})
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)
// TODO: handle .gz file here
http.ServeFile(c.Writer, c.Request, path.Join(fic.FilesDir, strings.TrimPrefix(c.Request.URL.Path, path.Join(baseURL, "files"))))
})
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"))))

File diff suppressed because it is too large Load diff

View file

@ -81,22 +81,6 @@ angular.module("FICApp")
});
}
}
}])
.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")

View file

@ -1,86 +0,0 @@
<div class="d-flex justify-content-between align-items-center">
<h2>
Authentification
</h2>
<div>
<div class="btn-group mr-1" role="group">
<button type="button" ng-click="generateHtpasswd()" class="btn btn-sm btn-secondary"><span class="glyphicon glyphicon-save-file" aria-hidden="true"></span> Générer <code>fichtpasswd</code></button>
<button type="button" ng-click="removeHtpasswd()" class="btn btn-sm btn-danger"><span class="glyphicon glyphicon-trash" aria-hidden="true"></span></button>
</div>
</div>
</div>
<div ng-controller="OAuthController">
<div class="d-flex justify-content-between align-items-center">
<h3>
OAuth 2
<span class="badge badge-success" ng-if="oauth_status.secret_defined">Actif</span>
<span class="badge badge-danger" ng-if="!oauth_status.secret_defined">Non configuré</span>
</h3>
<div>
<button type="button" ng-click="genDexCfg()" class="btn btn-success mr-2"><span class="glyphicon glyphicon-refresh" aria-hidden="true"></span> DexIdP</button>
</div>
</div>
</div>
<hr>
<div ng-controller="PKIController">
<div class="d-flex justify-content-between align-items-center">
<h3>
Autorité de certification
<span class="badge badge-success" ng-if="ca.version">Générée</span>
<span class="badge badge-danger" ng-if="!ca.version">Introuvable</span>
</h3>
<div>
<a
class="btn btn-primary"
href="/pki"
>
<span class="glyphicon glyphicon-certificate" aria-hidden="true"></span>
Gérer la PKI
</a>
</div>
</div>
<div class="alert alert-info" ng-if="!ca.version">
<strong>Aucune CA n'a été générée pour le moment.</strong>
</div>
<dl ng-if="ca.version">
<ng-repeat ng-repeat="(k, v) in ca" class="row">
<dt class="col-3 text-right">{{ k }}</dt>
<dd class="col-9" ng-if="v.CommonName">/CN={{ v.CommonName }}/OU={{ v.OrganizationalUnit }}/O={{ v.Organization }}/L={{ v.Locality }}/P={{ v.Province }}/C={{ v.Country }}/</dd>
<dd class="col-9" ng-if="!v.CommonName">{{ v }}</dd>
</ng-repeat>
</dl>
</div>
<hr>
<div class="mb-4" ng-controller="AllTeamAssociationsController">
<div class="d-flex justify-content-between align-items-center">
<h3>
Association utilisateurs et équipes
</h3>
<div>
</div>
</div>
<table class="table table-sm table-hover" ng-controller="TeamsListController" >
<tr>
<th class="text-right">Utilisateur</th>
<th></th>
<th>Équipe</th>
</tr>
<tr ng-repeat="association in allAssociations">
<td class="text-right">{{ association.association }}</td>
<td class="text-center">&#11020;</td>
<td ng-repeat="team in teams" ng-if="team.id == association.team_id">
<a ng-href="teams/{{ team.id }}">
{{ team.name }}
</a>
</td>
</tr>
</table>
</div>

View file

@ -73,29 +73,12 @@
</div>
<div class="col-4">
<div ng-controller="ExerciceFlagDepsController" ng-init="init(flag)">
<strong>Dépendances&nbsp;:</strong>
Dépendances&nbsp;:
<ul ng-if="deps.length > 0">
<dependancy ng-repeat="dep in deps" dep="dep"></dependancy>
</ul>
<span ng-if="deps.length == 0"> sans</span>
</div>
<hr>
<div ng-controller="ExerciceFlagStatsController" ng-init="init(flag)">
<strong>Statistiques</strong>
<ul>
<li>ID: {{ flag.id }}</li>
<li>Validés: {{ stats["completed"] }}</li>
<li>
Tentés: {{ stats["tries"] }}
<button type="button" ng-click="deleteTries()" class="btn btn-sm btn-danger" ng-if="stats['tries'] > 0"><span class="glyphicon glyphicon-trash" aria-hidden="true"></span></button>
</li>
<li>
Équipes:
<span ng-if="stats['teams'].length == 0">aucune</span>
<team-link ng-repeat="team in stats['teams']" id-team="team"></team-link>
</li>
</ul>
</div>
</div>
<div class="col-4" ng-controller="ExerciceFlagChoicesController">
<div class="btn-toolbar justify-content-end mb-2" role="toolbar">
@ -185,23 +168,6 @@
<dependancy ng-repeat="dep in deps" dep="dep"></dependancy>
</ul>
<span ng-if="deps.length == 0"> sans</span>
<hr>
<div ng-controller="ExerciceMCQStatsController" ng-init="init(q)">
<strong>Statistiques</strong>
<ul>
<li>ID: {{ q.id }}</li>
<li>Validés: {{ stats["completed"] }}</li>
<li>
Tentés: {{ stats["tries"] }}
<button type="button" ng-click="deleteTries()" class="btn btn-sm btn-danger" ng-if="stats['tries'] > 0"><span class="glyphicon glyphicon-trash" aria-hidden="true"></span></button>
</li>
<li>
Équipes:
<span ng-if="stats['teams'].length == 0">aucune</span>
<team-link ng-repeat="team in stats['teams']" id-team="team"></team-link>
</li>
</ul>
</div>
</div>
</form>
</div>

View file

@ -10,27 +10,11 @@
<div class="ml-auto d-flex flex-row-reverse text-nowrap">
<a href="exercices/{{exercice.id}}/resolution" ng-disabled="!exercice.videoURI" class="ml-2 btn btn-sm btn-info"><span class="glyphicon glyphicon-facetime-video" aria-hidden="true"></span> Vidéo</a>
<a href="exercices/{{exercice.id}}/flags" class="ml-2 btn btn-sm btn-success"><span class="glyphicon glyphicon-flag" aria-hidden="true"></span> Flags</a>
<div class="btn-group ml-2" role="group">
<button type="button" ng-click="syncExo()" ng-class="{'disabled': inSync}" class="btn btn-sm btn-light"><span class="glyphicon glyphicon-refresh" aria-hidden="true"></span> Synchroniser</button>
<button type="button" ng-click="checkExoSync()" ng-class="{'disabled': inSync}" class="btn btn-sm btn-light" title="Exporter l'exercice actuel"><span class="glyphicon glyphicon-thumbs-up" aria-hidden="true"></span></button>
</div>
<button type="button" ng-click="syncExo()" ng-class="{'disabled': inSync}" class="ml-2 btn btn-sm btn-light"><span class="glyphicon glyphicon-refresh" aria-hidden="true"></span> Synchroniser</button>
<a href="{{exercice.forge_link}}" target="_blank" class="ml-2 btn btn-sm btn-dark" ng-if="exercice.forge_link"><span class="glyphicon glyphicon-folder-open" aria-hidden="true"></span> Voir sur la forge</a>
</div>
</div>
<div ng-if="diff">
<h3>Différences par rapport au dépôt</h3>
<div ng-repeat="diffline in diff" class="row">
<a ng-href="{{ diffline.link }}" class="col-2 d-flex justify-content-end align-items-center text-monospace" title="{{ diffline.field }}">{{ diffline.field }}</a>
<div class="col">
<div class="text-danger"><span class="text-monospace">-</span>{{ diffline.be }}</div>
<div class="text-success"><span class="text-monospace">+</span>{{ diffline.af }}</div>
</div>
</div>
</div>
<hr ng-if="diff" class="my-3">
<div class="row mb-5">
<form class="col-md-8" ng-submit="saveExercice()">
@ -116,7 +100,7 @@
<form ng-submit="saveFile()" class="list-group-item bg-light text-dark" ng-repeat="file in files">
<div class="row form-group">
<input type="text" ng-model="file.name" class="col form-control form-control-sm" placeholder="Nom de fichier">
<a href="../files{{file.path}}" target="_self" class="btn btn-sm btn-secondary col-auto"><span class="glyphicon glyphicon-download" aria-hidden="true"></span></a>
<a href="../files{{file.path}}" class="btn btn-sm btn-secondary col-auto"><span class="glyphicon glyphicon-download" aria-hidden="true"></span></a>
<button type="submit" class="btn btn-sm btn-success col-auto"><span class="glyphicon glyphicon-ok" aria-hidden="true"></span></button><br>
<button type="button" ng-click="deleteFile()" class="btn btn-sm btn-danger col-auto"><span class="glyphicon glyphicon-remove" aria-hidden="true"></span></button>
</div>
@ -347,8 +331,7 @@
<a href="exercices/{{ row.primary }}#quizz-{{ row.secondary }}" ng-if="row.kind == 'mcq_found'">{{ row.secondary_title }}</a>
<a href="exercices/{{ row.primary }}#hint-{{ row.secondary }}" ng-if="row.kind == 'hint'">{{ row.secondary_title }}</a>
</span>
<span ng-if="!row.secondary_title && row.secondary && row.kind != 'solved' && row.kind != 'tries'">: {{ row.secondary }}</span>
<span ng-if="!row.secondary_title && row.secondary && row.kind == 'tries'" ng-controller="SearchTryController"><br><span ng-repeat="line in tr.details"><span ng-if="!$first">, </span>{{ line.kind }}<span ng-if="line.related">#{{ line.related }}</span></span></span>
<span ng-if="!row.secondary_title && row.secondary && row.kind != 'solved'">: {{ row.secondary }}</span>
</td>
<td style="vertical-align: middle; padding: 0; background-color: {{ row.team_color }}" ng-show="logged">
<button type="button" data-toggle="modal" data-target="#updHistory" ng-if="row.kind != 'flag_found' && row.kind != 'tries' && row.kind != 'mcq_found'" data-idteam="{{ row.team_id }}" data-kind="{{ row.kind }}" data-time="{{ row.time }}" data-secondary="{{ row.secondary }}" data-coeff="{{ row.coefficient }}" class="float-right btn btn-sm btn-info"><span class="glyphicon glyphicon-edit" aria-hidden="true"></span></button>

View file

@ -1,12 +0,0 @@
<h1>Accès rapide aux exercices</h1>
<div ng-repeat="theme in forge_links">
<h2>
<a ng-href="{{ theme.forge_link }}" target="_blank">{{ theme.name }}</a>&nbsp;: <span class="text-monospace">{{ theme.path }}</span>
</h2>
<ul>
<li ng-repeat="exercice in theme.exercices">
<a ng-href="{{ exercice.forge_link }}" target="_blank">{{ exercice.name }}</a>&nbsp;: <span class="text-monospace">{{ exercice.path }}</span>
</li>
</ul>
</div>

View file

@ -4,7 +4,6 @@
<small class="text-muted" ng-if="errzip > 0"><span class="glyphicon glyphicon-exclamation-sign"></span> <ng-pluralize count="errzip" when="{'one': '{} décompression problématique', 'other': '{} décompressions problématiques'}"></ng-pluralize></small>
<button type="button" ng-click="checksumAll()" class="float-right btn btn-sm" ng-class="{'btn-secondary': errfnd === null, 'btn-success': errfnd === 0, 'btn-danger': errfnd > 0}"><span class="glyphicon glyphicon-flash" aria-hidden="true"></span> Vérifier les fichiers</button>
<button type="button" ng-click="gunzipFiles()" class="float-right btn btn-sm mx-1" ng-class="{'btn-secondary': errzip === null, 'btn-success': errzip === 0, 'btn-danger': errzip > 0}" title="Décompresse tous les fichiers compressés afin d'afficher la bonne taille au moment du téléchargement"><span class="glyphicon glyphicon-compressed" aria-hidden="true"></span> Gunzip</button>
<button type="button" ng-click="clearFilesDir()" class="float-right btn btn-danger btn-sm mx-1" title="Supprime l'arborescence des fichiers"><span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Nuke files</button>
</h2>
<p><input type="search" class="form-control" placeholder="Search" ng-model="query" autofocus></p>
@ -30,13 +29,13 @@
<div class="spinner-border spinner-border-sm" role="status" ng-if="file.gunzipWIP"></div>
</button>
</td>
<td ng-repeat="field in fields" class="text-truncate" style="max-width: 30vw" title="{{ file[field] }}">
<td ng-repeat="field in fields">
{{ file[field] }}
<span ng-if="field == 'id' && file.err !== undefined && file.err !== true" title="{{ file.err }}" class="glyphicon glyphicon-exclamation-sign"></span>
</td>
<td style="max-width: 100px">
<div class="text-truncate" title="{{ file.checksum | bto16 }}">{{ file.checksum | bto16 }}</div>
<div class="text-truncate" ng-if="file.checksum_shown" title="{{ file.checksum_shown | bto16 }}">{{ file.checksum_shown | bto16 }}</div>
<td>
{{ file.checksum | bto16 }}
<div ng-if="file.checksum_shown">{{ file.checksum_shown | bto16 }}</div>
</td>
</tr>
</tbody>

View file

@ -9,20 +9,16 @@
<tr>
<th>Chemin</th>
<th>Branche</th>
<th>Commit <span class="text-muted">Plus récent</span></th>
<th>Commit</th>
<th>Plus récent</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="repository in repositories">
<td>{{ repository.path }}</td>
<td>{{ repository.branch }}</td>
<td>
{{ repository.hash }}<br>
<repository-uptodate repository="repository" />
</td>
<td>
<button type="button" ng-click="deleteRepository(repository)" class="btn btn-sm btn-danger"><span class="glyphicon glyphicon-trash" aria-hidden="true"></span></button>
</td>
<td>{{ repository.hash }}</td>
<td><repository-uptodate repository="repository" /></td>
</tr>
</tbody>
</table>

View file

@ -35,7 +35,7 @@
<label for="startTime" class="col-sm-3 col-form-label col-form-label-sm" ng-class="{'text-primary font-weight-bold': config.start != dist_config.start}">Début du challenge</label>
<div class="col-sm-9">
<div class="input-group">
<input type="datetime-local" class="form-control form-control-sm" id="startTime" ng-model="config.start" ng-change="durationChange()" ng-class="{'border-primary': config.start != dist_config.start}">
<input type="datetime-local" class="form-control form-control-sm" id="startTime" ng-model="config.start" ng-class="{'border-primary': config.start != dist_config.start}">
<div class="input-group-append">
<button ng-click="launchChallenge()" class="btn btn-sm btn-secondary" type="button"><span class="glyphicon glyphicon-play" aria-hidden="true"></span> Lancer le challenge</button>
</div>
@ -46,14 +46,14 @@
<div class="form-group row">
<label for="endTime" class="col-sm-3 col-form-label col-form-label-sm" ng-class="{'text-primary font-weight-bold': config.end != dist_config.end}">Fin du challenge</label>
<div class="col-sm-6">
<input type="datetime-local" class="form-control form-control-sm" id="endTime" ng-model="config.end" ng-change="durationChange(true)" ng-class="{'border-primary': config.end != dist_config.end}">
<input type="datetime-local" class="form-control form-control-sm" id="endTime" ng-model="config.end" ng-class="{'border-primary': config.end != dist_config.end}">
</div>
<div class="col-sm-1 text-right">
<label for="duration" class="col-form-label col-form-label-sm">Durée</label>
</div>
<div class="col-sm-2">
<div class="input-group input-group-sm">
<input type="number" class="form-control form-control-sm" id="duration" ng-model="duration" ng-change="durationChange()" integer>
<input type="text" class="form-control form-control-sm" id="duration" ng-model="duration" integer>
<div class="input-group-append">
<span class="input-group-text">min</span>
</div>
@ -64,60 +64,46 @@
<hr>
<div class="form-group row">
<div class="col-sm row">
<label for="globalScoreCoefficient" class="col-form-label col-form-label-sm" ng-class="{'text-primary': config.globalScoreCoefficient != dist_config.globalScoreCoefficient}"><strong>Coefficients</strong></label>
<div class="col">
<input type="text" class="form-control form-control-sm" id="globalScoreCoefficient" ng-model="config.globalScoreCoefficient" float title="Coefficient multiplicateur global du score final (le coefficient est appliqué dans la fonction et vaut pour tout le challenge, présent/passé/futur, sans effet de bord)" ng-class="{'border-primary': config.globalScoreCoefficient != dist_config.globalScoreCoefficient}">
</div>
<label for="globalScoreCoefficient" class="col-sm-2 col-form-label col-form-label-sm" ng-class="{'text-primary': config.globalScoreCoefficient != dist_config.globalScoreCoefficient}"><strong>Coefficients</strong></label>
<div class="col-sm-1">
<input type="text" class="form-control form-control-sm" id="globalScoreCoefficient" ng-model="config.globalScoreCoefficient" float title="Coefficient multiplicateur global du score final (le coefficient est appliqué dans la fonction et vaut pour tout le challenge, présent/passé/futur, sans effet de bord)" ng-class="{'border-primary': config.globalScoreCoefficient != dist_config.globalScoreCoefficient}">
</div>
<div class="col-sm row">
<label for="hintcoefficient" class="col-form-label col-form-label-sm text-right" ng-class="{'text-primary font-weight-bold': config.hintCurrentCoefficient != dist_config.hintCurrentCoefficient}">incides</label>
<div class="col">
<input type="text" class="form-control form-control-sm" id="hintcoefficient" ng-model="config.hintCurrentCoefficient" float title="Coefficient multiplicateur temporaire du nombre de points que fait perdre un indice (le coefficient est enregistré au moment où l'équipe demande un indice, ce n'est pas global)" ng-class="{'border-primary': config.hintCurrentCoefficient != dist_config.hintCurrentCoefficient}">
</div>
<label for="hintcoefficient" class="col-sm-2 col-form-label col-form-label-sm text-right" ng-class="{'text-primary font-weight-bold': config.hintCurrentCoefficient != dist_config.hintCurrentCoefficient}">incides</label>
<div class="col-sm-1">
<input type="text" class="form-control form-control-sm" id="hintcoefficient" ng-model="config.hintCurrentCoefficient" float title="Coefficient multiplicateur temporaire du nombre de points que fait perdre un indice (le coefficient est enregistré au moment où l'équipe demande un indice, ce n'est pas global)" ng-class="{'border-primary': config.hintCurrentCoefficient != dist_config.hintCurrentCoefficient}">
</div>
<div class="col-sm row">
<label for="wchoicescoefficient" class="col-form-label col-form-label-sm text-right" ng-class="{'text-primary font-weight-bold': config.wchoiceCurrentCoefficient != dist_config.wchoiceCurrentCoefficient}">WChoices</label>
<div class="col">
<input type="text" class="form-control form-control-sm" id="wchoicescoefficient" ng-model="config.wchoiceCurrentCoefficient" float title="Coefficient multiplicateur temporaire du nombre de points que fait perdre une demande de liste de choix (le coefficient est enregistré au moment où l'équipe demande la liste de choix, ce n'est pas global)" ng-class="{'border-primary': config.wchoiceCurrentCoefficient != dist_config.wchoiceCurrentCoefficient}">
</div>
<label for="wchoicescoefficient" class="col-sm-2 col-form-label col-form-label-sm text-right" ng-class="{'text-primary font-weight-bold': config.wchoiceCurrentCoefficient != dist_config.wchoiceCurrentCoefficient}">WChoices</label>
<div class="col-sm-1">
<input type="text" class="form-control form-control-sm" id="wchoicescoefficient" ng-model="config.wchoiceCurrentCoefficient" float title="Coefficient multiplicateur temporaire du nombre de points que fait perdre une demande de liste de choix (le coefficient est enregistré au moment où l'équipe demande la liste de choix, ce n'est pas global)" ng-class="{'border-primary': config.wchoiceCurrentCoefficient != dist_config.wchoiceCurrentCoefficient}">
</div>
<div class="col-sm row">
<label for="exercicecurcoefficient" class="col-form-label col-form-label-sm text-right" ng-class="{'text-primary font-weight-bold': config.exerciceCurrentCoefficient != dist_config.exerciceCurrentCoefficient}">défis</label>
<div class="col">
<input type="text" class="form-control form-control-sm" id="exercicecurcoefficient" ng-model="config.exerciceCurrentCoefficient" float title="Coefficient multiplicateur temporaire du nombre de points que fait gagner un exercice validé (le coefficient est enregistré au moment où l'équipe valide l'exercice, ce n'est pas global)" ng-class="{'border-primary': config.exerciceCurrentCoefficient != dist_config.exerciceCurrentCoefficient}">
</div>
<label for="exercicecurcoefficient" class="col-sm-2 col-form-label col-form-label-sm text-right" ng-class="{'text-primary font-weight-bold': config.exerciceCurrentCoefficient != dist_config.exerciceCurrentCoefficient}">défis</label>
<div class="col-sm-1">
<input type="text" class="form-control form-control-sm" id="exercicecurcoefficient" ng-model="config.exerciceCurrentCoefficient" float title="Coefficient multiplicateur temporaire du nombre de points que fait gagner un exercice validé (le coefficient est enregistré au moment où l'équipe valide l'exercice, ce n'est pas global)" ng-class="{'border-primary': config.exerciceCurrentCoefficient != dist_config.exerciceCurrentCoefficient}">
</div>
</div>
<div class="form-group row" title="Attribuer ce pourcentage de points bonus supplémentaire à la première équipe qui valide un exercice">
<div class="form-group row">
<div class="col-sm row">
<label for="firstBlood" class="col-sm-8 col-form-label col-form-label-sm" ng-class="{'text-primary font-weight-bold': config.firstBlood != dist_config.firstBlood}">Premier sang</label>
<label for="firstBlood" class="col-sm-8 col-form-label col-form-label-sm" ng-class="{'text-primary font-weight-bold': config.firstBlood != dist_config.firstBlood}">Bonus premier sang</label>
<div class="col-sm-4">
<input type="text" class="form-control form-control-sm" id="firstBlood" ng-model="config.firstBlood" float ng-class="{'border-primary': config.firstBlood != dist_config.firstBlood}">
</div>
</div>
<div class="col-sm row" title="Pour chaque validation supplémentaire d'un exercice donné, on retire ce pourcentage de points à l'exercice. Les points rapportés par un exercice sont alors dynamiques : ils baissent pour toutes les équipes y compris celles ayant validé cet exercice il y a longtemps.">
<label for="discountFactor" class="col-sm-8 col-form-label col-form-label-sm text-truncate" ng-class="{'text-primary font-weight-bold': config.discountedFactor != dist_config.discountedFactor}">Décote exercices</label>
<div class="col-sm row">
<label for="discountFactor" class="col-sm-8 col-form-label col-form-label-sm" ng-class="{'text-primary font-weight-bold': config.discountedFactor != dist_config.discountedFactor}">Décote des exercices</label>
<div class="col-sm-4">
<input type="text" class="form-control form-control-sm" id="discountFactor" ng-model="config.discountedFactor" float ng-class="{'border-primary': config.discountedFactor != dist_config.discountedFactor}">
</div>
</div>
<div class="col-sm row" title="Coefficient de base retiré pour chaque soumission invalide au delà de 10 soumissions">
<label for="submissionCostBase" class="col-sm-8 col-form-label col-form-label-sm text-right" ng-class="{'text-primary font-weight-bold': config.submissionCostBase != dist_config.submissionCostBase}">Coût tentative</label>
<div class="col-sm row">
<label for="submissionCostBase" class="col-sm-8 col-form-label col-form-label-sm text-right" ng-class="{'text-primary font-weight-bold': config.submissionCostBase != dist_config.submissionCostBase}">Coût de base tentative</label>
<div class="col-sm-4">
<input type="text" class="form-control form-control-sm" id="submissionCostBase" ng-model="config.submissionCostBase" float ng-class="{'border-primary': config.submissionCostBase != dist_config.submissionCostBase}">
</div>
</div>
<div class="col-sm row" title="Accorder des points aux exercices partiellement résolu, par questions validée. Ce champ est le pourcentage de points que peut rapporter la complétion de toutes les questions d'un exercice. Par exemple avec 25%, un exercice avec 10 questions, chaque question validée rapportera GAIN * 25% / 10">
<label for="questionGainRatio" class="col-sm-8 col-form-label col-form-label-sm text-right text-truncate" ng-class="{'text-primary font-weight-bold': config.questionGainRatio != dist_config.questionGainRatio}">Gain par questions</label>
<div class="col-sm-4">
<input type="text" class="form-control form-control-sm" id="questionGainRatio" ng-model="config.questionGainRatio" float ng-class="{'border-primary': config.questionGainRatio != dist_config.questionGainRatio}">
</div>
</div>
</div>
<hr>
@ -320,7 +306,7 @@
</div>
</form>
<div class="card my-3">
<form ng-submit="addDelegatedQA()" class="card my-3">
<div class="card-header">
<h3>Managers QA</h3>
</div>
@ -332,23 +318,22 @@
<span class="glyphicon glyphicon-remove" aria-hidden="true"></span>
</button>
</li>
<li class="row">
<div class="col" ng-controller="AllTeamAssociationsController">
<select class="form-control form-control-sm" ng-model="newdqa">
<option ng-repeat="(i,m) in allAssociations" ng-value="m">{{ m }}</option>
</select>
</div>
<div class="col input-group">
<input type="text" class="form-control form-control-sm" ng-model="newdqa" placeholder="Nouveau manager QA">
<span class="input-group-append">
<button class="btn btn-sm btn-success" ng-disabled="!newdqa.length"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span></button>
</span>
</div>
</li>
</ul>
<form class="row" ng-controller="AllTeamAssociationsController" ng-submit="addDelegatedQA()">
<div class="col">
<select class="form-control form-control-sm" ng-model="newdqa">
<option ng-selected="newdqa == m.association" ng-repeat="(i,m) in allAssociations" ng-value="m.association">{{ m.association }}</option>
</select>
</div>
<div class="col input-group">
<input type="text" class="form-control form-control-sm" ng-model="newdqa" placeholder="Nouveau manager QA">
<span class="input-group-append">
<button class="btn btn-sm btn-success" ng-disabled="!newdqa.length"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span></button>
</span>
</div>
</form>
</div>
</div>
</form>
<form ng-submit="saveChallengeInfo()" class="card my-3">
<div class="card-header">

View file

@ -22,7 +22,7 @@
<div class="badge badge-success align-self-center" ng-if="syncReport" title="{{ syncReport._updated[syncReport._updated.length-1] }}">
Dernier import&nbsp;: {{ syncReport._updated[syncReport._updated.length-1] | date:"medium" }}
</div>
<a ng-if="syncStatus['sync-type'] === 'GitImporter'" href="repositories" class="btn btn-secondary">
<a ng-if="configro['sync-type'] === 'GitImporter'" href="repositories" class="btn btn-secondary">
Voir les dépôts
</a>
</div>
@ -32,25 +32,17 @@
<div class="card-body">
<dl class="row">
<dt class="col-2">Type</dt>
<dd class="col-10" ng-bind="syncStatus['sync-type']"></dd>
<dd class="col-10" ng-bind="configro['sync-type']"></dd>
<dt class="col-2">Synchronisation</dt>
<dd class="col-10" title="{{ syncStatus['sync'] }}" ng-bind="syncStatus.sync"></dd>
<dt class="col-2" ng-if="syncStatus['sync-id']">ID</dt>
<dd class="col-10" ng-if="syncStatus['sync-id']">
{{ syncStatus['sync-id'] }}
<button ng-if="syncStatus['sync-type'] === 'GitImporter'" type="button" class="btn btn-sm btn-dark" ng-click="baseSync()" ng-disabled="deepSyncInProgress"><span class="glyphicon glyphicon-repeat" aria-hidden="true"></span> Pull</button>
</dd>
<dt class="col-2" ng-if="syncStatus['sync']">Statut</dt>
<dd class="col-10" ng-if="syncStatus['sync']">
<span ng-if="(syncStatus.syncMutex !== undefined && syncStatus.syncMutex) || (syncPercent > 0 && syncPercent < 100)">{{ syncPercent }}&nbsp;%</span>
<span class="badge badge-pill" ng-class="{'badge-success': !syncStatus.pullMutex, 'badge-danger': syncStatus.pullMutex}" ng-if="syncStatus.pullMutex !== undefined">Pull</span>
<span class="badge badge-pill" ng-class="{'badge-success': !syncStatus.syncMutex, 'badge-danger': syncStatus.syncMutex}" ng-if="syncStatus.syncMutex !== undefined">Synchronisation</span>
</dd>
<dd class="col-10" title="{{ configro['sync'] }}" ng-bind="configro.sync"></dd>
<dt class="col-2" ng-if="configro['sync-id']">ID</dt>
<dd class="col-10" ng-if="configro['sync-id']">{{ configro['sync-id'] }}</dd>
<dt class="col-2" ng-if="configro['sync']">Statut</dt>
<dd class="col-10" ng-if="configro['sync']">{{ syncProgress }}</dd>
</dl>
<pre style="background: black; color: white;" class="p-2" ng-if="syncStatus.lastError">{{ syncStatus.lastError }}</pre>
<div class="d-flex justify-content-around" ng-if="syncStatus.sync">
<div class="d-flex justify-content-around" ng-if="configro.sync">
<button ng-if="configro['sync-type'] === 'GitImporter'" type="button" class="btn btn-info" ng-click="baseSync()" ng-disabled="deepSyncInProgress"><span class="glyphicon glyphicon-repeat" aria-hidden="true"></span> Pull</button>
<div class="btn-group dropright">
<button type="button" class="btn btn-secondary" ng-click="deepSync()" ng-disabled="deepSyncInProgress"><span class="glyphicon glyphicon-import" aria-hidden="true"></span> Synchronisation intégrale</button>
<button type="button" class="btn btn-secondary dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" ng-disabled="deepSyncInProgress">
@ -58,6 +50,7 @@
</button>
<div class="dropdown-menu" ng-controller="ThemesListController" style="max-height: 45vh; overflow: auto">
<a class="dropdown-item" ng-click="deepSync(theme)" ng-repeat="theme in themes" ng-bind="theme.name"></a>
<a class="dropdown-item" ng-click="deepSync({name: 'Exercices indépendants', id: 0})">Exercices indépendants</a>
</div>
</div>
<button type="button" class="btn btn-secondary" ng-click="speedyDeepSync()" ng-disabled="deepSyncInProgress"><span class="glyphicon glyphicon-import" aria-hidden="true"></span> Synchronisation sans fichiers</button>
@ -119,41 +112,3 @@
</ul>
</div>
</div>
<div class="card mb-5" ng-controller="ThemesListController">
<div class="card-header">
<button type="button" class="btn btn-primary float-right mx-1" ng-click="diffWithRepo()" title="Calculer les différences avec le dépôt">
<span class="glyphicon glyphicon-refresh" aria-hidden="true"></span>
</button>
<h3 class="mb-0">
Différences avec le dépôts
</h3>
</div>
<div class="card-body" ng-if="!diff">
<div class="alert alert-info">Lancez la génération du rapport pour lister les différences.</div>
</div>
<div ng-repeat="(th, lines) in diff" class="card-body" ng-if="diff">
<div class="d-flex">
<h3>
{{ th }}
</h3>
<div class="d-inline-block" ng-repeat="theme in themes" ng-if="theme.name == th">
<a href="themes/{{ theme.id }}" class="btn btn-link" title="Voir le thème">
<span class="glyphicon glyphicon-hand-right" aria-hidden="true"></span>
</a>
<button class="btn btn-light" title="Resynchroniser uniquement ce thème" ng-click="deepSync(theme)" ng-if="settings.wip || !timeProgression || displayDangerousActions">
<span class="glyphicon glyphicon-hdd" aria-hidden="true"></span>
</button>
</div>
</div>
<ul>
<li ng-repeat="diffline in lines" class="row">
<a ng-href="{{ diffline.link }}" class="col-2 d-flex align-items-center text-truncate text-monospace" title="{{ diffline.field }}">{{ diffline.field }}</a>
<div class="col">
<div class="text-danger"><span class="text-monospace">-</span>{{ diffline.be }}</div>
<div class="text-success"><span class="text-monospace">+</span>{{ diffline.af }}</div>
</div>
</li>
</ul>
</div>
</div>

View file

@ -1,19 +1,11 @@
<div class="d-flex justify-content-between align-items-center">
<h2>
&Eacute;quipes
</h2>
<div>
<button type="button" ng-click="show('new')" class="btn btn-sm btn-primary ml-1"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Ajouter une équipe</button>
<form class="d-inline">
<input id="crTeamsInput" type="file" file-model="selectedFile" class="d-none" />
<button type="button" ng-click="triggerTeamsImport()" class="btn btn-sm btn-secondary ml-1"><span class="glyphicon glyphicon-import" aria-hidden="true"></span> Import Cyberrange</button>
</form>
<button type="button" ng-click="show('print')" class="btn btn-sm btn-secondary ml-1" title="Imprimer les équipes et leurs membres"><span class="glyphicon glyphicon-print" aria-hidden="true"></span></button>
<button type="button" ng-click="refineTeamsColors()" class="btn btn-sm btn-secondary ml-1" title="Réarranger automatiquement les couleurs des équipes pour maximiser le spectre utilisé"><span class="glyphicon glyphicon-adjust" aria-hidden="true"></span></button>
<button type="button" ng-click="show('export')" class="btn btn-sm btn-secondary ml-1"><span class="glyphicon glyphicon-export" aria-hidden="true"></span> Statistiques générales</button>
<button type="button" ng-click="desactiveTeams()" class="btn btn-sm btn-danger ml-1" title="Cliquer pour marquer les équipes sans certificat comme inactives (et ainsi éviter que ses fichiers ne soient générés)"><span class="glyphicon glyphicon-leaf" aria-hidden="true"></span> Désactiver les équipes inactives</button>
</div>
</div>
<h2>
&Eacute;quipes
<button type="button" ng-click="show('new')" class="float-right btn btn-sm btn-primary"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Ajouter une équipe</button>
<button type="button" ng-click="show('print')" class="float-right btn btn-sm btn-secondary mr-2"><span class="glyphicon glyphicon-print" aria-hidden="true"></span> Imprimer les équipes</button>
<button type="button" ng-click="show('export')" class="float-right btn btn-sm btn-secondary mr-2"><span class="glyphicon glyphicon-export" aria-hidden="true"></span> Statistiques générales</button>
<button type="button" ng-click="desactiveTeams()" class="float-right btn btn-sm btn-danger mr-2" title="Cliquer pour marquer les équipes sans certificat comme inactives (et ainsi éviter que ses fichiers ne soient générés)"><span class="glyphicon glyphicon-leaf" aria-hidden="true"></span> Désactiver les équipes inactives</button>
<button type="button" ng-click="genDexCfg()" class="float-right btn btn-sm btn-success mr-2"><span class="glyphicon glyphicon-refresh" aria-hidden="true"></span> DexIdP</button>
</h2>
<p><input type="search" class="form-control" placeholder="Search" ng-model="query" ng-keypress="validateSearch($event)" autofocus></p>
<table class="table table-hover table-bordered table-striped table-sm">

View file

@ -13,7 +13,7 @@
<th>Date</th>
</thead>
<tbody>
<tr ng-repeat="row in scores | filter: query | orderBy:'time'" ng-class="{'bg-danger': row.reason == 'Bonus flag', 'bg-ffound': row.reason == 'First blood', 'bg-wchoices': row.reason == 'Display choices', 'bg-success': row.reason == 'Validation', 'bg-info': row.reason == 'Hint', 'bg-secondary': row.reason.startsWith('Response '), 'bg-warning': row.reason == 'Tries'}">
<tr ng-repeat="row in scores | filter: query | orderBy:'time'" ng-class="{'bg-danger': row.reason == 'Bonus flag', 'bg-ffound': row.reason == 'First blood', 'bg-wchoices': row.reason == 'Display choices', 'bg-success': row.reason == 'Validation', 'bg-info': row.reason == 'Hint', 'bg-warning': row.reason == 'Tries'}">
<td>
<a ng-repeat="exercice in exercices" ng-if="exercice.id == row.id_exercice" href="exercices/{{ row.id_exercice }}">{{ exercice.title }}</a>
</td>
@ -23,12 +23,9 @@
<td>
{{ row.points * row.coeff }}
</td>
<td ng-if="!row.reason.startsWith('Response ')">
<td>
{{ row.points }} * {{ row.coeff }}
</td>
<td ng-if="row.reason.startsWith('Response ')">
{{ row.points }} * {{ settings.questionGainRatio }} / {{ settings.questionGainRatio / row.coeff }}
</td>
<td>
<nobr title="{{ row.time }}">{{ row.time | date:"mediumTime" }}</nobr>
</td>
@ -36,7 +33,7 @@
<tfoot>
<th></th>
<th></th>
<th>{{ my.score100 / 100 }}</th>
<th>{{ my.score }}</th>
</thead>
</tbody>
</table>

View file

@ -37,7 +37,7 @@
<dt ng-bind="theme.name"></dt>
<dd>
<ul class="list-unstyled">
<li ng-repeat="exercice in theme.exercices" ng-if="my.exercices[exercice.id] && my.exercices[exercice.id].solved_rank"><a href="/{{ my.exercices[exercice.id].theme_id }}/{{ exercice.id }}" target="_blank"><abbr title="{{ my.exercices[exercice.id].statement }}">{{ exercice.title }}</abbr></a> (<abbr title="{{ my.exercices[exercice.id].solved_time | date:'mediumDate' }} à {{ my.exercices[exercice.id].solved_time | date:'mediumTime' }}">{{ my.exercices[exercice.id].solved_rank }}<sup>e</sup></abbr>)</li>
<li ng-repeat="(eid,exercice) in theme.exercices" ng-if="my.exercices[eid] && my.exercices[eid].solved_rank"><a href="/{{ my.exercices[eid].theme_id }}/{{ eid }}" target="_blank"><abbr title="{{ my.exercices[eid].statement }}">{{ exercice.title }}</abbr></a> (<abbr title="{{ my.exercices[eid].solved_time | date:'mediumDate' }} à {{ my.exercices[eid].solved_time | date:'mediumTime' }}">{{ my.exercices[eid].solved_rank }}<sup>e</sup></abbr>)</li>
</ul>
</dd>
</div>

View file

@ -2,7 +2,6 @@
Thèmes
<button type="button" ng-click="show('new')" class="float-right btn btn-sm btn-primary ml-2"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Ajouter un thème</button>
<button type="button" ng-click="sync()" ng-class="{'disabled': inSync}" class="float-right btn btn-sm btn-secondary ml-2"><span class="glyphicon glyphicon-refresh" aria-hidden="true"></span> Synchroniser</button>
<a href="forge-links" class="float-right btn btn-sm btn-dark ml-2"><span class="glyphicon glyphicon-folder-open" aria-hidden="true"></span> Liens d'accès à la forge</a>
</h2>
<p><input type="search" class="form-control" placeholder="Search" ng-model="query" ng-keypress="validateSearch($event)" autofocus></p>

View file

@ -7,21 +7,8 @@
</div>
</div>
<div ng-if="diff">
<h3>Différences par rapport au dépôt</h3>
<div ng-repeat="diffline in diff" class="row">
<a ng-href="{{ diffline.link }}" class="col-3 d-flex align-items-center text-truncate text-monospace" title="{{ diffline.field }}">{{ diffline.field }}</a>
<div class="col">
<div class="text-danger"><span class="text-monospace">-</span>{{ diffline.be }}</div>
<div class="text-success"><span class="text-monospace">+</span>{{ diffline.af }}</div>
</div>
</div>
</div>
<hr ng-if="diff" class="my-3">
<div class="row">
<form ng-submit="saveTheme()" class="col-4" ng-if="!(theme.id === 0 && theme.path)">
<form ng-submit="saveTheme()" class="col-4">
<div ng-class="{'form-group': field != 'locked', 'form-check': field == 'locked'}" ng-repeat="field in fields">
<input type="checkbox" class="form-check-input" id="{{ field }}" ng-model="theme[field]" ng-if="field == 'locked'">
<label for="{{ field }}">{{ field | capitalize }}</label>
@ -38,14 +25,11 @@
</div>
</form>
<div ng-if="theme.id || theme.path" class="col-md-8" ng-class="{'offset-md-2': theme.id === 0 && theme.path}" ng-controller="ExercicesListController">
<div ng-if="theme.id" class="col-md-8" ng-controller="ExercicesListController">
<h3>
Exercices ({{ exercices.length }})
<button type="button" ng-click="show('new')" class="float-right btn btn-sm btn-primary ml-2"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Ajouter un exercice</button>
<div class="float-right btn-group ml-2" role="group">
<button type="button" ng-click="syncExo()" ng-class="{'disabled': inSync}" class="btn btn-sm btn-light ml-2"><span class="glyphicon glyphicon-refresh" aria-hidden="true"></span> Synchroniser</button>
<button type="button" ng-click="checkExoSync()" ng-class="{'disabled': inSync}" class="btn btn-sm btn-light" title="Exporter l'exercice actuel"><span class="glyphicon glyphicon-thumbs-up" aria-hidden="true"></span></button>
</div>
<button type="button" ng-click="syncExo()" ng-class="{'disabled': inSync}" class="float-right btn btn-sm btn-light ml-2"><span class="glyphicon glyphicon-refresh" aria-hidden="true"></span> Synchroniser</button>
</h3>
<p><input type="search" class="form-control form-control-sm" placeholder="Search" ng-model="query" autofocus></p>

View file

@ -23,7 +23,7 @@ func NewThemeError(theme *fic.Theme, err error) *ThemeError {
if theme == nil {
return &ThemeError{
error: err,
ThemePath: fic.StandaloneExercicesDirectory,
ThemePath: StandaloneExercicesDirectory,
}
}
@ -106,9 +106,9 @@ func NewChallengeTxtError(exercice *fic.Exercice, line uint, err error, theme ..
func (e *ChallengeTxtError) Error() string {
if e.ChallengeTxtLine != 0 {
return fmt.Sprintf("%s:%d: %s", path.Join(e.ExercicePath, "challenge.toml"), e.ChallengeTxtLine, e.ThemeError.error.Error())
return fmt.Sprintf("%s:%d: %s", path.Join(e.ExercicePath, "challenge.txt"), e.ChallengeTxtLine, e.ThemeError.error.Error())
} else {
return fmt.Sprintf("%s: %s", path.Join(e.ExercicePath, "challenge.toml"), e.ThemeError.error.Error())
return fmt.Sprintf("%s: %s", path.Join(e.ExercicePath, "challenge.txt"), e.ThemeError.error.Error())
}
}
@ -127,7 +127,7 @@ func NewHintError(exercice *fic.Exercice, hint *fic.EHint, line int, err error,
}
func (e *HintError) Error() string {
return fmt.Sprintf("%s: hint#%d (%s): %s", path.Join(e.ExercicePath, "challenge.toml"), e.HintId, e.HintTitle, e.ThemeError.error.Error())
return fmt.Sprintf("%s: hint#%d (%s): %s", path.Join(e.ExercicePath, "challenge.txt"), e.HintId, e.HintTitle, e.ThemeError.error.Error())
}
type FlagError struct {
@ -144,5 +144,5 @@ func NewFlagError(exercice *fic.Exercice, flag *ExerciceFlag, line int, err erro
}
func (e *FlagError) Error() string {
return fmt.Sprintf("%s: flag#%d: %s", path.Join(e.ExercicePath, "challenge.toml"), e.FlagId, e.ThemeError.error.Error())
return fmt.Sprintf("%s: flag#%d: %s", path.Join(e.ExercicePath, "challenge.txt"), e.FlagId, e.ThemeError.error.Error())
}

View file

@ -26,10 +26,10 @@ const sampleFile = `0-exercice-1/overview.md:spelling:Sterik
0-exercice-1/resolution.md:11:typo_guillemets_typographiques_doubles_fermants
0-exercice-1/resolution.md:spelling:cronjob
0-exercice-1/resolution.md:spelling:Level
challenge.toml:spelling:time
challenge.toml:spelling:ago
challenge.txt:spelling:time
challenge.txt:spelling:ago
0-exercice-1/resolution.md:spelling:SCL
challenge.toml:spelling:SCL`
challenge.txt:spelling:SCL`
func TestLoadExceptions(t *testing.T) {
exceptions := ParseExceptionString(sampleFile, nil)
@ -47,7 +47,7 @@ func TestFilterExceptions(t *testing.T) {
t.Fatalf("Expected 1 exceptions, got %d", len(*filteredExceptions))
}
filteredExceptions = exceptions.GetFileExceptions("challenge.toml")
filteredExceptions = exceptions.GetFileExceptions("challenge.txt")
if len(*filteredExceptions) != 3 {
t.Fatalf("Expected 3 exceptions, got %d", len(*filteredExceptions))
}

View file

@ -173,9 +173,7 @@ func getExerciceParams(i Importer, exercice *fic.Exercice) (params ExerciceParam
if params, _, err = parseExerciceParams(i, exercice.Path); err != nil {
errs = multierr.Append(errs, NewChallengeTxtError(exercice, 0, err))
} else if len(params.Flags) == 0 && len(params.FlagsUCQ) == 0 && len(params.FlagsMCQ) == 0 {
if !params.WIP {
errs = multierr.Append(errs, NewChallengeTxtError(exercice, 0, fmt.Errorf("has no flag")))
}
errs = multierr.Append(errs, NewChallengeTxtError(exercice, 0, fmt.Errorf("has no flag")))
} else {
// Treat legacy UCQ flags as ExerciceFlag
for _, flag := range params.FlagsUCQ {

View file

@ -60,7 +60,7 @@ func BuildFilesListInto(i Importer, exercice *fic.Exercice, into string) (files
// Parse DIGESTS.txt
if digs, err := GetFileContent(i, path.Join(exercice.Path, into, "DIGESTS.txt")); err != nil {
errs = multierr.Append(errs, NewExerciceError(exercice, fmt.Errorf("unable to read %s: %w", path.Join(into, "DIGESTS.txt"), err)))
} else if len(digs) > 0 {
} else {
digests = map[string][]byte{}
for nline, d := range strings.Split(digs, "\n") {
if dsplt := strings.SplitN(d, " ", 2); len(dsplt) < 2 {
@ -315,57 +315,9 @@ func DownloadExerciceFile(pf ExerciceFile, dest string, exercice *fic.Exercice,
return
}
type importedFile struct {
file interface{}
Name string
}
func SyncExerciceFiles(i Importer, exercice *fic.Exercice, paramsFiles map[string]ExerciceFile, actionAfterImport func(fname string, digests map[string][]byte, filePath, origin string) (interface{}, error)) (ret []*importedFile, errs error) {
files, digests, berrs := BuildFilesListInto(i, exercice, "files")
errs = multierr.Append(errs, berrs)
// Import standard files
for _, fname := range files {
var f interface{}
var err error
if pf, exists := paramsFiles[fname]; exists && pf.URL != "" && !i.Exists(path.Join(exercice.Path, "files", fname)) {
dest := GetDestinationFilePath(pf.URL, &pf.Filename)
if _, err := os.Stat(dest); !os.IsNotExist(err) {
if d, err := actionAfterImport(fname, digests, dest, pf.URL); err == nil {
f = d
}
}
if f == nil {
errs = multierr.Append(errs, DownloadExerciceFile(paramsFiles[fname], dest, exercice, false))
f, err = actionAfterImport(fname, digests, dest, pf.URL)
}
} else {
f, err = i.importFile(path.Join(exercice.Path, "files", fname), func(filePath, origin string) (interface{}, error) {
return actionAfterImport(fname, digests, filePath, origin)
})
}
if err != nil {
errs = multierr.Append(errs, NewFileError(exercice, fname, err))
continue
}
ret = append(ret, &importedFile{
f,
fname,
})
}
return
}
// ImportExerciceFiles reads the content of files/ directory and import it as EFile for the given challenge.
// SyncExerciceFiles reads the content of files/ directory and import it as EFile for the given challenge.
// It takes care of DIGESTS.txt and ensure imported files match.
func ImportExerciceFiles(i Importer, exercice *fic.Exercice, exceptions *CheckExceptions) (errs error) {
func SyncExerciceFiles(i Importer, exercice *fic.Exercice, exceptions *CheckExceptions) (errs error) {
if _, err := exercice.WipeFiles(); err != nil {
errs = multierr.Append(errs, err)
}
@ -376,41 +328,63 @@ func ImportExerciceFiles(i Importer, exercice *fic.Exercice, exceptions *CheckEx
return
}
actionAfterImport := func(fname string, digests map[string][]byte, filePath, origin string) (interface{}, error) {
var digest_shown []byte
if strings.HasSuffix(fname, ".gz") {
if d, exists := digests[strings.TrimSuffix(fname, ".gz")]; exists {
digest_shown = d
}
}
files, digests, berrs := BuildFilesListInto(i, exercice, "files")
errs = multierr.Append(errs, berrs)
published := true
disclaimer := ""
if f, exists := paramsFiles[fname]; exists {
published = !f.Hidden
// Call checks hooks
for _, hk := range hooks.mdTextHooks {
for _, err := range multierr.Errors(hk(f.Disclaimer, exercice.Language, exceptions)) {
errs = multierr.Append(errs, NewFileError(exercice, fname, err))
// Import standard files
for _, fname := range files {
actionAfterImport := func(filePath string, origin string) (interface{}, error) {
var digest_shown []byte
if strings.HasSuffix(fname, ".gz") {
if d, exists := digests[strings.TrimSuffix(fname, ".gz")]; exists {
digest_shown = d
}
}
if disclaimer, err = ProcessMarkdown(i, fixnbsp(f.Disclaimer), exercice.Path); err != nil {
errs = multierr.Append(errs, NewFileError(exercice, fname, fmt.Errorf("error during markdown formating of disclaimer: %w", err)))
published := true
disclaimer := ""
if f, exists := paramsFiles[fname]; exists {
published = !f.Hidden
// Call checks hooks
for _, hk := range hooks.mdTextHooks {
for _, err := range multierr.Errors(hk(f.Disclaimer, exercice.Language, exceptions)) {
errs = multierr.Append(errs, NewFileError(exercice, fname, err))
}
}
if disclaimer, err = ProcessMarkdown(i, fixnbsp(f.Disclaimer), exercice.Path); err != nil {
errs = multierr.Append(errs, NewFileError(exercice, fname, fmt.Errorf("error during markdown formating of disclaimer: %w", err)))
}
}
return exercice.ImportFile(filePath, origin, digests[fname], digest_shown, disclaimer, published)
}
return exercice.ImportFile(filePath, origin, digests[fname], digest_shown, disclaimer, published)
}
var f interface{}
files, berrs := SyncExerciceFiles(i, exercice, paramsFiles, actionAfterImport)
errs = multierr.Append(errs, berrs)
if pf, exists := paramsFiles[fname]; exists && pf.URL != "" {
dest := GetDestinationFilePath(pf.URL, &pf.Filename)
// Import files in db
for _, file := range files {
fname := file.Name
f := file.file
if _, err := os.Stat(dest); !os.IsNotExist(err) {
if d, err := actionAfterImport(dest, pf.URL); err == nil {
f = d
}
}
if f == nil {
errs = multierr.Append(errs, DownloadExerciceFile(paramsFiles[fname], dest, exercice, false))
f, err = actionAfterImport(dest, pf.URL)
}
} else {
f, err = i.importFile(path.Join(exercice.Path, "files", fname), actionAfterImport)
}
if err != nil {
errs = multierr.Append(errs, NewFileError(exercice, fname, err))
continue
}
if f.(*fic.EFile).Size == 0 {
errs = multierr.Append(errs, NewFileError(exercice, fname, fmt.Errorf("imported file is empty!")))
@ -446,67 +420,36 @@ func ImportExerciceFiles(i Importer, exercice *fic.Exercice, exceptions *CheckEx
return
}
func GetRemoteExerciceFiles(thid, exid string) ([]*fic.EFile, error) {
theme, exceptions, errs := BuildTheme(GlobalImporter, thid)
if theme == nil {
return nil, errs
}
exercice, _, _, _, _, errs := BuildExercice(GlobalImporter, theme, path.Join(theme.Path, exid), nil, exceptions)
if exercice == nil {
return nil, errs
}
files, digests, errs := BuildFilesListInto(GlobalImporter, exercice, "files")
if files == nil {
return nil, errs
}
var ret []*fic.EFile
for _, fname := range files {
fPath := path.Join(exercice.Path, "files", fname)
fSize, _ := GetFileSize(GlobalImporter, fPath)
file := fic.EFile{
Path: fPath,
Name: fname,
Checksum: digests[fname],
Size: fSize,
Published: true,
}
if d, exists := digests[strings.TrimSuffix(file.Name, ".gz")]; exists {
file.Name = strings.TrimSuffix(file.Name, ".gz")
file.Path = strings.TrimSuffix(file.Path, ".gz")
file.ChecksumShown = d
}
ret = append(ret, &file)
}
// Complete with attributes
if paramsFiles, err := GetExerciceFilesParams(GlobalImporter, exercice); err == nil {
for _, file := range ret {
if f, ok := paramsFiles[file.Name]; ok {
file.Published = !f.Hidden
if disclaimer, err := ProcessMarkdown(GlobalImporter, fixnbsp(f.Disclaimer), exercice.Path); err == nil {
file.Disclaimer = disclaimer
}
}
}
}
return ret, nil
}
// ApiGetRemoteExerciceFiles is an accessor to remote exercice files list.
func ApiGetRemoteExerciceFiles(c *gin.Context) {
files, err := GetRemoteExerciceFiles(c.Params.ByName("thid"), c.Params.ByName("exid"))
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
theme, exceptions, errs := BuildTheme(GlobalImporter, c.Params.ByName("thid"))
if theme != nil {
exercice, _, _, _, _, errs := BuildExercice(GlobalImporter, theme, path.Join(theme.Path, c.Params.ByName("exid")), nil, exceptions)
if exercice != nil {
files, digests, errs := BuildFilesListInto(GlobalImporter, exercice, "files")
if files != nil {
var ret []*fic.EFile
for _, fname := range files {
fPath := path.Join(exercice.Path, "files", fname)
fSize, _ := GetFileSize(GlobalImporter, fPath)
ret = append(ret, &fic.EFile{
Path: fPath,
Name: fname,
Checksum: digests[fname],
Size: fSize,
})
}
c.JSON(http.StatusOK, ret)
} else {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Errorf("%q", errs)})
return
}
} else {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Errorf("%q", errs)})
return
}
} else {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Errorf("%q", errs)})
return
}
c.JSON(http.StatusOK, files)
}

View file

@ -121,7 +121,7 @@ func buildExerciceHints(i Importer, exercice *fic.Exercice, exceptions *CheckExc
// CheckExerciceHints checks if all hints are corrects..
func CheckExerciceHints(i Importer, exercice *fic.Exercice, exceptions *CheckExceptions) ([]importHint, error) {
exceptions = exceptions.GetFileExceptions("challenge.toml", "challenge.txt")
exceptions = exceptions.GetFileExceptions("challenge.txt", "challenge.toml")
hints, errs := buildExerciceHints(i, exercice, exceptions)
@ -139,7 +139,7 @@ func SyncExerciceHints(i Importer, exercice *fic.Exercice, flagsBindings map[int
if _, err := exercice.WipeHints(); err != nil {
errs = multierr.Append(errs, err)
} else {
exceptions = exceptions.GetFileExceptions("challenge.toml", "challenge.txt")
exceptions = exceptions.GetFileExceptions("challenge.txt", "challenge.toml")
hints, berrs := buildExerciceHints(i, exercice, exceptions)
errs = multierr.Append(errs, berrs)
@ -169,32 +169,25 @@ func SyncExerciceHints(i Importer, exercice *fic.Exercice, flagsBindings map[int
return
}
func GetRemoteExerciceHints(thid, exid string) ([]importHint, error) {
theme, exceptions, errs := BuildTheme(GlobalImporter, thid)
if theme == nil {
return nil, errs
}
exercice, _, _, eexceptions, _, errs := BuildExercice(GlobalImporter, theme, path.Join(theme.Path, exid), nil, exceptions)
if exercice == nil {
return nil, errs
}
hints, errs := CheckExerciceHints(GlobalImporter, exercice, eexceptions)
if hints == nil {
return nil, errs
}
return hints, nil
}
// ApiListRemoteExerciceHints is an accessor letting foreign packages to access remote exercice hints.
func ApiGetRemoteExerciceHints(c *gin.Context) {
hints, errs := GetRemoteExerciceHints(c.Params.ByName("thid"), c.Params.ByName("exid"))
if hints != nil {
theme, exceptions, errs := BuildTheme(GlobalImporter, c.Params.ByName("thid"))
if theme != nil {
exercice, _, _, eexceptions, _, errs := BuildExercice(GlobalImporter, theme, path.Join(theme.Path, c.Params.ByName("exid")), nil, exceptions)
if exercice != nil {
hints, errs := CheckExerciceHints(GlobalImporter, exercice, eexceptions)
if hints != nil {
c.JSON(http.StatusOK, hints)
return
}
c.AbortWithStatusJSON(http.StatusInternalServerError, fmt.Errorf("%q", errs))
return
}
c.AbortWithStatusJSON(http.StatusInternalServerError, fmt.Errorf("%q", errs))
return
}
c.JSON(http.StatusOK, hints)
c.AbortWithStatusJSON(http.StatusInternalServerError, fmt.Errorf("%q", errs))
}

View file

@ -279,7 +279,6 @@ func buildKeyFlag(exercice *fic.Exercice, flag ExerciceFlag, flagline int, defau
}
type importFlag struct {
origin ExerciceFlag
Line int
Flag fic.Flag
JustifyOf *fic.MCQ_entry
@ -356,10 +355,8 @@ func buildExerciceFlag(i Importer, exercice *fic.Exercice, flag ExerciceFlag, nl
flag.Type = "radio"
case "mcq":
flag.Type = "mcq"
case "justified":
flag.Type = "justified"
default:
errs = multierr.Append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("invalid type of flag: should be 'key', 'number', 'text', 'mcq', 'justified', 'ucq', 'radio' or 'vector'")))
errs = multierr.Append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("invalid type of flag: should be 'key', 'number', 'text', 'mcq', 'ucq', 'radio' or 'vector'")))
return
}
@ -393,9 +390,8 @@ func buildExerciceFlag(i Importer, exercice *fic.Exercice, flag ExerciceFlag, nl
errs = multierr.Append(errs, berrs)
if addedFlag != nil {
ret = append(ret, importFlag{
origin: flag,
Line: nline + 1,
Flag: addedFlag,
Line: nline + 1,
Flag: addedFlag,
})
}
} else if flag.Type == "key" || strings.HasPrefix(flag.Type, "number") || flag.Type == "text" || flag.Type == "ucq" || flag.Type == "radio" || flag.Type == "vector" {
@ -403,13 +399,12 @@ func buildExerciceFlag(i Importer, exercice *fic.Exercice, flag ExerciceFlag, nl
errs = multierr.Append(errs, berrs)
if addedFlag != nil {
ret = append(ret, importFlag{
origin: flag,
Line: nline + 1,
Flag: *addedFlag,
Choices: choices,
})
}
} else if flag.Type == "mcq" || flag.Type == "justified" {
} else if flag.Type == "mcq" {
addedFlag := fic.MCQ{
IdExercice: exercice.Id,
Order: int8(nline + 1),
@ -418,7 +413,7 @@ func buildExerciceFlag(i Importer, exercice *fic.Exercice, flag ExerciceFlag, nl
}
hasOne := false
isJustified := flag.Type == "justified"
isJustified := false
if len(flag.Variant) != 0 {
errs = multierr.Append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("variant is not defined for this kind of flag")))
@ -465,7 +460,6 @@ func buildExerciceFlag(i Importer, exercice *fic.Exercice, flag ExerciceFlag, nl
errs = multierr.Append(errs, berrs)
if addedFlag != nil {
ret = append(ret, importFlag{
origin: flag,
Line: nline + 1,
Flag: *addedFlag,
JustifyOf: entry,
@ -483,9 +477,8 @@ func buildExerciceFlag(i Importer, exercice *fic.Exercice, flag ExerciceFlag, nl
}
ret = append([]importFlag{importFlag{
origin: flag,
Line: nline + 1,
Flag: &addedFlag,
Line: nline + 1,
Flag: &addedFlag,
}}, ret...)
}
return
@ -548,7 +541,7 @@ func buildExerciceFlags(i Importer, exercice *fic.Exercice, exceptions *CheckExc
// CheckExerciceFlags checks if all flags for the given challenge are correct.
func CheckExerciceFlags(i Importer, exercice *fic.Exercice, files []string, exceptions *CheckExceptions) (rf []fic.Flag, errs error) {
exceptions = exceptions.GetFileExceptions("challenge.toml", "challenge.txt")
exceptions = exceptions.GetFileExceptions("challenge.txt", "challenge.toml")
flags, flagsids, berrs := buildExerciceFlags(i, exercice, exceptions)
errs = multierr.Append(errs, berrs)
@ -575,10 +568,6 @@ func CheckExerciceFlags(i Importer, exercice *fic.Exercice, files []string, exce
if int64(fk.ChoicesCost) >= exercice.Gain {
errs = multierr.Append(errs, NewFlagError(exercice, nil, flag.Line, fmt.Errorf("flag's choice_cost is higher than exercice gain")))
}
if raw, ok := flag.origin.Raw.(string); ok && raw == fk.Placeholder {
errs = multierr.Append(errs, NewFlagError(exercice, nil, flag.Line, fmt.Errorf("flag's placeholder and raw are identical")))
}
}
// Check dependency loop
@ -645,7 +634,7 @@ func SyncExerciceFlags(i Importer, exercice *fic.Exercice, exceptions *CheckExce
} else if _, err := exercice.WipeMCQs(); err != nil {
errs = multierr.Append(errs, err)
} else {
exceptions = exceptions.GetFileExceptions("challenge.toml", "challenge.txt")
exceptions = exceptions.GetFileExceptions("challenge.txt", "challenge.toml")
flags, flagids, berrs := buildExerciceFlags(i, exercice, exceptions)
errs = multierr.Append(errs, berrs)
@ -699,32 +688,26 @@ func SyncExerciceFlags(i Importer, exercice *fic.Exercice, exceptions *CheckExce
return
}
func GetRemoteExerciceFlags(thid, exid string) ([]fic.Flag, error) {
theme, exceptions, errs := BuildTheme(GlobalImporter, thid)
if theme == nil {
return nil, errs
}
exercice, _, _, eexceptions, _, errs := BuildExercice(GlobalImporter, theme, path.Join(theme.Path, exid), nil, exceptions)
if exercice == nil {
return nil, errs
}
flags, errs := CheckExerciceFlags(GlobalImporter, exercice, []string{}, eexceptions)
if flags == nil {
return nil, errs
}
return flags, nil
}
// ApiListRemoteExerciceFlags is an accessor letting foreign packages to access remote exercice flags.
func ApiGetRemoteExerciceFlags(c *gin.Context) {
flags, err := GetRemoteExerciceFlags(c.Params.ByName("thid"), c.Params.ByName("exid"))
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
theme, exceptions, errs := BuildTheme(GlobalImporter, c.Params.ByName("thid"))
if theme != nil {
exercice, _, _, eexceptions, _, errs := BuildExercice(GlobalImporter, theme, path.Join(theme.Path, c.Params.ByName("exid")), nil, exceptions)
if exercice != nil {
flags, errs := CheckExerciceFlags(GlobalImporter, exercice, []string{}, eexceptions)
if flags != nil {
c.JSON(http.StatusOK, flags)
return
}
c.AbortWithStatusJSON(http.StatusInternalServerError, fmt.Errorf("%q", errs))
return
}
c.AbortWithStatusJSON(http.StatusInternalServerError, fmt.Errorf("%q", errs))
return
}
c.JSON(http.StatusOK, flags)
c.AbortWithStatusJSON(http.StatusInternalServerError, fmt.Errorf("%q", errs))
return
}

View file

@ -63,17 +63,10 @@ func buildDependancyMap(i Importer, theme *fic.Theme) (dmap map[int64]*fic.Exerc
continue
}
// ename can be overrride by title.txt
if i.Exists(path.Join(theme.Path, edir, "title.txt")) {
if myTitle, err := GetFileContent(i, path.Join(theme.Path, edir, "title.txt")); err == nil {
ename = strings.TrimSpace(myTitle)
}
}
var e *fic.Exercice
e, err = theme.GetExerciceByTitle(ename)
if err != nil {
return dmap, fmt.Errorf("unable to GetExerciceByTitle(ename=%q, tid=%d): %w", ename, theme.Id, err)
return
}
dmap[int64(eid)] = e
@ -287,7 +280,7 @@ func BuildExercice(i Importer, theme *fic.Theme, epath string, dmap *map[int64]*
e.WIP = p.WIP
if p.WIP && !AllowWIPExercice {
errs = multierr.Append(errs, NewExerciceError(e, fmt.Errorf("exercice declared Work In Progress in challenge.toml"), theme))
errs = multierr.Append(errs, NewExerciceError(e, fmt.Errorf("exercice declared Work In Progress in challenge.txt"), theme))
}
if p.Gain == 0 {
@ -398,7 +391,7 @@ func SyncExercice(i Importer, theme *fic.Theme, epath string, dmap *map[int64]*f
if len(e.Image) > 0 {
if _, err := i.importFile(e.Image,
func(filePath string, origin string) (interface{}, error) {
if err := resizePicture(i, origin, filePath, image.Rect(0, 0, 500, 300)); err != nil {
if err := resizePicture(filePath, image.Rect(0, 0, 500, 300)); err != nil {
return nil, err
}
@ -475,57 +468,59 @@ func SyncExercices(i Importer, theme *fic.Theme, exceptions *CheckExceptions) (e
return
}
func ListRemoteExercices(thid string) ([]string, error) {
if thid == "_" {
return GetExercices(GlobalImporter, &fic.StandaloneExercicesTheme)
}
theme, _, errs := BuildTheme(GlobalImporter, thid)
if theme == nil {
return nil, errs
}
return GetExercices(GlobalImporter, theme)
}
// ApiListRemoteExercices is an accessor letting foreign packages to access remote exercices list.
func ApiListRemoteExercices(c *gin.Context) {
exercices, err := ListRemoteExercices(c.Params.ByName("thid"))
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
if c.Params.ByName("thid") == "_" {
exercices, err := GetExercices(GlobalImporter, &fic.Theme{Path: StandaloneExercicesDirectory})
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.JSON(http.StatusOK, exercices)
return
}
c.JSON(http.StatusOK, exercices)
}
theme, _, errs := BuildTheme(GlobalImporter, c.Params.ByName("thid"))
if theme != nil {
exercices, err := GetExercices(GlobalImporter, theme)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
func GetRemoteExercice(thid, exid string, inTheme *fic.Theme) (*fic.Exercice, error) {
if thid == fic.StandaloneExercicesDirectory || thid == "_" {
exercice, _, _, _, _, errs := BuildExercice(GlobalImporter, nil, path.Join(fic.StandaloneExercicesDirectory, exid), nil, nil)
return exercice, errs
}
theme, exceptions, errs := BuildTheme(GlobalImporter, thid)
if theme == nil {
return nil, fmt.Errorf("Theme not found")
}
if inTheme == nil {
inTheme = theme
}
exercice, _, _, _, _, errs := BuildExercice(GlobalImporter, inTheme, path.Join(theme.Path, exid), nil, exceptions)
return exercice, errs
}
// ApiGetRemoteExercice is an accessor letting foreign packages to access remote exercice attributes.
func ApiGetRemoteExercice(c *gin.Context) {
exercice, err := GetRemoteExercice(c.Params.ByName("thid"), c.Params.ByName("exid"), nil)
if exercice != nil {
c.JSON(http.StatusOK, exercice)
return
c.JSON(http.StatusOK, exercices)
} else {
c.JSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
c.AbortWithStatusJSON(http.StatusInternalServerError, fmt.Errorf("%q", errs))
return
}
}
// ApiListRemoteExercice is an accessor letting foreign packages to access remote exercice attributes.
func ApiGetRemoteExercice(c *gin.Context) {
if c.Params.ByName("thid") == "_" {
exercice, _, _, _, _, errs := BuildExercice(GlobalImporter, nil, path.Join(StandaloneExercicesDirectory, c.Params.ByName("exid")), nil, nil)
if exercice != nil {
c.JSON(http.StatusOK, exercice)
return
} else {
c.JSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Errorf("%q", errs)})
return
}
}
theme, exceptions, errs := BuildTheme(GlobalImporter, c.Params.ByName("thid"))
if theme != nil {
exercice, _, _, _, _, errs := BuildExercice(GlobalImporter, theme, path.Join(theme.Path, c.Params.ByName("exid")), nil, exceptions)
if exercice != nil {
c.JSON(http.StatusOK, exercice)
return
} else {
c.JSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Errorf("%q", errs)})
return
}
} else {
c.JSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Errorf("%q", errs)})
return
}
}

View file

@ -172,6 +172,10 @@ func GetFileContent(i Importer, URI string) (string, error) {
buf = append(buf, b)
}
if len(buf) == 0 {
return "", fmt.Errorf("File is empty")
}
return strings.TrimSpace(string(buf)), nil
}
}
@ -188,35 +192,26 @@ func GetDestinationFilePath(URI string, filename *string) string {
return path.Join(fic.FilesDir, strings.ToLower(base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(hash[:])), *filename)
}
var fileWriter = fileWriterToFS
func SetWriteFileFunc(writerFunc func(dest string) (io.WriteCloser, error)) {
fileWriter = writerFunc
}
func fileWriterToFS(dest string) (io.WriteCloser, error) {
func importFile(i Importer, URI string, dest string) error {
if err := os.MkdirAll(path.Dir(dest), 0751); err != nil {
return nil, err
return err
}
return os.Create(dest)
}
func importFile(i Importer, URI string, dest string) error {
if fdfrom, closer, err := GetFile(i, URI); err != nil {
os.Remove(dest)
// Write file
if fdto, err := os.Create(dest); err != nil {
return err
} else {
defer closer()
fdto, err := fileWriter(dest)
if err != nil {
return err
}
defer fdto.Close()
_, err = io.Copy(fdto, fdfrom)
return err
if fdfrom, closer, err := GetFile(i, URI); err != nil {
os.Remove(dest)
return err
} else {
defer closer()
_, err = io.Copy(fdto, fdfrom)
return err
}
}
}
@ -247,21 +242,3 @@ func WriteFileContent(i Importer, URI string, content []byte) error {
return fmt.Errorf("%t is not capable of writing", i)
}
}
func OpenOrGetFile(i Importer, URI string) (fd io.Reader, closer func() error, err error) {
if strings.HasPrefix(URI, "$FILES$") {
var fdc io.ReadCloser
fdc, err = os.Open(path.Join(fic.FilesDir, strings.TrimPrefix(URI, "$FILES$/")))
fd = fdc
closer = fdc.Close
} else {
fd, err = GlobalImporter.GetFile(URI)
if fdcloser, ok := fd.(io.ReadCloser); ok {
closer = fdcloser.Close
} else {
closer = func() error { return nil }
}
}
return
}

View file

@ -24,22 +24,6 @@ var oneThemeDeepSync sync.Mutex
// DeepSyncProgress expose the progression of the depp synchronization (0 = 0%, 255 = 100%).
var DeepSyncProgress uint8
func OneDeepSyncStatus() bool {
if oneDeepSync.TryLock() {
oneDeepSync.Unlock()
return true
}
return false
}
func OneThemeDeepSyncStatus() bool {
if oneThemeDeepSync.TryLock() {
oneThemeDeepSync.Unlock()
return true
}
return false
}
type SyncReport struct {
DateStart time.Time `json:"_started"`
DateEnd time.Time `json:"_ended"`
@ -72,9 +56,13 @@ func SpeedySyncDeep(i Importer) (errs SyncReport) {
errs.ThemesSync = append(errs.ThemesSync, sterr.Error())
}
if themes, err := fic.GetThemesExtended(); err == nil {
if themes, err := fic.GetThemes(); err == nil {
DeepSyncProgress = 2
if i.Exists(StandaloneExercicesDirectory) {
themes = append(themes, &fic.Theme{Path: StandaloneExercicesDirectory})
}
var themeStep uint8 = uint8(250) / uint8(len(themes))
for tid, theme := range themes {
@ -139,9 +127,14 @@ func SyncDeep(i Importer) (errs SyncReport) {
}
// Synchronize themes
if themes, err := fic.GetThemesExtended(); err == nil {
if themes, err := fic.GetThemes(); err == nil {
DeepSyncProgress = 2
// Also synchronize standalone exercices
if i.Exists(StandaloneExercicesDirectory) {
themes = append(themes, &fic.Theme{Path: StandaloneExercicesDirectory})
}
var themeStep uint8 = uint8(250) / uint8(len(themes))
for tid, theme := range themes {
@ -237,7 +230,7 @@ func SyncThemeDeep(i Importer, theme *fic.Theme, tid int, themeStep uint8, excep
log.Printf("Deep synchronization in progress: %d/255 - doing Theme %q, Exercice %q: %q\n", DeepSyncProgress, theme.Name, exercice.Title, exercice.Path)
DeepSyncProgress = 3 + uint8(tid)*themeStep + uint8(eid)*exerciceStep
errs = multierr.Append(errs, ImportExerciceFiles(i, exercice, ex_exceptions[eid]))
errs = multierr.Append(errs, SyncExerciceFiles(i, exercice, ex_exceptions[eid]))
DeepSyncProgress += exerciceStep / 3
flagsBindings, ferrs := SyncExerciceFlags(i, exercice, ex_exceptions[eid])

View file

@ -12,14 +12,6 @@ var gitRemoteRe = regexp.MustCompile(`^(?:(?:git@|https://)([\w.@]+)(?:/|:))((?:
var oneGitPull sync.Mutex
func OneGitPullStatus() bool {
if oneGitPull.TryLock() {
oneGitPull.Unlock()
return true
}
return false
}
func countFileInDir(dirname string) (int, error) {
files, err := os.ReadDir(dirname)
if err != nil {
@ -65,10 +57,6 @@ func (i GitImporter) Kind() string {
return "git originated from " + i.Remote + " on " + i.li.Kind()
}
func (i GitImporter) DeleteDir(filename string) error {
return i.li.DeleteDir(filename)
}
func getForgeBaseLink(remote string) (u *url.URL, err error) {
res := gitRemoteRe.FindStringSubmatch(remote)
u, err = url.Parse(res[2])

View file

@ -205,7 +205,7 @@ func (i GitImporter) GetThemeLink(th *fic.Theme) (u *url.URL, err error) {
return
}
u.Path = path.Join(u.Path, "-", "tree", i.Branch, strings.TrimPrefix("/"+th.Path, prefix))
u.Path = path.Join(u.Path, "-", "tree", i.Branch, strings.TrimPrefix(th.Path, prefix))
return
}
@ -241,7 +241,7 @@ func (i GitImporter) GetExerciceLink(e *fic.Exercice) (u *url.URL, err error) {
return
}
u.Path = path.Join(u.Path, "-", "tree", i.Branch, strings.TrimPrefix("/"+e.Path, prefix))
u.Path = path.Join(u.Path, "-", "tree", i.Branch, strings.TrimPrefix(e.Path, prefix))
return
}

View file

@ -113,11 +113,3 @@ func (i LocalImporter) ListDir(filename string) ([]string, error) {
func (i LocalImporter) Stat(filename string) (os.FileInfo, error) {
return os.Stat(path.Join(i.Base, filename))
}
type DeletableImporter interface {
DeleteDir(filename string) error
}
func (i LocalImporter) DeleteDir(filename string) error {
return os.RemoveAll(path.Join(i.Base, filename))
}

View file

@ -3,7 +3,9 @@ package sync
import (
"bytes"
"encoding/base32"
"io"
"net/url"
"os"
"path"
"strings"
@ -87,10 +89,27 @@ func (t *imageImporterTransformer) Transform(doc *ast.Document, reader text.Read
dPath := path.Join(fic.FilesDir, strings.ToLower(base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(t.hash[:])), iPath)
child.Destination = []byte(path.Join(t.absPath, string(child.Destination)))
err := importFile(t.importer, path.Join(t.rootDir, iPath), dPath)
if err != nil {
if err := os.MkdirAll(path.Dir(dPath), 0755); err != nil {
return ast.WalkStop, err
}
if fdto, err := os.Create(dPath); err != nil {
return ast.WalkStop, err
} else {
defer fdto.Close()
if fd, closer, err := GetFile(t.importer, path.Join(t.rootDir, iPath)); err != nil {
os.Remove(dPath)
return ast.WalkStop, err
} else {
defer closer()
_, err = io.Copy(fdto, fd)
if err != nil {
return ast.WalkStop, err
}
}
}
}
return ast.WalkContinue, nil

View file

@ -5,7 +5,6 @@ import (
"fmt"
"image"
"image/jpeg"
"io"
"math/rand"
"net/http"
"os"
@ -23,13 +22,15 @@ import (
"srs.epita.fr/fic-server/libfic"
)
const StandaloneExercicesDirectory = "exercices"
// GetThemes returns all theme directories in the base directory.
func GetThemes(i Importer) (themes []string, err error) {
if dirs, err := i.ListDir("/"); err != nil {
return nil, err
} else {
for _, dir := range dirs {
if !strings.HasPrefix(dir, ".") && !strings.HasPrefix(dir, "_") && dir != fic.StandaloneExercicesDirectory {
if !strings.HasPrefix(dir, ".") && !strings.HasPrefix(dir, "_") && dir != StandaloneExercicesDirectory {
if _, err := i.ListDir(dir); err == nil {
themes = append(themes, dir)
}
@ -40,34 +41,16 @@ func GetThemes(i Importer) (themes []string, err error) {
return themes, nil
}
// GetThemesExtended returns all theme directories, including standalone exercices.
func GetThemesExtended(i Importer) (themes []string, err error) {
themes, err = GetThemes(i)
if err != nil {
return
}
if i.Exists(fic.StandaloneExercicesDirectory) {
themes = append(themes, fic.StandaloneExercicesDirectory)
}
return
}
// resizePicture makes the given image just fill the given rectangle.
func resizePicture(i Importer, imgPath string, importedPath string, rect image.Rectangle) error {
if fl, err := i.GetFile(imgPath); err != nil {
func resizePicture(importedPath string, rect image.Rectangle) error {
if fl, err := os.Open(importedPath); err != nil {
return err
} else {
if src, _, err := image.Decode(fl); err != nil {
if flc, ok := fl.(io.ReadCloser); ok {
flc.Close()
}
fl.Close()
return err
} else if src.Bounds().Max.X > rect.Max.X && src.Bounds().Max.Y > rect.Max.Y {
if flc, ok := fl.(io.ReadCloser); ok {
flc.Close()
}
fl.Close()
mWidth := rect.Max.Y * src.Bounds().Max.X / src.Bounds().Max.Y
mHeight := rect.Max.X * src.Bounds().Max.Y / src.Bounds().Max.X
@ -80,7 +63,7 @@ func resizePicture(i Importer, imgPath string, importedPath string, rect image.R
dst := image.NewRGBA(rect)
draw.CatmullRom.Scale(dst, rect, src, src.Bounds(), draw.Over, nil)
dstFile, err := fileWriter(strings.TrimSuffix(importedPath, ".jpg") + ".thumb.jpg")
dstFile, err := os.Create(strings.TrimSuffix(importedPath, ".jpg") + ".thumb.jpg")
if err != nil {
return err
}
@ -90,7 +73,7 @@ func resizePicture(i Importer, imgPath string, importedPath string, rect image.R
return err
}
} else {
dstFile, err := fileWriter(strings.TrimSuffix(importedPath, ".jpg") + ".thumb.jpg")
dstFile, err := os.Create(strings.TrimSuffix(importedPath, ".jpg") + ".thumb.jpg")
if err != nil {
return err
}
@ -197,10 +180,8 @@ func BuildTheme(i Importer, tdir string) (th *fic.Theme, exceptions *CheckExcept
th.URLId = fic.ToURLid(th.Name)
if authors, err := getAuthors(i, tdir); err != nil {
if tdir != fic.StandaloneExercicesDirectory {
errs = multierr.Append(errs, NewThemeError(th, fmt.Errorf("unable to get AUTHORS.txt: %w", err)))
return nil, nil, errs
}
errs = multierr.Append(errs, NewThemeError(th, fmt.Errorf("unable to get AUTHORS.txt: %w", err)))
return nil, nil, errs
} else {
// Format authors
th.Authors = strings.Join(authors, ", ")
@ -292,36 +273,6 @@ func BuildTheme(i Importer, tdir string) (th *fic.Theme, exceptions *CheckExcept
return
}
// SyncThemeFiles import all theme's related files
func SyncThemeFiles(i Importer, btheme *fic.Theme) (errs error) {
if len(btheme.Image) > 0 {
if _, err := i.importFile(btheme.Image,
func(filePath string, origin string) (interface{}, error) {
if err := resizePicture(i, origin, filePath, image.Rect(0, 0, 500, 300)); err != nil {
return nil, err
}
btheme.Image = strings.TrimPrefix(filePath, fic.FilesDir)
btheme.BackgroundColor, _ = getBackgroundColor(filePath)
return nil, nil
}); err != nil {
errs = multierr.Append(errs, NewThemeError(btheme, fmt.Errorf("unable to import heading image: %w", err)))
}
}
if len(btheme.PartnerImage) > 0 {
if _, err := i.importFile(btheme.PartnerImage,
func(filePath string, origin string) (interface{}, error) {
btheme.PartnerImage = strings.TrimPrefix(filePath, fic.FilesDir)
return nil, nil
}); err != nil {
errs = multierr.Append(errs, NewThemeError(btheme, fmt.Errorf("unable to import partner image: %w", err)))
}
}
return
}
// SyncThemes imports new or updates existing themes.
func SyncThemes(i Importer) (exceptions map[string]*CheckExceptions, errs error) {
if themes, err := GetThemes(i); err != nil {
@ -343,9 +294,29 @@ func SyncThemes(i Importer) (exceptions map[string]*CheckExceptions, errs error)
exceptions[tdir] = excepts
err = SyncThemeFiles(i, btheme)
if err != nil {
errs = multierr.Append(errs, NewThemeError(btheme, fmt.Errorf("unable to import heading image: %w", err)))
if len(btheme.Image) > 0 {
if _, err := i.importFile(btheme.Image,
func(filePath string, origin string) (interface{}, error) {
if err := resizePicture(filePath, image.Rect(0, 0, 500, 300)); err != nil {
return nil, err
}
btheme.Image = strings.TrimPrefix(filePath, fic.FilesDir)
btheme.BackgroundColor, _ = getBackgroundColor(filePath)
return nil, nil
}); err != nil {
errs = multierr.Append(errs, NewThemeError(btheme, fmt.Errorf("unable to import heading image: %w", err)))
}
}
if len(btheme.PartnerImage) > 0 {
if _, err := i.importFile(btheme.PartnerImage,
func(filePath string, origin string) (interface{}, error) {
btheme.PartnerImage = strings.TrimPrefix(filePath, fic.FilesDir)
return nil, nil
}); err != nil {
errs = multierr.Append(errs, NewThemeError(btheme, fmt.Errorf("unable to import partner image: %w", err)))
}
}
var theme *fic.Theme
@ -384,34 +355,18 @@ func ApiListRemoteThemes(c *gin.Context) {
c.JSON(http.StatusOK, themes)
}
func GetRemoteTheme(thid string) (*fic.Theme, error) {
if thid == fic.StandaloneExercicesTheme.URLId || thid == fic.StandaloneExercicesDirectory {
return &fic.StandaloneExercicesTheme, nil
}
theme, _, errs := BuildTheme(GlobalImporter, thid)
if theme == nil {
return nil, errs
}
return theme, nil
}
// ApiListRemoteTheme is an accessor letting foreign packages to access remote main theme attributes.
func ApiGetRemoteTheme(c *gin.Context) {
var theme *fic.Theme
var err error
if c.Params.ByName("thid") == fic.StandaloneExercicesTheme.URLId {
theme, err = GetRemoteTheme(fic.StandaloneExercicesDirectory)
} else {
theme, err = GetRemoteTheme(c.Params.ByName("thid"))
}
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
if c.Params.ByName("thid") == "_" {
c.Status(http.StatusNoContent)
return
}
c.JSON(http.StatusOK, theme)
r, _, errs := BuildTheme(GlobalImporter, c.Params.ByName("thid"))
if r == nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Errorf("%q", errs)})
return
}
c.JSON(http.StatusOK, r)
}

View file

@ -90,7 +90,6 @@ func reloadSettings(config *settings.Settings) {
fic.GlobalScoreCoefficient = config.GlobalScoreCoefficient
fic.CountOnlyNotGoodTries = config.CountOnlyNotGoodTries
fic.DiscountedFactor = config.DiscountedFactor
fic.QuestionGainRatio = config.QuestionGainRatio
}
func main() {

View file

@ -20,24 +20,3 @@ ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCUKevt/f1n2byv5oH43iQsZ7b4kAATHlHNUF6WMQjk
# ?
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICo5yumHfQbMwhZAtEZByQR0xIVcoealS7g4MNTMEVaX roote@roote-VirtualBox
# cyril.blin
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCit9xqC+/EcC0D+x4Irg/AgnTx9rJDbE4FdKK2Z2vE0nSPzp1MbAtijVi5ndvr/JPlY3jUHeGEZBJHmADXeOvdJl1nvkqry/69phfr4nDYacvH0v66nDTRipqmCmebaYOkfXYG55oy40+6C6DwAGETTYq+PIaRcA/mSf6V9UxKBfnLVqdml7LFYEo1SbihAIFd0EZkwq1wevXdVmrDwF7VLiCin/5Axa6LUOe4l1SAYBpsV8pCY3PQ/KxpgCyJuYj2szhOl0shTPiV48f194xGtYrpx1uGhOHRDx6Rm/5LKY/5DUvKbHCa/ZAdUSoMTnd1TshAPJe1sYKSAAI1xPVmffOgF/Jh98QEuAuFmHfZXVgPdvApJ9r9Ea7gEyN6Xe37emkW1Dond4ARdNdaslVu0iwV6bQnDOGcEdAl3x3seRVRAiPAKp2tAEtVEqu7uFwX6v2mmpE5/uw8rfsl/wNgttjuYa/kJURNkto3bN02XNfzOnXXZ3bRtbNjHEyJQuM= cyril@nixos
# victor.chartraire
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJgDuQ4FrDuDjoo1Xv7pA0WEev1hhgJ8lXpT/17QXsds celian@DESKTOP-BQA641E
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPfUVWWSUrfuQYofWwvotQip4uqG34dF1Ybn4tTU1l0n celian@DESKTOP-BQA641E
# hugo.rubio
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC68cnL/66U9dKfGZe3Bfr+ggosVXZh5OymRuHEdl26lYr3uJAQ7tKtfgXXyfuIW87vxGuzZUOEAnDafJx0J24BVyTwNjJVRarB8efHVIvlL/S9hHPVKWaQyo1ZIgizCiRdljJLWweZKjRE6Yr8xPc6zlC7grQ0wG/WGtAKaedhxYUZcCkdxPMz9Jf2ufx23DB/ALUZ63KVwbWIof8LPtlBP3G74ooeuykR+BTH+6NinoOWlL94JsSTAhuVnKsSnG1eQ742Cv4XhhGleAStP3wFhyakdsvCOfw9bKN/z3dXA0nZGzwOqvHyI9OJx01GLHhyykfRL7PtpD8MMwzyq3r5 hugo0@LAPTOP-C4LN612T
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA1BINotXNgrTmHrep0BbxtUIAvCXOjSN8GrfTq03Sfd ryugo@LAPTOP-C4LN612T
# alexandra.delin
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOJGg902VI7w6nOSiGm6qyUnPn9w3sD3VKlMin3fbYK6 alex@AlexandraSRS
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIArw17NpbJRNf8m9eji5K+GcD8oiyJi/3ygceojEVtBL alexa@Alexandra_SRS
# maxence.michot
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCqiBbRBWhnG5+LU/pKevR5oJV3+2ZQRQ6Fu87F8873DkKP0okvAYqVXYEDIm/bq9+VocZHI8y9O+zbCXwIqdWRGESJFiM7r1L3HtimFlmWhzM+nzduS8dWGcGGa9XCw+OBHFMGUFEMyzNbi/YcF7QGybOGETdcyzXQng1JtW4IURdrFJm/0/EavRwO2O2xTexmHv8l7JAe0ChteeuDatADrWgNnoUXIqeQQIsPxKQJ9D4QZ7+QrD5K2iLCx8sqC8Eu10p7cRqB3pCcBzKc6MEOgA1FXJzYgNtWkkLR9s7cOVjKL6mL3DuYQla3VaSvkTh8SAKEkadgYXpYifF6ygL298EmhQIrqRdjDYAm+VDGfCvmtcEW4NM68tt74xLuhdDutLq9Riqx4n2pGPo8ws8ecCokcu5/ESGxpZwYmY+nMj8392yH4sgmW/nrs0WmAqawdwlDj09LIqbYXiGMd5zYYOu8V2nuMKG0zRtRESKbs04Uw2DUd/gTwfP2l5YqTn3K93xDNU2iZY+yLJai3vYaBLHzt6PEvESdhAceYBMTIN2iA6hGZfaMWJ4JHImoqCgwbnJw6qrIPHaj1SCWKWLPMjjYZbwndkH3Iy4oJ6gA6I/vT9YrzNvGYY7rVspa+O0RM+huVH1a0YoXSSzx6dop1k1ZWm7VwRCUJQ2ri+L2jw== thecr@maxence-portable-desktop
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMwyRf67AH/rUzSFsKpFfGDD/La4JqpYk9xCM04kJ6+P maxence@vokunaav
# leo.blanc-di-pasquale
ssh-rsa AAAAB4NzaC1yc2EAAAADAQABAAABgQDBovLxpJA0+gJiAVWrpWmjy9fyEQwOXv3Ytou7C4Y1O3lAyvmmHfBWuW1ZqIY/8yKIpJeSLCoGeN5kyMD9ITvMVcfSAxnDHXj04Q4rJzBGjQ/LF/CInx+HHjJWzGVgJC76m0BS7J0J7g5gAxfprGKEb3Z1kJmCTRQ470Azv6WIPDCw7aLNd1o5JqAsrmWSh/LwYBSapyp3Tk0KjcbxRifvnBJfYLHwNm1ZoaLCGAT3u3TKIifawEomx5QEel/211FaEdbglkeDtRL0YuvKQSuZCf4KSg7xSRQoWHmSuPothE9eQGzEKpJONKViqkxBo12O04cBEhYzQBh7GHmH3U3/9weNUX4EaKJWkfqh43eAnWohN07IXDnANYPRxWL4ITv+MNBWxYL5Zp3Zr85rhJZYhkyKEfcGjvolHaESegnsndhC74QW3eOXazuscAeROQbiMShk5iBIceTsEFTqI02/+XqTs9gXkuq3H3StJ6+9cavDwRyObGx5nVHmhQYnMEE= leo21@Aled

View file

@ -20,8 +20,6 @@ escape_newline () {
sed 's/$/\\n/g' | tr -d '\n'
}
which mkisofs > /dev/null 2> /dev/null || { echo "Please install genisoimage (Debian/Ubuntu) or cdrkit (Alpine)" >&2; exit 1; }
if [ $# -gt 0 ]
then
which jq > /dev/null 2> /dev/null || { echo "Please install jq" >&2; exit 1; }

View file

@ -77,7 +77,7 @@ server {
}
}
location ~ ^/([A-Z]|_/) {
location ~ ^/[A-Z_] {
include fic-auth.conf;
rewrite ^/.*$ /index.html;

View file

@ -63,7 +63,7 @@ server {
rewrite ^${FIC_BASEURL2}(.*)$ /$1;
}
location ~ ^/([A-Z]|_/) {
location ~ ^/[A-Z_] {
include fic-get-team.conf;
rewrite ^/.*$ /index.html;

View file

@ -67,7 +67,7 @@ server {
}
}
location ~ ^/([A-Z]|_/) {
location ~ ^/[A-Z_] {
include fic-get-team.conf;
rewrite ^/.*$ /index.html;

View file

@ -21,8 +21,6 @@ OLD_KEY=$(cat /run/config/dm-crypt/key)
[ "${NEW_KEY}" != "${OLD_KEY}" ] && {
read -p "DM-CRYPT key changed in metadata, are you sure you want to erase it? (y/N) " V
[ "$V" != "y" ] && [ "$V" != "Y" ] && while true; do
mv /boot/imgs/fickit-metadata.iso /boot/imgs/fickit-metadata.iso.skipped
cp /boot/imgs/fickit-metadata.iso.bak /boot/imgs/fickit-metadata.iso
echo
echo "Metadata drive not erased"
echo

View file

@ -365,7 +365,7 @@
<div class="carousel slide" data-interval="12000" style="padding-bottom: 0px" autocarousel>
<div class="carousel-inner">
<div class="carousel-item" ng-repeat="theme in themes" ng-class="{active: $first}">
<div class="carousel-caption text-indent" ng-if="theme.urlid !== '_'">
<div class="carousel-caption text-indent">
<div class="card-img-top theme-card" style="background-image: url('{{ theme.image.substr(0, theme.image.length-3) }}thumb.jpg')"></div>
<h3 class="text-left" ng-bind="theme.name"></h3>
<p class="text-justify" style="font-size: 111%" ng-bind-html="theme.headline"></p>
@ -377,8 +377,7 @@
</div>
<div class="card niceborder bg-dark" ng-if="s.type == 'exercice' && !s.params.hide">
<div class="card-img-top theme-card" style="background-image: url('{{exercices[s.params.exercice].image}}')" ng-if="exercices[s.params.exercice] && exercices[s.params.exercice].image"></div>
<div class="card-img-top theme-card" style="background-image: url('{{themes[my.exercices[s.params.exercice].theme_id].image}}')" ng-if="!exercices[s.params.exercice] || !exercices[s.params.exercice].image"></div>
<div class="card-img-top theme-card" style="background-image: url('{{themes[my.exercices[s.params.exercice].theme_id].image}}')"></div>
<div class="card-body text-light">
<h3 style="font-size: 1.0rem; text-weight: bold; overflow: hidden; text-overflow: ellipsis; white-space: nowrap">Défi <em>{{ exercices[s.params.exercice].title }}</em> du thème <em>{{ themes[my.exercices[s.params.exercice].theme_id].name }}</em></h3>
<p ng-bind-html="my.exercices[s.params.exercice].overview"></p>
@ -398,8 +397,7 @@
Challenges à la une
</span>
</div>
<div class="card-img-top theme-card" style="background-image: url('{{exercices[lastExercice].image}}')" ng-if="exercices[lastExercice] && exercices[lastExercice].image"></div>
<div class="card-img-top theme-card" style="background-image: url('{{themes[my.exercices[lastExercice].theme_id].image}}')" ng-if="!exercices[s.params.exercice] || !exercices[s.params.exercice].image"></div>
<div class="card-img-top theme-card" style="background-image: url('{{themes[my.exercices[lastExercice].theme_id].image}}')"></div>
<div class="card-body text-light">
<h3 style="font-size: 1.0rem; text-weight: bold; overflow: hidden; text-overflow: ellipsis; white-space: nowrap"><em>{{ exercices[lastExercice].title }}</em> du thème <em>{{ themes[my.exercices[lastExercice].theme_id].name }}</em></h3>
<p ng-bind-html="my.exercices[lastExercice].overview"></p>

View file

@ -50,7 +50,6 @@ services:
networks:
- fic-net
volumes:
- dashboard:/srv/DASHBOARD
- settings:/srv/SETTINGS
- settingsdist:/srv/SETTINGSDIST

View file

@ -1,54 +1,54 @@
kernel:
#image: nemunaire/kernel:5.10.62-0b705d955f5e283f62583c4e227d64a7924c138f-amd64
image: linuxkit/kernel:6.6.71
image: linuxkit/kernel:6.6.13
cmdline: "console=ttyS0 console=tty0"
init:
- linuxkit/init:8eea386739975a43af558eec757a7dcb3a3d2e7b
- linuxkit/runc:667e7ea2c426a2460ca21e3da065a57dbb3369c9
- linuxkit/containerd:a988a1a8bcbacc2c0390ca0c08f949e2b4b5915d
- linuxkit/ca-certificates:7b32a26ca9c275d3ef32b11fe2a83dbd2aee2fdb
- linuxkit/getty:05eca453695984a69617f1f1f0bcdae7f7032967
- linuxkit/init:v1.0.0
- linuxkit/runc:6062483d748609d505f2bcde4e52ee64a3329f5f
- linuxkit/containerd:v1.0.0
- linuxkit/ca-certificates:v1.0.0
- linuxkit/getty:v1.0.0
- nemunaire/mdadm:04814350d71ba9417e1f861be1685de26adf7a67
- nemunaire/kexec:839b4eedfce02a56c581dec2383dc6faff120855
onboot:
- name: mod
image: linuxkit/modprobe:773ee174006ecbb412830e48889795bae40b62f9
image: linuxkit/modprobe:v1.0.0
command: ["/bin/sh", "-c", "modprobe xhci_pci ahci intel_lpss_pci i2c_i801 megaraid_sas tg3 bnxt_en"]
- name: sysctl
image: linuxkit/sysctl:5f56434b81004b50b47ed629b222619168c2bcdf
image: linuxkit/sysctl:v1.0.0
binds:
- /etc/sysctl.d/01-fic.conf:/etc/sysctl.d/01-fic.conf:ro
# Metadata
- name: metadata
image: linuxkit/metadata:4f81c0c3a2b245567fd7d32d799018c9614a9907
image: linuxkit/metadata:v1.0.0
command: ["/usr/bin/metadata", "-v", "cdrom"]
# Filesystem
- name: swap
image: linuxkit/swap:f4b8ffef87c8c72165bd8a92b790ac252ccf1821
image: linuxkit/swap:v1.0.0
command: ["/sbin/swapon", "/dev/sda3"]
- name: dm-crypt
image: linuxkit/dm-crypt:981fde241bb84616a5ba94c04cdefa1489431a25
image: linuxkit/dm-crypt:d49723bc9d10c5ada9e03b0670f4e57416d5d084
command: ["/usr/bin/crypto", "-l", "crypt_fic", "/dev/sda4"]
binds:
- /dev:/dev
- /run/config/dm-crypt:/etc/dm-crypt
- name: mount
image: linuxkit/mount:cb8caa72248f7082fc2074ce843d53cdc15df04a
image: linuxkit/mount:v1.0.0
command: ["/usr/bin/mountie", "-device", "/dev/mapper/crypt_fic", "/var/lib/fic" ]
# Network
# - name: dhcpcd
# image: linuxkit/dhcpcd:157df9ef45a035f1542ec2270e374f18efef98a5
# image: linuxkit/dhcpcd:v1.0.0
# command: ["/sbin/dhcpcd", "--nobackground", "-f", "/dhcpcd.conf", "-1"]
# - name: ntp
# image: linuxkit/openntpd:f99c4117763480815553b72022b426639a13ce86
# image: linuxkit/openntpd:v1.0.0
- name: synchro-ip-setup
image: linuxkit/ip:9696394a7d57b384ae919662ae162c9152029156
image: linuxkit/ip:v1.0.0
command: ["/bin/sh", "-c", "ip a add 10.10.10.1/29 dev eth2; ip link set eth2 up;" ]
net: new
runtime:
@ -57,7 +57,7 @@ onboot:
bindNS:
net: /run/netns/synchro
- name: qa-ip-setup
image: linuxkit/ip:9696394a7d57b384ae919662ae162c9152029156
image: linuxkit/ip:v1.0.0
command: ["/bin/sh", "-c", "ip link show eth1 2> /dev/null && { ip a add 10.10.10.1/29 dev eth1; ip link set eth1 up; }; ip a add 172.17.0.6/24 dev vethin-qa; ip link set vethin-qa up" ]
net: new
runtime:
@ -69,7 +69,7 @@ onboot:
bindNS:
net: /run/netns/fic-qa
- name: admin-ip-setup
image: linuxkit/ip:9696394a7d57b384ae919662ae162c9152029156
image: linuxkit/ip:v1.0.0
#command: ["/bin/sh", "-c", "ip link add link eth3 name adminiface type vlan id 99; ip a add 172.16.99.219/24 dev adminiface; ip link set eth3 up; ip link set adminiface up; ip r add default via 172.16.99.1; ip a add 172.17.0.2/24 dev vethin-admin; ip link set vethin-admin up; ping -W 10 -c 1 172.16.99.1;" ]
command: ["/bin/sh", "-c", "ip link set eth3 up; while read IP; do ip a add ${IP} dev eth3; done < /run/config/ip_config/backend-admin; ip r add default via $(cat /run/config/ip_config/backend-router); ip a add 172.17.0.2/24 dev vethin-admin; ip link set vethin-admin up; echo 'Waiting for' $(cat /run/config/ip_config/backend-router); ping -W 10 -c 1 $(cat /run/config/ip_config/backend-router); ip link show eth1 2> /dev/null && { ip a add 10.0.0.1/24 dev eth1; ip link set eth1 up; };" ]
net: new
@ -85,7 +85,7 @@ onboot:
bindNS:
net: /run/netns/fic-admin
- name: checker-ip-setup
image: linuxkit/ip:9696394a7d57b384ae919662ae162c9152029156
image: linuxkit/ip:v1.0.0
command: ["/bin/sh", "-c", "ip a add 172.17.0.3/24 dev vethin-checker; ip link set vethin-checker up;" ]
net: new
runtime:
@ -96,7 +96,7 @@ onboot:
bindNS:
net: /run/netns/fic-checker
- name: generator-ip-setup
image: linuxkit/ip:9696394a7d57b384ae919662ae162c9152029156
image: linuxkit/ip:v1.0.0
command: ["/bin/sh", "-c", "ip a add 172.17.0.5/24 dev vethin-generat; ip link set vethin-generat up;" ]
net: new
runtime:
@ -107,7 +107,7 @@ onboot:
bindNS:
net: /run/netns/fic-generator
- name: mysql-ip-setup
image: linuxkit/ip:9696394a7d57b384ae919662ae162c9152029156
image: linuxkit/ip:v1.0.0
command: ["/bin/sh", "-c", "ip a add 172.17.0.4/24 dev vethin-db; ip link set vethin-db up;" ]
net: new
runtime:
@ -118,7 +118,7 @@ onboot:
bindNS:
net: /run/netns/db
- name: bridge-setup
image: linuxkit/ip:9696394a7d57b384ae919662ae162c9152029156
image: linuxkit/ip:v1.0.0
command: ["/bin/sh", "-c", "ip a add 172.17.0.1/24 dev br0; ip link set veth-admin master br0; ip link set veth-checker master br0; ip link set veth-generator master br0; ip link set veth-db master br0; ip link set veth-qa master br0; ip link set br0 up; ip link set veth-admin up; ip link set veth-checker up; ip link set veth-generator up; ip link set veth-db up; ip link set veth-qa up;" ]
runtime:
interfaces:
@ -126,7 +126,7 @@ onboot:
add: bridge
- name: firewall-synchro
image: linuxkit/ip:9696394a7d57b384ae919662ae162c9152029156
image: linuxkit/ip:v1.0.0
command: ["/bin/bash", "-c", "/sbin/iptables-restore < /etc/iptables/rules-synchro.v4; /sbin/ip6tables-restore < /etc/iptables/rules.v6" ]
binds:
- /etc/iptables/rules-synchro.v4:/etc/iptables/rules-synchro.v4:ro
@ -136,7 +136,7 @@ onboot:
mkdir:
- /var/lib/fic/teams
- name: firewall-admin
image: linuxkit/ip:9696394a7d57b384ae919662ae162c9152029156
image: linuxkit/ip:v1.0.0
command: ["/bin/bash", "-c", "/sbin/iptables-restore < /etc/iptables/rules-admin.v4; /sbin/ip6tables-restore < /etc/iptables/rules.v6" ]
binds:
- /etc/iptables/rules-admin.v4:/etc/iptables/rules-admin.v4:ro
@ -144,7 +144,7 @@ onboot:
net: /run/netns/fic-admin
- name: create-secrets
image: alpine:3.21
image: alpine:3.19
command: ["/bin/init_secrets.sh"]
binds:
- /bin/init_secrets.sh:/bin/init_secrets.sh:ro
@ -153,26 +153,17 @@ onboot:
mkdir:
- /var/lib/fic/secrets
- name: create-ssh-keys
image: nemunaire/rsync:a3d76b2dd0a9ad73be44dc77ad765b20d96a3285
command: ["/bin/sh", "-c", "touch /etc/ssh/sshd_config && ssh-keygen -A"]
binds:
- /var/lib/fic/ssh:/etc/ssh
runtime:
mkdir:
- /var/lib/fic/ssh
services:
# - name: getty
# image: linuxkit/getty:05eca453695984a69617f1f1f0bcdae7f7032967
# image: linuxkit/getty:v1.0.0
# env:
# - INSECURE=true
# Enable acpi to shutdown on power events
- name: acpid
image: linuxkit/acpid:6cb5575e487a8fcbd4c3eb6721c23299e6ea452f
image: linuxkit/acpid:v1.0.0
- name: rngd
image: linuxkit/rngd:1a18f2149e42a0a1cb9e7d37608a494342c26032
image: linuxkit/rngd:v1.0.0
- name: db
image: mariadb:11
command: ["/bin/bash", "/usr/local/bin/docker-entrypoint.sh", "mariadbd"]
@ -237,7 +228,7 @@ services:
- /var/lib/fic/generator:/srv/GENERATOR:ro
- /var/lib/fic/pki:/srv/PKI
- /var/lib/fic/settings:/srv/SETTINGS
- /var/lib/fic/submissions:/srv/submissions
- /var/lib/fic/submissions:/srv/submissions:ro
- /var/lib/fic/sync:/srv/SYNC
- /var/lib/fic/teams:/srv/TEAMS
net: /run/netns/fic-admin
@ -278,10 +269,7 @@ services:
binds:
- /etc/hosts:/etc/hosts:ro
- /var/lib/fic/generator:/srv/GENERATOR:ro
# Uncomment this to disallow registrations
- /var/lib/fic/teams:/srv/TEAMS:ro
# Uncomment this to allow registrations
#- /var/lib/fic/teams:/srv/TEAMS
- /var/lib/fic/secrets/mysql_password:/run/secrets/mysql_password:ro
- /var/lib/fic/settingsdist:/srv/SETTINGSDIST:ro
- /var/lib/fic/submissions:/srv/submissions
@ -373,6 +361,7 @@ services:
- /var/lib/fic/files
- /var/lib/fic/pki/shared
- /var/lib/fic/settingsdist
- /var/lib/fic/ssh
- /var/lib/fic/submissions
- /var/lib/fic/teams
- /var/log/frontend

View file

@ -1,6 +1,6 @@
kernel:
#image: nemunaire/kernel:5.10.62-0b705d955f5e283f62583c4e227d64a7924c138f-amd64
image: linuxkit/kernel:6.6.71
image: linuxkit/kernel:6.6.13
cmdline: "console=ttyS0 console=tty0"
init:

View file

@ -1,50 +1,50 @@
kernel:
#image: nemunaire/kernel:5.10.62-0b705d955f5e283f62583c4e227d64a7924c138f-amd64
image: linuxkit/kernel:6.6.71
image: linuxkit/kernel:6.6.13
cmdline: "console=ttyS0 console=tty0"
init:
- linuxkit/init:8eea386739975a43af558eec757a7dcb3a3d2e7b
- linuxkit/runc:667e7ea2c426a2460ca21e3da065a57dbb3369c9
- linuxkit/containerd:a988a1a8bcbacc2c0390ca0c08f949e2b4b5915d
- linuxkit/ca-certificates:7b32a26ca9c275d3ef32b11fe2a83dbd2aee2fdb
- linuxkit/getty:05eca453695984a69617f1f1f0bcdae7f7032967
- linuxkit/init:v1.0.0
- linuxkit/runc:6062483d748609d505f2bcde4e52ee64a3329f5f
- linuxkit/containerd:v1.0.0
- linuxkit/ca-certificates:v1.0.0
- linuxkit/getty:v1.0.0
- nemunaire/mdadm:04814350d71ba9417e1f861be1685de26adf7a67
- nemunaire/kexec:839b4eedfce02a56c581dec2383dc6faff120855
- nemunaire/fic-frontend-ui:latest
onboot:
- name: mod
image: linuxkit/modprobe:773ee174006ecbb412830e48889795bae40b62f9
image: linuxkit/modprobe:v1.0.0
command: ["/bin/sh", "-c", "modprobe xhci_pci ahci intel_lpss_pci i2c_i801 megaraid_sas tg3 bnxt_en"]
- name: sysctl
image: linuxkit/sysctl:5f56434b81004b50b47ed629b222619168c2bcdf
image: linuxkit/sysctl:v1.0.0
# Metadata
- name: metadata
image: linuxkit/metadata:4f81c0c3a2b245567fd7d32d799018c9614a9907
image: linuxkit/metadata:v1.0.0
command: ["/usr/bin/metadata", "-v", "cdrom"]
# Filesystem
- name: swap
image: linuxkit/swap:f4b8ffef87c8c72165bd8a92b790ac252ccf1821
image: linuxkit/swap:v1.0.0
command: ["/sbin/swapon", "/dev/sda3"]
- name: dm-crypt
image: linuxkit/dm-crypt:981fde241bb84616a5ba94c04cdefa1489431a25
image: linuxkit/dm-crypt:d49723bc9d10c5ada9e03b0670f4e57416d5d084
command: ["/usr/bin/crypto", "-l", "crypt_fic", "/dev/sda4"]
binds:
- /dev:/dev
- /run/config/dm-crypt:/etc/dm-crypt
- name: mount
image: linuxkit/mount:cb8caa72248f7082fc2074ce843d53cdc15df04a
image: linuxkit/mount:v1.0.0
command: ["/usr/bin/mountie", "-device", "/dev/mapper/crypt_fic", "/var/lib/fic" ]
# Network
# - name: ntp
# image: linuxkit/openntpd:f99c4117763480815553b72022b426639a13ce86
# image: linuxkit/openntpd:v1.0.0
- name: nginx-ip-setup
image: linuxkit/ip:9696394a7d57b384ae919662ae162c9152029156
image: linuxkit/ip:v1.0.0
command: ["/bin/sh", "-c", "ip a add 172.17.1.2/24 dev vethin-nginx; ip link set vethin-nginx up;" ]
net: new
runtime:
@ -55,7 +55,7 @@ onboot:
bindNS:
net: /run/netns/nginx
- name: frontal-ip-setup # without bonding
image: linuxkit/ip:9696394a7d57b384ae919662ae162c9152029156
image: linuxkit/ip:v1.0.0
command: ["/bin/sh", "-c", "ip link set name bond-frontal eth3; ip link set bond-frontal up; while read IP; do ip a add ${IP} dev bond-frontal; done < /run/config/ip_config/frontend-players; ip r add default via $(cat /run/config/ip_config/frontend-router); ip link add link bond-frontal name internet type vlan id 4; ip a add 10.10.10.2/29 dev internet; ip link set internet up;" ]
net: /run/netns/nginx
binds:
@ -67,7 +67,7 @@ onboot:
- name: eth3
# - name: eth4
# - name: frontal-ip-setup # with bonding
# image: linuxkit/ip:9696394a7d57b384ae919662ae162c9152029156
# image: linuxkit/ip:v1.0.0
# command: ["/bin/sh", "-c", "ip link set dev bond-frontal type bond mode balance-alb; ip link set bond-frontal up; ifenslave bond-frontal eth1 eth2 eth3 eth4; while read IP; do ip a add ${IP} dev bond-frontal; done < /run/config/ip_config/frontend-players; ip r add default via $(cat /run/config/ip_config/frontend-router); ip link add link bond-frontal name internet type vlan id 4; ip link set internet up; sysctl -w net.ipv4.ip_forward=1;" ]
# net: /run/netns/nginx
# binds:
@ -81,7 +81,7 @@ onboot:
# - name: bond-frontal
# add: bond
- name: receiver-ip-setup
image: linuxkit/ip:9696394a7d57b384ae919662ae162c9152029156
image: linuxkit/ip:v1.0.0
command: ["/bin/sh", "-c", "ip a add 172.17.1.3/24 dev vethin-receiver; ip link set vethin-receiver up;" ]
net: new
runtime:
@ -92,7 +92,7 @@ onboot:
bindNS:
net: /run/netns/fic-receiver
- name: sshd-ip-setup
image: linuxkit/ip:9696394a7d57b384ae919662ae162c9152029156
image: linuxkit/ip:v1.0.0
command: ["/bin/sh", "-c", "ip a add 10.10.10.2/29 dev eth2; ip link set eth2 up;" ]
net: new
runtime:
@ -101,7 +101,7 @@ onboot:
bindNS:
net: /run/netns/sshd
- name: auth-ip-setup
image: linuxkit/ip:9696394a7d57b384ae919662ae162c9152029156
image: linuxkit/ip:v1.0.0
command: ["/bin/sh", "-c", "ip a add 172.17.1.4/24 dev vethin-auth; ip link set vethin-auth up;" ]
net: new
runtime:
@ -112,7 +112,7 @@ onboot:
bindNS:
net: /run/netns/auth
- name: bridge-setup
image: linuxkit/ip:9696394a7d57b384ae919662ae162c9152029156
image: linuxkit/ip:v1.0.0
command: ["/bin/sh", "-c", "ip a add 172.17.1.1/24 dev br0; ip link set veth-nginx master br0; ip link set veth-receiver master br0; ip link set veth-auth master br0; ip link set br0 up; ip link set veth-nginx up; ip link set veth-receiver up; ip link set veth-auth up;" ]
runtime:
interfaces:
@ -120,7 +120,7 @@ onboot:
add: bridge
- name: firewall-frontal
image: linuxkit/ip:9696394a7d57b384ae919662ae162c9152029156
image: linuxkit/ip:v1.0.0
command: ["/bin/bash", "-c", "/sbin/iptables-restore < /etc/iptables/rules-frontal.v4; /sbin/ip6tables-restore < /etc/iptables/rules.v6; [ -f /run/config/remote_sync/destination ] && /sbin/iptables -I OUTPUT 7 -o bond-frontal -d $(cat /run/config/remote_sync/destination | tr -d '\n') -p tcp -m tcp --dport https -j ACCEPT;" ]
binds:
- /etc/iptables/rules-frontal.v4:/etc/iptables/rules-frontal.v4:ro
@ -129,35 +129,26 @@ onboot:
- /run/config/remote_sync/:/run/config/remote_sync/:ro
net: /run/netns/nginx
- name: firewall-sshd
image: linuxkit/ip:9696394a7d57b384ae919662ae162c9152029156
image: linuxkit/ip:v1.0.0
command: ["/bin/bash", "-c", "/sbin/iptables-restore < /etc/iptables/rules-sshd.v4; /sbin/ip6tables-restore < /etc/iptables/rules.v6" ]
binds:
- /etc/iptables/rules-sshd.v4:/etc/iptables/rules-sshd.v4:ro
- /etc/iptables/rules.v6:/etc/iptables/rules.v6:ro
net: /run/netns/sshd
- name: create-ssh-keys
image: nemunaire/rsync:a3d76b2dd0a9ad73be44dc77ad765b20d96a3285
command: ["/bin/sh", "-c", "touch /etc/ssh/sshd_config && ssh-keygen -A"]
binds:
- /var/lib/fic/ssh:/etc/ssh
runtime:
mkdir:
- /var/lib/fic/ssh
services:
# - name: getty
# image: linuxkit/getty:05eca453695984a69617f1f1f0bcdae7f7032967
# image: linuxkit/getty:v1.0.0
# env:
# - INSECURE=true
# Enable acpi to shutdown on power events
- name: acpid
image: linuxkit/acpid:6cb5575e487a8fcbd4c3eb6721c23299e6ea452f
image: linuxkit/acpid:v1.0.0
- name: rngd
image: linuxkit/rngd:1a18f2149e42a0a1cb9e7d37608a494342c26032
image: linuxkit/rngd:v1.0.0
- name: dhcpcd
image: linuxkit/dhcpcd:157df9ef45a035f1542ec2270e374f18efef98a5
image: linuxkit/dhcpcd:v1.0.0
net: /run/netns/nginx
binds:
- /etc/dhcpcd.conf:/dhcpcd.conf:ro
@ -266,6 +257,7 @@ services:
- /var/lib/fic/files
- /var/lib/fic/pki
- /var/lib/fic/settingsdist
- /var/lib/fic/ssh
- /var/lib/fic/submissions
- /var/lib/fic/teams
@ -288,7 +280,7 @@ services:
# net: /run/netns/nginx
- name: dexidp
image: ghcr.io/dexidp/dex:v2.42.0
image: ghcr.io/dexidp/dex:v2.39.0
net: /run/netns/auth
binds:
- /etc/hosts:/etc/hosts:ro
@ -302,7 +294,7 @@ services:
mkdir:
- /var/lib/fic/dex
- name: vouch-proxy
image: quay.io/vouch/vouch-proxy:alpine-0.41
image: quay.io/vouch/vouch-proxy:alpine-0.39
env:
- VOUCH_CONFIG=/etc/vouch/config.yml
net: /run/netns/auth

View file

@ -1,15 +1,15 @@
kernel:
#image: nemunaire/kernel:5.10.62-0b705d955f5e283f62583c4e227d64a7924c138f-amd64
image: linuxkit/kernel:6.6.71
image: linuxkit/kernel:6.6.13
cmdline: "console=ttyS0 console=tty0"
init:
- nemunaire/mdadm:04814350d71ba9417e1f861be1685de26adf7a67
- nemunaire/syslinux:086f221f281d577d300949aa1094fb20c5cd90dc
- linuxkit/format:3fb088f60ed73ba4a15be41e44654b74112fd3f9
- linuxkit/dm-crypt:981fde241bb84616a5ba94c04cdefa1489431a25
- linuxkit/metadata:4f81c0c3a2b245567fd7d32d799018c9614a9907
- linuxkit/format:v1.0.0
- linuxkit/dm-crypt:d49723bc9d10c5ada9e03b0670f4e57416d5d084
- linuxkit/metadata:v1.0.0
- alpine:latest
files:

View file

@ -1,12 +1,12 @@
kernel:
#image: nemunaire/kernel:5.10.62-0b705d955f5e283f62583c4e227d64a7924c138f-amd64
image: linuxkit/kernel:6.6.71
image: linuxkit/kernel:6.6.13
cmdline: "console=ttyS0 console=tty0"
init:
- nemunaire/mdadm:04814350d71ba9417e1f861be1685de26adf7a67
- linuxkit/metadata:4f81c0c3a2b245567fd7d32d799018c9614a9907
- linuxkit/metadata:v1.0.0
- alpine:latest

View file

@ -1 +0,0 @@
fileexporter

View file

@ -1,58 +0,0 @@
package main
import (
"archive/zip"
"errors"
"io"
"log"
"os"
"path"
)
type archiveFileCreator interface {
Create(name string) (io.Writer, error)
Close() error
}
type filesCloser []io.Closer
func (fds filesCloser) Close() error {
log.Println("Closing fd..")
for _, fd := range fds {
err := fd.Close()
if err != nil {
return err
}
}
return nil
}
func init() {
OutputFormats["archive"] = func(args ...string) (func(string) (io.WriteCloser, error), io.Closer, error) {
if len(args) != 1 {
return nil, nil, errors.New("archive has 1 required argument: [destination-file]")
}
fd, err := os.Create(args[0])
if err != nil {
return nil, nil, err
}
var w archiveFileCreator
if path.Ext(args[0]) == ".zip" {
w = zip.NewWriter(fd)
} else {
return nil, nil, errors.New("destination file has to have .zip extension")
}
return func(dest string) (io.WriteCloser, error) {
fw, err := w.Create(dest)
if err != nil {
return nil, err
}
return NopCloser(fw), nil
}, filesCloser{w, fd}, nil
}
}

View file

@ -1,22 +0,0 @@
package main
import (
"errors"
"io"
"srs.epita.fr/fic-server/libfic"
)
func init() {
OutputFormats["copy"] = func(args ...string) (func(string) (io.WriteCloser, error), io.Closer, error) {
if len(args) > 1 {
return nil, nil, errors.New("copy can only take 1 argument: [destination-folder]")
}
if len(args) == 1 {
fic.FilesDir = args[0]
}
return nil, nil, nil
}
}

View file

@ -1,184 +0,0 @@
package main
import (
"bytes"
"errors"
"flag"
"io"
"log"
"os"
"path"
"strings"
"srs.epita.fr/fic-server/admin/sync"
"srs.epita.fr/fic-server/libfic"
)
var OutputFormats = map[string]func(...string) (func(string) (io.WriteCloser, error), io.Closer, error){}
func exportThemeFiles(tdir string) (errs error) {
theme, exceptions, err := sync.BuildTheme(sync.GlobalImporter, tdir)
errs = errors.Join(errs, err)
err = sync.SyncThemeFiles(sync.GlobalImporter, theme)
if err != nil {
errs = errors.Join(errs, err)
}
exercices, err := sync.GetExercices(sync.GlobalImporter, theme)
if err != nil {
log.Fatalf("Unable to list exercices for theme %q: %s", theme.Name, err)
}
dmap := map[int64]*fic.Exercice{}
for i, edir := range exercices {
log.Printf("In theme %s, doing exercice %d/%d: %s", tdir, i+1, len(exercices), edir)
err = exportExerciceFiles(theme, edir, &dmap, exceptions)
errs = errors.Join(errs, err)
}
return
}
func exportExerciceFiles(theme *fic.Theme, edir string, dmap *map[int64]*fic.Exercice, exceptions *sync.CheckExceptions) (errs error) {
exercice, _, eid, exceptions, _, berrs := sync.BuildExercice(sync.GlobalImporter, theme, path.Join(theme.Path, edir), dmap, nil)
errs = errors.Join(errs, berrs)
if exercice != nil {
paramsFiles, err := sync.GetExerciceFilesParams(sync.GlobalImporter, exercice)
if err != nil {
errs = errors.Join(errs, sync.NewChallengeTxtError(exercice, 0, err))
return
}
_, err = sync.SyncExerciceFiles(sync.GlobalImporter, exercice, paramsFiles, func(fname string, digests map[string][]byte, filePath, origin string) (interface{}, error) {
return nil, nil
})
errs = errors.Join(errs, err)
if dmap != nil {
(*dmap)[int64(eid)] = exercice
}
}
return
}
type nopCloser struct {
w io.Writer
}
func (nc *nopCloser) Close() error {
return nil
}
func (nc *nopCloser) Write(p []byte) (int, error) {
return nc.w.Write(p)
}
func NopCloser(w io.Writer) *nopCloser {
return &nopCloser{w}
}
func writeFileToTar(dest string) (io.WriteCloser, error) {
log.Println("import2Tar", dest)
return NopCloser(bytes.NewBuffer([]byte{})), nil
}
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.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.Parse()
// Do not display timestamp
log.SetFlags(0)
// Instantiate importer
if localImporterDirectory != "" {
sync.GlobalImporter = sync.LocalImporter{Base: localImporterDirectory, Symlink: false}
} else if cloudDAVBase != "" {
sync.GlobalImporter, _ = sync.NewCloudImporter(cloudDAVBase, cloudUsername, cloudPassword)
}
if sync.GlobalImporter == nil {
log.Fatal("No importer configured!")
}
log.Println("Using", sync.GlobalImporter.Kind())
hasError := doExport()
if hasError {
os.Exit(1)
}
}
func doExport() bool {
// Configure destination
if flag.NArg() < 1 {
var formats []string
for k := range OutputFormats {
formats = append(formats, k)
}
log.Fatal("Please define wanted output format between [" + strings.Join(formats, " ") + "]")
} else if outputFormat, ok := OutputFormats[flag.Arg(0)]; !ok {
var formats []string
for k := range OutputFormats {
formats = append(formats, k)
}
log.Fatal("Please define wanted output format between [" + strings.Join(formats, " ") + "]")
} else {
fw, closer, err := outputFormat(flag.Args()[1:]...)
if closer != nil {
defer closer.Close()
}
if err != nil {
log.Fatal(err)
} else if fw != nil {
sync.SetWriteFileFunc(fw)
}
}
themes, err := sync.GetThemesExtended(sync.GlobalImporter)
if err != nil {
log.Fatal(err)
}
hasError := false
for i, tdir := range themes {
log.Printf("Doing theme %d/%d: %s", i+1, len(themes), tdir)
err = exportThemeFiles(tdir)
if err != nil {
hasError = true
log.Println(err)
}
}
return hasError
}

File diff suppressed because it is too large Load diff

View file

@ -12,14 +12,14 @@
"@sveltejs/adapter-static": "^3.0.0",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@sveltestrap/sveltestrap": "^7.0.0",
"eslint": "^9.0.0",
"eslint-config-prettier": "^10.0.0",
"eslint-plugin-svelte": "^3.0.0",
"@sveltestrap/sveltestrap": "^6.2.1",
"eslint": "^8.4.2",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-svelte": "^2.35.1",
"prettier": "^3.0.0",
"prettier-plugin-svelte": "^3.1.2",
"sass": "^1.51.0",
"sass-loader": "^16.0.0",
"sass-loader": "^14.0.0",
"svelte": "^4.0.0",
"vite": "^5.0.0"
},

View file

@ -44,7 +44,7 @@
style:filter={theme.locked ? "grayscale(60%)":null}
></div>
{/if}
<CardBody>
<CardBody class="text-indent">
{#if exercice}
{#if $exercices_idx[exercice.id].tags.includes("Reverse") || $exercices_idx[exercice.id].tags.includes("Reverse Engineering")}
<Badge class="float-end">#Reverse</Badge>

View file

@ -9,7 +9,6 @@
ListGroupItem,
} from '@sveltestrap/sveltestrap';
import { hasDownloaded } from '$lib/stores/downloaded.js';
import FileSize from './FileSize.svelte';
export let files = [];
@ -28,15 +27,15 @@
</CardBody>
<ListGroup flush class="border-secondary">
{#each files as file, index}
<ListGroupItem tag="a" href={file.path} target={(file.name.endsWith(".txt") || file.name.endsWith(".xml") || file.name.endsWith(".jpg") || file.name.endsWith(".png") || file.name.endsWith(".pdf"))?"_blank":"_self"} class="d-flex" action on:click={() => hasDownloaded.update((u) => {u[file.path] = true; return u;})}>
<h1 class="me-3" class:text-info={!$hasDownloaded[file.path]}>
<ListGroupItem tag="a" href={file.path} target={(file.name.endsWith(".txt") || file.name.endsWith(".jpg") || file.name.endsWith(".png") || file.name.endsWith(".pdf"))?"_blank":"_self"} class="d-flex">
<h1 class="me-3">
<Icon name="arrow-down-circle" />
</h1>
<div style="min-width: 0">
<h4 class="fw-bold"><samp>{file.name}</samp></h4>
{#if file.disclaimer}
<div class="file-disclaimer text-warning">
{file.disclaimer}
{#if file.disclamer}
<div class="file-disclamer text-warning">
{file.disclamer}
</div>
{/if}
<nobr>
@ -62,10 +61,10 @@
{/if}
<style>
.file-disclaimer {
.file-disclamer {
display: none;
}
:global(.list-group-item:hover .file-disclaimer) {
:global(.list-group-item:hover .file-disclamer) {
display: block;
}
</style>

View file

@ -17,7 +17,6 @@
import { my } from '$lib/stores/my.js';
import { settings } from '$lib/stores/settings.js';
import { submissions } from '$lib/stores/submissions.js';
import { timeouted, waitDiff, waitInProgress } from '$lib/wait.js';
import DateFormat from './DateFormat.svelte';
@ -29,32 +28,11 @@
export let readonly = false;
export let forcesolved = false;
let last_submission = { };
let responses = { };
async function submitFlags() {
waitInProgress.set(true);
sberr = "";
message = "";
last_submission = JSON.parse(JSON.stringify(responses));
submissions.update((u) => {
for (const k in last_submission.flags) {
if (last_submission.flags[k])
u.flags[k] = last_submission.flags[k];
}
for (const k in last_submission.mcqs) {
if (last_submission.mcqs[k])
u.mcqs[k] = last_submission.mcqs[k];
}
for (const k in last_submission.justifications) {
if (last_submission.justifications[k])
u.justifications[k] = last_submission.justifications[k];
}
return u;
})
if ($my && $my.team_id === 0) {
let allGoodResponse = true;
@ -141,11 +119,6 @@
mcqs: { },
justifications: { },
};
last_submission = {
flags: { },
mcqs: { },
justifications: { },
};
}
let last_exercice = null;
@ -179,12 +152,10 @@
{#if exercice.tries || exercice.solved_time || exercice.submitted || sberr || $timeouted}
<ListGroup class="border-dark">
{#if exercice.solved_time || exercice.tries}
<div class="d-flex align-items-center">
<ListGroupItem class="rounded-0 {$waitInProgress?'text-secondary':'text-warning'}">
{#if exercice.tries > 0}{exercice.tries} {exercice.tries==1?"tentative effectuée":"tentatives effectuées"}.{/if}
Dernière solution envoyée le <span class:placeholder={$waitInProgress} class:placeholder-glow={$waitInProgress}><DateFormat date={exercice.solved_time} /></span>.
</ListGroupItem>
</div>
<ListGroupItem class="text-warning rounded-0">
{#if exercice.tries > 0}{exercice.tries} {exercice.tries==1?"tentative effectuée":"tentatives effectuées"}.{/if}
Dernière solution envoyée à <DateFormat date={exercice.solved_time} />.
</ListGroupItem>
{/if}
{#if exercice.solve_dist}
<ListGroupItem class="rounded-0">
@ -220,7 +191,6 @@
<FlagMCQ
exercice_id={exercice.id}
{flag}
previous_values={$waitInProgress ? { justifications: { }, mcqs: { } } : last_submission}
bind:values={responses.mcqs}
bind:justifications={responses.justifications}
/>
@ -229,7 +199,6 @@
class="mb-3"
exercice_id={exercice.id}
{flag}
previous_value={$waitInProgress ? "" : last_submission.flags[flag.id]}
bind:value={responses.flags[flag.id]}
/>
{/if}

View file

@ -9,7 +9,6 @@
import { my } from '$lib/stores/my.js';
import { settings } from '$lib/stores/settings.js';
import { submissions } from '$lib/stores/submissions.js';
export { className as class };
let className = '';
@ -17,7 +16,6 @@
export let exercice_id = 0;
export let flag = { };
export let no_label = false;
export let previous_value = "";
export let value = "";
let values = [""];
@ -143,7 +141,6 @@
<input
type="number"
class="form-control flag"
class:is-invalid={previous_value && previous_value == value}
id="sol_{flag.type}{flag.id}_{index}"
autocomplete="off"
bind:value={values[index]}
@ -157,7 +154,6 @@
<input
type="text"
class="form-control flag"
class:is-invalid={previous_value && previous_value == value}
id="sol_{flag.type}{flag.id}_{index}"
autocomplete="off"
bind:value={values[index]}
@ -168,7 +164,6 @@
{:else}
<textarea
class="form-control flag"
class:is-invalid={previous_value && previous_value == value}
id="sol_{flag.type}{flag.id}_{index}"
autocomplete="off"
bind:value={values[index]}
@ -213,7 +208,6 @@
value={l}
bind:group={values[index]}
class="form-check-input"
class:is-invalid={previous_value && previous_value == value}
>
<label
class="form-check-label"
@ -226,7 +220,6 @@
{:else}
<select
class="form-select"
class:is-invalid={previous_value && previous_value == value}
id="sol_{flag.type}{flag.id}_{index}"
bind:value={values[index]}
>
@ -248,15 +241,6 @@
title="Flag trouvé à {flag.found}"
value={value}
>
{:else if $submissions && $submissions.flags && $submissions.flags[flag.id]}
<input
class="form-control is-valid"
disabled
id="sol_{flag.type}{flag.id}_0"
type="text"
title="Flag trouvé à {flag.found}"
value={$submissions.flags[flag.id]}
>
{:else}
<Icon
name="check"

View file

@ -8,7 +8,6 @@
export let exercice_id = 0;
export let flag = { };
export let previous_values = { justifications: { }, mcqs: { } };
export let values = { };
export let justifications = { };
</script>
@ -25,7 +24,7 @@
{#each Object.keys(flag.choices) as cid, index}
<div class="form-check ms-3">
{#if typeof flag.choices[cid] != "object"}
<input class="form-check-input" class:is-invalid={previous_values.mcqs && Object.keys(flag.choices).reduce((acc, cur) => acc + (previous_values.mcqs[Number(cur)] !== undefined ? 1 : 0), 0) > 0 && Object.keys(flag.choices).reduce((acc, cur) => acc && previous_values.mcqs[Number(cur)] == values[Number(cur)], true)} type="checkbox" id="mcq_{flag.id}_{cid}" bind:checked={values[Number(cid)]} disabled={flag.found || flag.part_solved}>
<input class="form-check-input" type="checkbox" id="mcq_{flag.id}_{cid}" bind:checked={values[Number(cid)]} disabled={flag.found || flag.part_solved}>
<label class="form-check-label" for="mcq_{flag.id}_{cid}">
{flag.choices[cid]}{#if values[Number(cid)] && flag.justify}&nbsp;:{/if}
</label>
@ -35,7 +34,6 @@
{exercice_id}
flag={{id: cid, placeholder: "Flag correspondant"}}
no_label={true}
previous_value={previous_values.justifications && previous_values.justifications[cid]}
bind:value={justifications[cid]}
/>
{/if}
@ -45,7 +43,6 @@
class={flag.choices[cid].justification.found?"":"mb-3"}
{exercice_id}
flag={flag.choices[cid].justification}
previous_value={previous_values.justifications[cid]}
bind:value={justifications[cid]}
/>
{/if}

View file

@ -1,6 +1,5 @@
<script>
import { base } from '$app/paths';
import { page } from '$app/stores';
import {
Badge,
@ -50,7 +49,7 @@
<a href="." style="max-width: 50%">
{#if $challengeInfo && $challengeInfo.main_logo}
{#each $challengeInfo.main_logo as logo, i}
<img src={logo.replace('$FILES$/', base + '/files/')} alt={'Logo principal #' + i} class={'h-100' + (i > 0?' d-none d-md-inline ms-2':'')}>
<img src={logo.replace('$FILES$', base + '/files/')} alt={'Logo principal #' + i} class={'h-100' + (i > 0?' d-none d-md-inline ms-2':'')}>
{/each}
{/if}
</a>
@ -63,7 +62,7 @@
<NavbarToggler on:click={() => (isOpen = !isOpen)} />
<Collapse {isOpen} navbar expand="md" on:update={handleUpdate}>
<Nav navbar>
<NavItem active={$page.route && $page.route.id === "/"}>
<NavItem>
<NavLink href=".">
<Icon name="house" />
Accueil
@ -72,7 +71,7 @@
<NavThemes />
<NavTags />
{#if $settings && $settings.end - $settings.start > 0 && $teams && Object.keys($teams).length}
<NavItem active={$page.route && $page.route.id === "/rank"}>
<NavItem>
<NavLink href="rank">
<Icon name="sort-down" />
Classement
@ -80,7 +79,7 @@
</NavItem>
{/if}
<HeaderIssues />
<NavItem active={$page.route && $page.route.id === "/rules"}>
<NavItem>
<NavLink href="rules">
<Icon name="signpost-split" />
Aide
@ -95,7 +94,7 @@
<Nav class="ms-auto text-light" navbar>
{#if $my && $my.team_id}
<NavItem>
{$my.score100/100} {Math.abs($my.score) < 2 ? 'point' : 'points'}
{Math.round($my.score*100)/100} {$my.score === 1 ? 'point' : 'points'}
{#if $teams && $teams[$my.team_id] && $teams[$my.team_id].rank}
&ndash; {$teams[$my.team_id].rank}<sup>e</sup> sur {Object.keys($teams).length}
{/if}

View file

@ -1,6 +1,4 @@
<script>
import { page } from '$app/stores';
import {
Badge,
Icon,
@ -26,7 +24,7 @@
</script>
{#if $issues.length}
<NavItem active={$page.route && $page.route.id === "/issues"}>
<NavItem>
<NavLink href="issues">
<Icon name="bug" />
Problèmes

View file

@ -1,6 +1,4 @@
<script>
import { page } from '$app/stores';
import {
Badge,
Dropdown,
@ -16,7 +14,7 @@
let filter = "";
</script>
<Dropdown nav inNavbar active={$page.params.tag}>
<Dropdown nav inNavbar>
<DropdownToggle nav caret>
<Icon name="tags" />
Tags
@ -29,8 +27,8 @@
bind:value={filter}
>
<div>
{#each Object.keys($tags).sort(function (a, b) { return a.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().localeCompare(b.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase()); }) as itag, index}
{#if (filter === "" && $tags[itag].count > 1) || (filter !== "" && itag.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().indexOf(filter.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase()) >= 0)}
{#each Object.keys($tags).sort(function (a, b) { return a.toLowerCase().localeCompare(b.toLowerCase()); }) as itag, index}
{#if (filter === "" && $tags[itag].count > 1) || (filter !== "" && itag.toLowerCase().indexOf(filter.toLowerCase()) >= 0)}
<DropdownItem href="tags/{itag}">
#{itag}
<Badge>

View file

@ -14,7 +14,6 @@
import { myThemes, themes } from '$lib/stores/mythemes.js';
</script>
{#if $themes.length > 0 && ($themes[0].id != 0 || $themes.length > 1)}
<Dropdown nav inNavbar active={$current_theme && $current_theme.id != 0}>
<DropdownToggle nav caret>
<Icon name="tv" />
@ -55,7 +54,6 @@
</div>
</DropdownMenu>
</Dropdown>
{/if}
{#if $themesStore && $themesStore["0"] && $themesStore["0"].exercices}
<Dropdown nav inNavbar active={$current_theme && $current_theme && $current_theme.id == 0}>
<DropdownToggle nav caret>
@ -67,7 +65,7 @@
{#each $themesStore["0"].exercices as exercice, index}
<DropdownItem href="{$themesStore["0"].urlid}/{exercice.urlid}" active={$current_theme && $current_theme.id == 0 && $current_exercice && $current_exercice.id == exercice.id}>
{exercice.title}
{#if $my && $my.id_team && exercice.solved}
{#if exercice.solved}
<Badge color="success" pill>
<Icon name="check" />
</Badge>

View file

@ -30,12 +30,6 @@
partJ = true;
}
function JchangeTeam() {
value = { };
partJ = false;
}
function submit(event) {
if (!partJ) {
JvalidateTeam();
@ -68,11 +62,7 @@
</option>
{/each}
</select>
{#if partJ}
<Button color="info" type="button" on:click={JchangeTeam}>Changer</Button>
{:else}
<Button color="info" type="button" on:click={JvalidateTeam} disabled={partJ}>Valider</Button>
{/if}
<Button color="info" type="button" on:click={JvalidateTeam} disabled={partJ}>Valider</Button>
<div class="invalid-feedback">
Veuillez indiquer une équipe valide.
</div>

View file

@ -10,7 +10,6 @@
import DateFormat from '$lib/components/DateFormat.svelte';
import { my } from '$lib/stores/my.js';
import { settings } from '$lib/stores/settings.js';
import { themes, exercices_idx } from '$lib/stores/themes.js';
let req = null;
@ -56,10 +55,6 @@
{:else if row.reason == "Display choices"}
<Badge color="secondary"><Icon name="info-square" /></Badge>
Échange champ de texte contre liste de choix
{:else if row.reason.startsWith("Response ")}
{@const fields = row.reason.split(" ")}
<Badge class="bg-success-subtle text-dark"><Icon name="clipboard2-check" /></Badge>
Validation {fields[1]} n<sup>o</sup>&nbsp;{fields[3]}
{:else}
<Badge color="primary"><Icon name="question" /></Badge>
{row.reason}
@ -71,16 +66,14 @@
{/if}
</Column>
<Column header="Détail">
<span title="Valeur initiale (cette valeur est fixe)">{Math.trunc(10*row.points)/10}</span> &times; {#if row.reason.startsWith("Response ")}<span title="Pourcentage des points accordé pour avoir répondu aux questions d'un défi, sans avoir validé entièrement le défi">{Math.trunc($settings.questionGainRatio * 1000)/10}&nbsp;&percnt;</span> &divide; <span title="Nombre de questions du défi">{$settings.questionGainRatio / row.coeff}</span>{:else if row.reason == "Validation" && $settings.questionGainRatio != 0}(<span title="Coefficient multiplicateur (il varie selon les événements en cours sur la plateforme)">{row.coeff + $settings.questionGainRatio}</span> &minus; <span title="Pourcentage des points déjà obtenu au travers des réponses aux questions">{Math.trunc($settings.questionGainRatio * 1000)/10}&nbsp;&percnt;</span>){:else}<span title="Coefficient multiplicateur (il varie selon les événements en cours sur la plateforme)">{row.coeff}</span>{/if}
<span title="Valeur initiale (cette valeur est fixe)">{Math.trunc(10*row.points)/10}</span> &times; <span title="Coefficient multiplicateur (il varie selon les événements en cours sur la plateforme)">{row.coeff}</span>
</Column>
<Column header="Points">
{Math.trunc(10*row.points * row.coeff)/10}
</Column>
</Table>
{:else}
<CardBody>
Vous n'avez fait aucune action vous faisant gagner ou perdre des points.
</CardBody>
Vous n'avez fait aucune action vous faisant gagner ou perdre des points.
{/if}
<button class="btn btn-primary" on:click={refresh_scores}>
<Icon name="arrow-clockwise" />

View file

@ -1,25 +0,0 @@
import { writable } from 'svelte/store';
function createDownloadedStore() {
let init = { };
try {
if (window.localStorage && window.localStorage.getItem("downloadedStore")) {
init = JSON.parse(window.localStorage.getItem("downloadedStore"));
}
} catch {
init = { };
}
const { subscribe, set, update } = writable(init);
return {
subscribe,
update: (u) => {
update(u);
if (window.localStorage) localStorage.setItem("downloadedStore", JSON.stringify(init));
},
}
}
export const hasDownloaded = createDownloadedStore();

View file

@ -37,9 +37,6 @@ function createMyStore() {
});
} else if (res_my.status === 404) {
update((m) => (null));
if (cb) {
cb(null);
}
}
}

View file

@ -59,7 +59,6 @@ export const tags = derived([my, themesStore], ([$my, $themesStore]) => {
for (const key in $themesStore) {
for (const exercice of $themesStore[key].exercices) {
exercice.tags.forEach((tag) => {
tag = tag.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
if (!tags[tag])
tags[tag] = {count: 1, solved: 0};
else

View file

@ -153,7 +153,6 @@ export const time = readable({}, function start(set) {
_settings = settings;
});
set(updateTime(_settings));
const interval = setInterval(() => {
set(updateTime(_settings));
}, 1000);

View file

@ -1,33 +0,0 @@
import { writable } from 'svelte/store';
function createSubmissionsStore() {
let init = {
flags: { },
mcqs: { },
justifications: { },
};
try {
if (window.localStorage && window.localStorage.getItem("submissionsStore")) {
init = JSON.parse(window.localStorage.getItem("submissionsStore"));
}
} catch {
init = {
flags: { },
mcqs: { },
justifications: { },
};
}
const { subscribe, set, update } = writable(init);
return {
subscribe,
update: (u) => {
update(u);
if (window.localStorage) localStorage.setItem("submissionsStore", JSON.stringify(init));
},
}
}
export const submissions = createSubmissionsStore();

View file

@ -3,9 +3,7 @@
import "bootstrap-icons/font/bootstrap-icons.css";
import { base } from '$app/paths';
import { page } from '$app/stores';
import {
Alert,
Container,
//Styles,
} from '@sveltestrap/sveltestrap';
@ -13,7 +11,6 @@
import Header from '$lib/components/Header.svelte';
import { challengeInfo } from '$lib/stores/challengeinfo';
import { my } from '$lib/stores/my.js';
import { settings } from '$lib/stores/settings';
</script>
@ -35,21 +32,6 @@
</div>
{/if}
<Header />
{#if !$my && $page.route.id != "/register"}
<Container class="mt-3 mb-3">
{#if $settings.allowRegistration}
<Alert color="warning" class="text-justify" fade={false}>
<strong>Votre équipe n'est pas encore enregistrée.</strong> Rendez-vous sur <a href="register">cette page</a> pour procéder à votre inscription.
</Alert>
{:else}
<Alert color="danger" class="text-justify" fade={false}>
<strong>Il semblerait qu'il y ait eu un problème lors de l'attribution de votre certificat.</strong> Veuillez vous signaler auprès de notre équipe afin de corriger ce problème.
</Alert>
{/if}
</Container>
{/if}
<slot></slot>
<style>

View file

@ -26,7 +26,7 @@
document.body.style.backgroundColor = "";
let items = [];
function refresh_items() {
$: {
const tmpitems = [];
for (const th of $themes) {
if (th.id == 0) continue;
@ -35,54 +35,54 @@
}
if ($themesStore["0"] && !$themesStore["0"].locked && $themesStore["0"].exercices) {
if (tmpitems.length) {
let nb_ex_max = tmpitems.length;
let i = 1;
let j = 0;
for (j = $themesStore["0"].exercices.length - 1; j >= 0 && i < tmpitems.length; j--) {
if ($my && $my.team_id && !$my.exercices[$themesStore["0"].exercices[j].id]) {
// Only apply after start
if (!($time.startIn && j < nb_ex_max && j < $settings.unlockedStandaloneExercices))
continue;
} else if ($my && !$my.team_id && j >= nb_ex_max) {
let nb_ex_max = tmpitems.length;
let i = 1;
let j = 0;
for (j = $themesStore["0"].exercices.length - 1; j >= 0 && i < tmpitems.length; j--) {
if ($my && $my.team_id && !$my.exercices[$themesStore["0"].exercices[j].id]) {
// Only apply after start
if (!($time.startIn && j < nb_ex_max && j < $settings.unlockedStandaloneExercices))
continue;
}
tmpitems.splice(i, 0, {id: tmpitems.length, theme: $themesStore["0"], exercice: $themesStore["0"].exercices[j]});
i += 2;
}
if (j >= 0 || i == 1) {
tmpitems.push({
id: tmpitems.length,
theme: {
...$themesStore["0"],
name: "Voir les autres défis",
headline: "Il y a " + ($themesStore["0"].exercices.length) + " défis à découvrir&nbsp;! Cliquez ici pour les afficher.",
locked: $themesStore["0"].locked || i == 1,
},
color: "light",
});
}
} else {
for (const j in $themesStore["0"].exercices) {
tmpitems.push({id: tmpitems.length, theme: $themesStore["0"], exercice: $themesStore["0"].exercices[j]});
}
tmpitems.splice(i, 0, {id: tmpitems.length, theme: $themesStore["0"], exercice: $themesStore["0"].exercices[j]});
i += 2;
}
if (j >= 0 || i == 1) {
tmpitems.push({
id: tmpitems.length,
theme: {
...$themesStore["0"],
name: "Voir les autres défis",
headline: "Il y a " + ($themesStore["0"].exercices.length) + " défis à découvrir&nbsp;! Cliquez ici pour les afficher.",
locked: $themesStore["0"].locked || i == 1,
},
color: "light",
});
}
}
items = tmpitems;
}
$: refresh_items($themes);
</script>
<Container class="mt-3 mb-5">
{#if $my}
{#if !($my.team_id)}
{#if !$my}
{#if $settings.allowRegistration}
<Alert color="warning" class="text-justify" fade={false}>
<strong>Votre équipe n'est pas encore enregistrée.</strong> Rendez-vous sur <a href="register">cette page</a> pour procéder à votre inscription.
</Alert>
{:else}
<Alert color="danger" class="text-justify" fade={false}>
<strong>Il semblerait qu'il y ait eu un problème lors de l'attribution de votre certificat.</strong> Veuillez vous signaler auprès de notre équipe afin de corriger ce problème.
</Alert>
{/if}
{:else if !($my.team_id)}
<Alert color="danger" fade={false}>
<strong>Attention&nbsp;:</strong> puisqu'il s'agit de captures effectuées dans le but de découvrir si des actes malveillants ont été commis sur différents systèmes d'information, les contenus qui sont téléchargeables <em>peuvent</em> contenir du contenu malveillant&nbsp;!
</Alert>
{:else if $teams[$my.team_id]}
{:else if $teams[$my.team_id]}
<Alert color="info" class="text-justify" fade={false}>
<strong>Félicitations {#if $my.members}{#each $my.members as member, index (member.id)}{#if member.id !== $my.members[0].id}{#if member.id === $my.members[$my.members.length - 1].id}&nbsp;et {:else}, {/if}{/if}{member.firstname} {member.lastname}{/each}&nbsp;{/if}!</strong> vous êtes maintenant connecté à l'espace de votre équipe <em>{$teams[$my.team_id].name}</em>.
{#if !$settings.denyNameChange}Vous pouvez changer ce nom dès maintenant en vous rendant sur la page de <a href="edit">votre équipe</a>.{/if}
@ -93,9 +93,9 @@
<strong>Les membres de votre équipe ne sont pas encore enregistrés.</strong> Passez voir l'équipe serveur pour corriger cela.
</Alert>
{/if}
{/if}
{/if}
<Masonry
{items}
let:item

View file

@ -84,27 +84,24 @@
</Col>
<Col lg={6} xl={5}>
{#if $current_theme.exercices && $current_theme.exercices.length}
<ul class="list-group h-100 d-flex flex-column">
<ul class="list-group">
{#each $current_theme.exercices as exercice, index}
<li
class="list-group-item flex-fill border-0 rounded-0"
class="list-group-item"
class:list-group-item-action={$my && $my.exercices[exercice.id]}
on:click={goto(`${$current_theme.urlid}/${exercice.urlid}`)}
on:keypress={goto(`${$current_theme.urlid}/${exercice.urlid}`)}
>
<div class="row h-100">
{#if index + 1 == $current_theme.exercices.length}
<div class="col-1"></div>
{:else}
<div class="col-1" style="margin-top: -0.5rem; margin-bottom: -0.5rem; text-align: right; border-right: 5px solid #{$my && $my.exercices[exercice.id] && $my.exercices[exercice.id].solved_rank ? '62c462' : 'bbb'}"></div>
{/if}
<div class="row">
<div class="col-1" style="margin-top: -0.5rem; margin-bottom: -0.5rem; text-align: right; border-right: 5px solid #{$my && $my.exercices[exercice.id] && $my.exercices[exercice.id].solved_rank ? '62c462' : 'bbb'}">
</div>
<div class="col-10">
<div style="position: absolute; margin-left: calc(var(--bs-gutter-x) * -.5 - 15px); margin-top: -0.5rem;">
<svg style="height: 50px; width: 23px;">
<rect
style="fill:#{$my && $my.exercices[exercice.id] && (index < 1 || ($my.exercices[$current_theme.exercices[index-1].id] && $my.exercices[$current_theme.exercices[index-1].id].solved_rank)) ? '62c462' : 'bbb'}"
width="5"
height="27"
height="30"
x="10"
y="0" />
<path
@ -114,17 +111,24 @@
</div>
<div class="d-flex justify-content-between flex-wrap">
<h5 class="fw-bold text-truncate">
<span style="white-space: nowrap">
{#if $my && $my.exercices[exercice.id] && $my.exercices[exercice.id].wip}
<Icon name="cone-striped" aria-hidden="true" title="Cette étape est encore en construction." />
{/if}
{exercice.title}
</span>
{#if $my && $my.exercices[exercice.id]}
<span style="white-space: nowrap">
{#if $my.exercices[exercice.id].wip}
<Icon name="cone-striped" aria-hidden="true" title="Cette étape est encore en construction." />
{/if}
{exercice.title}
</span>
{:else}
<span style="white-space: nowrap">
<Icon name="lock-fill" aria-hidden="true" title="Vous n'avez pas encore accès à ce défi" />
{exercice.title}
</span>
{/if}
{#if exercice.curcoeff > 1.0}
<Icon name="gift" aria-hidden="true" title="Un bonus est actuellement appliqué lors de la résolution de ce défi" />
{/if}
</h5>
<div class="mb-2">
<div>
{#each exercice.tags as tag, idx}
<Badge href="tags/{tag}" pill color="secondary" class="mx-1 float-end">#{tag}</Badge>
{/each}
@ -132,14 +136,14 @@
</div>
<p>{@html exercice.headline}</p>
</div>
<div class="d-none d-md-flex flex-column h-100 justify-content-center align-items-end col-1 pe-0">
<div class="d-none d-md-block col-1 pe-0">
{#if $my && $my.exercices[exercice.id]}
<a href="{$current_theme.urlid}/{exercice.urlid}" style="font-size: 3rem">
<a class="float-end" href="{$current_theme.urlid}/{exercice.urlid}" style="font-size: 3rem">
<Icon name="chevron-right" aria-hidden="true" />
</a>
{:else}
<span style="font-size: 3rem">
<Icon name="lock-fill" aria-hidden="true" title="Vous n'avez pas encore accès à ce défi" />
<span class="float-end" style="font-size: 3rem">
<Icon name="chevron-right" aria-hidden="true" />
</span>
{/if}
</div>

View file

@ -7,7 +7,6 @@
Container,
Icon,
Row,
Spinner,
} from '@sveltestrap/sveltestrap';
import { goto } from '$app/navigation';
@ -24,10 +23,8 @@
let partJ = false;
let messageClass;
let message;
let registrationInProgress = false;
function gotoHomeOnDiff(i) {
registrationInProgress = true;
my.refresh((my) => {
if (my && my.team_id) {
themesStore.refresh(() => {
@ -35,10 +32,6 @@
});
} else if (i > 0) {
setTimeout(gotoHomeOnDiff, 650, i-1);
} else {
registrationInProgress = false;
messageClass = 'danger';
message = "Temps d'attente dépassé.";
}
})
}
@ -113,13 +106,9 @@
<strong>Oups, il semblerait qu'il y ait eu un problème lors de l'attribution de votre certificat.</strong>
Veuillez vous signaler auprès de notre équipe afin de corriger ce problème.
</Alert>
{:else if registrationInProgress}
<div class="d-flex justify-content-center align-items-center gap-4 mt-5 fw-bold text-primary">
<Spinner size="lg" color="primary" /> Inscription en cours&hellip;
</div>
{:else}
{#if !$settings.denyTeamCreation && !partJ}
<Card body class="niceborder my-3 text-white">
<Card body class="niceborder my-3">
<p>
Votre équipe n'est pas encore enregistrée sur notre serveur. Afin de
pouvoir participer au challenge, nous vous remercions de bien vouloir
@ -129,7 +118,7 @@
</Card>
{/if}
{#if $settings.canJoinTeam && !partR}
<Card body class="niceborder my-3 text-white">
<Card body class="niceborder my-3">
<p>
{#if !$settings.denyTeamCreation}
Si votre équipe est déjà créée, rejoignez-là&nbsp;!

View file

@ -21,7 +21,7 @@
<div class="card-group text-justify mb-5">
<div class="card niceborder">
<div class="card-body text-indent text-white">
<div class="card-body text-indent">
<h2>Débloquage des challenges</h2>
<p>
Au début, seul le premier défi de chaque scénario est
@ -31,7 +31,7 @@
{#if $settings.unlockedStandaloneExercicesByThemeStepValidation > 0 || $settings.unlockedStandaloneExercicesByStandaloneExerciceValidation > 0}
<p>
Vous avez également accès à {$settings.unlockedStandaloneExercices} défis indépendants.
D'autres défis sont débloqués
Ces défis sont débloqués
{#if $settings.unlockedStandaloneExercicesByThemeStepValidation > 0}{#if $settings.unlockedStandaloneExercicesByThemeStepValidation < 1} toutes les {1/$settings.unlockedStandaloneExercicesByThemeStepValidation} étape{#if 1/$settings.unlockedStandaloneExercicesByThemeStepValidation > 1}s{/if} de scénario que vous validez{:else}par {$settings.unlockedStandaloneExercicesByThemeStepValidation} défis pour chaque étape de scénario validée{/if}{/if}
{#if $settings.unlockedStandaloneExercicesByStandaloneExerciceValidation > 0}{#if $settings.unlockedStandaloneExercicesByStandaloneExerciceValidation < 1} tous les {1/$settings.unlockedStandaloneExercicesByStandaloneExerciceValidation} défi{#if 1/$settings.unlockedStandaloneExercicesByStandaloneExerciceValidation > 1}s{/if} indépendant que vous validez{:else}par {$settings.unlockedStandaloneExercicesByStandaloneExerciceValidation} exercice indépendant validé{/if}{/if}
</p>
@ -55,21 +55,6 @@
proposés. Plus le challenge est compliqué, plus il rapporte de points.
</p>
{#if $settings.questionGainRatio != 0}
<p>
Même si vous n'arrivez pas à valider un défi, toutes les questions validées augmentent votre score.
{Math.trunc($settings.questionGainRatio * 1000)/10}&nbsp;&percnt; des points du défi sont répartis à parts égales entre toutes les questions.
</p>
<p>
Par exemple, pour un défi de 5 questions valant 20 points, en ayant répondu à 3 questions sur les 5, votre score sera augmenté de&nbsp;:<br>
20 &times; {Math.trunc($settings.questionGainRatio * 1000)/10}&nbsp;&percnt; &divide; 5 &times; 3 &equals; {Math.trunc(20 * $settings.questionGainRatio / 5 * 3 * 100)/100}&nbsp;points.
</p>
<p>
Les {Math.trunc(1000 - $settings.questionGainRatio * 1000)/10}&nbsp;&percnt; restants sont obtenus à la validation complète du défi.
</p>
{/if}
{#if $settings.submissionCostBase != 0}
<h3>Coût des tentatives</h3>
<p>
Vous disposez de 10&nbsp;tentatives pour trouver la/les solutions d'un
@ -129,21 +114,20 @@
parmi ce nombre de tentatives.
</p>
{/if}
{/if}
</div>
</div>
<div class="card niceborder">
<div class="card-body text-indent text-white">
<div class="card-body text-indent">
{#if $settings.discountedFactor > 0}
<h3>Décote des gains</h3>
<p>
Une validation d'étape ne vous garantit pas un solde de points fixe.
Une validation d'étape ne vous garanti pas un solde de points fixe.
</p>
<p>
Selon le nombre d'équipes qui valident un challenge donné, sa cote diminue et vous rapporte alors moins de points. Le gain final est donc indépendant du fait que vous ayez validé l'étape avant une autre équipe&nbsp;: le gain affiché est un gain maximum que vous obtiendriez si aucune autre équipe ne valide cette étape.
Selon le nombre d'équipe qui valident un challenge donné, sa cote diminue et vous rapporte alors moins de points. Le gain est donc indépendemment du fait que vous ayez validé l'étape avant une autre équipe : le gain affiché est un gain maximum, entendu si aucune autre équipe ne le valide.
</p>
<p>
Chaque validation réduit de {$settings.discountedFactor*100}&nbsp;&percnt; la cote de l'exercice.
Chaque validation réduit de {$settings.discountedFactor*100}&nbsp;% la cote de l'exercice.
</p>
<p>
Ainsi, pour un exercice d'une valeur initiale de {10*$settings.globalScoreCoefficient}&nbsp;points&nbsp;:
@ -162,19 +146,19 @@
</tr>
<tr>
<td>2</td>
<td>{Math.round(100*$settings.globalScoreCoefficient*(1-$settings.discountedFactor))/10}&nbsp;points</td>
<td>{10*$settings.globalScoreCoefficient*(1-$settings.discountedFactor)}&nbsp;points</td>
</tr>
<tr>
<td>5</td>
<td>{Math.round(100*$settings.globalScoreCoefficient*(1-$settings.discountedFactor*5))/10}&nbsp;points</td>
<td>{10*$settings.globalScoreCoefficient*(1-$settings.discountedFactor*5)}&nbsp;points</td>
</tr>
<tr>
<td>10</td>
<td>{Math.round(100*$settings.globalScoreCoefficient*(1-$settings.discountedFactor*10))/10}&nbsp;points</td>
<td>{10*$settings.globalScoreCoefficient*(1-$settings.discountedFactor*10)}&nbsp;points</td>
</tr>
<tr>
<td>20</td>
<td>{Math.round(100*$settings.globalScoreCoefficient*(1-$settings.discountedFactor*20))/10}&nbsp;points</td>
<td>{10*$settings.globalScoreCoefficient*(1-$settings.discountedFactor*20)}&nbsp;points</td>
</tr>
<tr>
<td>...</td>
@ -208,12 +192,10 @@
défi.
</p>
{#if $settings.firstBlood}
<h4>Prem's</h4>
<p>
Un bonus de +{$settings.firstBlood * 100}&nbsp;&percnt; est attribué à la première équipe qui résout un défi.
</p>
{/if}
<h4>Prem's</h4>
<p>
Un bonus de +{$settings.firstBlood * 100}&nbsp;% est attribué à la première équipe qui résout un défi.
</p>
<h4>Bonus temporaires <small><Icon name="gift" aria-hidden="true" title="Des
bonus existent pour au moins un challenge de ce thème" /></small></h4>

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