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 5574 additions and 9204 deletions

View file

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

View file

@ -62,7 +62,7 @@ dependency_scanning:
get-deps: get-deps:
stage: deps stage: deps
image: golang:1-alpine image: golang:alpine3.18
before_script: before_script:
- export GOPATH="$CI_PROJECT_DIR/.go" - export GOPATH="$CI_PROJECT_DIR/.go"
- mkdir -p .go - mkdir -p .go
@ -75,7 +75,7 @@ vet:
needs: ["build-qa-ui"] needs: ["build-qa-ui"]
dependencies: dependencies:
- build-qa-ui - build-qa-ui
image: golang:1-alpine image: golang:alpine3.18
before_script: before_script:
- export GOPATH="$CI_PROJECT_DIR/.go" - export GOPATH="$CI_PROJECT_DIR/.go"
- mkdir -p .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 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 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 \ RUN apk add --no-cache \
ca-certificates \ 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 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 go build -v -buildvcs=false -o checker/checker ./checker
FROM alpine:3.21 FROM alpine:3.19
WORKDIR /srv 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 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 go build -v -buildvcs=false -o dashboard/dashboard ./dashboard
FROM alpine:3.21 FROM alpine:3.19
EXPOSE 8082 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 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/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 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 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/ 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 67/udp
EXPOSE 69/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 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 go build -v -buildvcs=false -o evdist/evdist ./evdist
FROM alpine:3.21 FROM alpine:3.19
WORKDIR /srv WORKDIR /srv

View file

@ -1,4 +1,4 @@
FROM node:23-alpine AS nodebuild FROM node:21-alpine as nodebuild
WORKDIR /ui 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 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 go build -v -buildvcs=false -o generator/generator ./generator
FROM alpine:3.21 FROM alpine:3.19
WORKDIR /srv 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 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 go build -v -o get-remote-files ./admin/get-remote-files
FROM alpine:3.21 FROM alpine:3.19
RUN apk add --no-cache \ RUN apk add --no-cache \
ca-certificates ca-certificates

View file

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

View file

@ -1,4 +1,4 @@
FROM node:23-alpine AS nodebuild FROM node:21-alpine as nodebuild
WORKDIR /ui WORKDIR /ui
@ -8,7 +8,7 @@ RUN npm install --network-timeout=100000 && \
npm run build npm run build
FROM golang:1-alpine AS gobuild FROM golang:1-alpine as gobuild
RUN apk add --no-cache git 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 go build -v -buildvcs=false -o qa/qa ./qa
FROM alpine:3.21 FROM alpine:3.19
EXPOSE 8083 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 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 go build -v -buildvcs=false -o ./receiver/receiver ./receiver
FROM alpine:3.21 FROM alpine:3.19
EXPOSE 8080 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 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 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 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 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 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 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 RUN apk add --no-cache git
@ -23,7 +23,7 @@ RUN go get -d -v ./repochecker && \
ENV GRAMMALECTE_VERSION 2.1.1 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 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 authentication, lots of state of the art cryptographic methods and aims to be
deployed in a DMZ network architecture. 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 This is a [monorepo](https://danluu.com/monorepo/), containing several
micro-services : micro-services :

View file

@ -1,11 +1,9 @@
package api package api
import ( import (
"bytes"
"fmt" "fmt"
"log" "log"
"net/http" "net/http"
"path"
"reflect" "reflect"
"strconv" "strconv"
"strings" "strings"
@ -20,7 +18,6 @@ import (
func declareGlobalExercicesRoutes(router *gin.RouterGroup) { func declareGlobalExercicesRoutes(router *gin.RouterGroup) {
router.GET("/resolutions.json", exportResolutionMovies) router.GET("/resolutions.json", exportResolutionMovies)
router.GET("/exercices_stats.json", getExercicesStats) router.GET("/exercices_stats.json", getExercicesStats)
router.GET("/exercices_forge_bindings.json", getExercicesForgeLinks)
router.GET("/tags", listTags) router.GET("/tags", listTags)
} }
@ -35,18 +32,9 @@ func declareExercicesRoutes(router *gin.RouterGroup) {
apiExercicesRoutes.PATCH("", partUpdateExercice) apiExercicesRoutes.PATCH("", partUpdateExercice)
apiExercicesRoutes.DELETE("", deleteExercice) apiExercicesRoutes.DELETE("", deleteExercice)
apiExercicesRoutes.POST("/diff-sync", APIDiffExerciceWithRemote)
apiExercicesRoutes.GET("/history.json", getExerciceHistory)
apiExercicesRoutes.GET("/stats.json", getExerciceStats) apiExercicesRoutes.GET("/stats.json", getExerciceStats)
apiExercicesRoutes.GET("/tries", listTries) apiExercicesRoutes.GET("/history.json", getExerciceHistory)
apiTriesRoutes := apiExercicesRoutes.Group("/tries/:trid")
apiTriesRoutes.Use(ExerciceTryHandler)
apiTriesRoutes.GET("", getExerciceTry)
apiTriesRoutes.DELETE("", deleteExerciceTry)
apiHistoryRoutes := apiExercicesRoutes.Group("/history.json") apiHistoryRoutes := apiExercicesRoutes.Group("/history.json")
apiHistoryRoutes.Use(AssigneeCookieHandler) apiHistoryRoutes.Use(AssigneeCookieHandler)
@ -74,8 +62,6 @@ func declareExercicesRoutes(router *gin.RouterGroup) {
apiFlagsRoutes.POST("/try", tryExerciceFlag) apiFlagsRoutes.POST("/try", tryExerciceFlag)
apiFlagsRoutes.DELETE("/", deleteExerciceFlag) apiFlagsRoutes.DELETE("/", deleteExerciceFlag)
apiFlagsRoutes.GET("/dependancies", showExerciceFlagDeps) apiFlagsRoutes.GET("/dependancies", showExerciceFlagDeps)
apiFlagsRoutes.GET("/statistics", showExerciceFlagStats)
apiFlagsRoutes.DELETE("/tries", deleteExerciceFlagTries)
apiFlagsRoutes.GET("/choices/", listFlagChoices) apiFlagsRoutes.GET("/choices/", listFlagChoices)
apiFlagsChoicesRoutes := apiExercicesRoutes.Group("/choices/:cid") apiFlagsChoicesRoutes := apiExercicesRoutes.Group("/choices/:cid")
apiFlagsChoicesRoutes.Use(FlagChoiceHandler) apiFlagsChoicesRoutes.Use(FlagChoiceHandler)
@ -91,8 +77,6 @@ func declareExercicesRoutes(router *gin.RouterGroup) {
apiQuizRoutes.PUT("", updateExerciceQuiz) apiQuizRoutes.PUT("", updateExerciceQuiz)
apiQuizRoutes.DELETE("", deleteExerciceQuiz) apiQuizRoutes.DELETE("", deleteExerciceQuiz)
apiQuizRoutes.GET("/dependancies", showExerciceQuizDeps) apiQuizRoutes.GET("/dependancies", showExerciceQuizDeps)
apiQuizRoutes.GET("/statistics", showExerciceQuizStats)
apiQuizRoutes.DELETE("/tries", deleteExerciceQuizTries)
apiExercicesRoutes.GET("/tags", listExerciceTags) apiExercicesRoutes.GET("/tags", listExerciceTags)
apiExercicesRoutes.POST("/tags", addExerciceTag) apiExercicesRoutes.POST("/tags", addExerciceTag)
@ -103,8 +87,8 @@ func declareExercicesRoutes(router *gin.RouterGroup) {
// Remote // Remote
router.GET("/remote/themes/:thid/exercices/:exid", sync.ApiGetRemoteExercice) 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/hints", sync.ApiGetRemoteExerciceHints)
router.GET("/remote/themes/:thid/exercices/:exid/flags", sync.ApiGetRemoteExerciceFlags)
} }
type Exercice struct { type Exercice struct {
@ -142,7 +126,7 @@ func ExerciceHandler(c *gin.Context) {
c.Set("theme", theme) c.Set("theme", theme)
} else { } 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) 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) { func AssigneeCookieHandler(c *gin.Context) {
myassignee, err := c.Cookie("myassignee") myassignee, err := c.Cookie("myassignee")
if err != nil { if err != nil {
@ -933,60 +852,6 @@ func showExerciceFlagDeps(c *gin.Context) {
c.JSON(http.StatusOK, deps) 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) { func tryExerciceFlag(c *gin.Context) {
flag := c.MustGet("flag-key").(*fic.FlagKey) flag := c.MustGet("flag-key").(*fic.FlagKey)
@ -1030,23 +895,6 @@ func updateExerciceFlag(c *gin.Context) {
flag.Help = uk.Help flag.Help = uk.Help
flag.IgnoreCase = uk.IgnoreCase flag.IgnoreCase = uk.IgnoreCase
flag.Multiline = uk.Multiline 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 { if len(uk.Flag) > 0 {
var err error var err error
flag.Checksum, err = flag.ComputeChecksum([]byte(uk.Flag)) flag.Checksum, err = flag.ComputeChecksum([]byte(uk.Flag))
@ -1058,6 +906,14 @@ func updateExerciceFlag(c *gin.Context) {
} else { } else {
flag.Checksum = uk.Value 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 { if _, err := flag.Update(); err != nil {
log.Println("Unable to updateExerciceFlag:", err.Error()) log.Println("Unable to updateExerciceFlag:", err.Error())
@ -1166,60 +1022,6 @@ func showExerciceQuizDeps(c *gin.Context) {
c.JSON(http.StatusOK, deps) 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) { func updateExerciceQuiz(c *gin.Context) {
quiz := c.MustGet("flag-quiz").(*fic.MCQ) quiz := c.MustGet("flag-quiz").(*fic.MCQ)
@ -1352,398 +1154,3 @@ func updateExerciceTags(c *gin.Context) {
exercice.WipeTags() exercice.WipeTags()
addExerciceTag(c) 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 ( import (
"archive/zip" "archive/zip"
"encoding/json" "encoding/json"
"io"
"log"
"net/http" "net/http"
"path" "path"
"srs.epita.fr/fic-server/admin/sync"
"srs.epita.fr/fic-server/libfic" "srs.epita.fr/fic-server/libfic"
"srs.epita.fr/fic-server/settings" "srs.epita.fr/fic-server/settings"
@ -34,9 +31,6 @@ func declareExportRoutes(router *gin.RouterGroup) {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return return
} }
s.End = nil
s.NextChangeTime = nil
s.DelegatedQA = []string{}
teams, err := fic.ExportTeams(false) teams, err := fic.ExportTeams(false)
if err != nil { if err != nil {
@ -62,41 +56,6 @@ func declareExportRoutes(router *gin.RouterGroup) {
json.NewEncoder(f).Encode(challengeinfo) 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 // my.json
f, err = w.Create("my.json") f, err = w.Create("my.json")
if err == nil { if err == nil {

View file

@ -5,7 +5,6 @@ import (
"fmt" "fmt"
"log" "log"
"net/http" "net/http"
"os"
"path/filepath" "path/filepath"
"strconv" "strconv"
@ -144,27 +143,12 @@ func listFiles(c *gin.Context) {
} }
func clearFiles(c *gin.Context) { func clearFiles(c *gin.Context) {
err := os.RemoveAll(fic.FilesDir) _, err := fic.ClearFiles()
if err != nil { if err != nil {
log.Println("Unable to remove files:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return 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) c.JSON(http.StatusOK, true)
} }

View file

@ -8,6 +8,7 @@ import (
"net/http" "net/http"
"os" "os"
"path" "path"
"strings"
"text/template" "text/template"
"unicode" "unicode"
@ -33,13 +34,9 @@ func declarePasswordRoutes(router *gin.RouterGroup) {
c.JSON(http.StatusOK, gin.H{"password": passwd}) 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) { router.GET("/dex.yaml", func(c *gin.Context) {
cfg, err := genDexConfig() _, staticpassword := c.Request.URL.Query()["staticpassword"]
cfg, err := genDexConfig(staticpassword)
if err != nil { if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return return
@ -48,7 +45,8 @@ func declarePasswordRoutes(router *gin.RouterGroup) {
c.String(http.StatusOK, string(cfg)) c.String(http.StatusOK, string(cfg))
}) })
router.POST("/dex.yaml", func(c *gin.Context) { 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()}) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return return
} else if err := ioutil.WriteFile(path.Join(pki.PKIDir, "shared", "dex-config.yaml"), []byte(dexcfg), 0644); err != nil { } 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 file: /var/dex/dex.db
web: web:
http: 0.0.0.0:5556 http: 0.0.0.0:5556
{{ if .GRPC }}
grpc:
addr: 127.0.0.1:5557
{{ end }}
frontend: frontend:
issuer: {{ .Name }} issuer: Challenge forensic
logoURL: {{ .LogoPath }} logoURL: {{ .LogoPath }}
dir: /srv/dex/web/ dir: /srv/dex/web/
oauth2: oauth2:
@ -214,16 +216,23 @@ type dexConfig struct {
Clients []dexConfigClient Clients []dexConfigClient
Teams []*fic.Team Teams []*fic.Team
LogoPath string LogoPath string
GRPC bool
} }
func genDexConfig() ([]byte, error) { func genDexConfig(withTeams bool) ([]byte, error) {
if OidcSecret == "" { if OidcSecret == "" {
return nil, fmt.Errorf("Unable to generate dex configuration: OIDC Secret not defined. Please define FICOIDC_SECRET in your environment.") return nil, fmt.Errorf("Unable to generate dex configuration: OIDC Secret not defined. Please define FICOIDC_SECRET in your environment.")
} }
teams, err := fic.GetTeams() var teams []*fic.Team
if err != nil { var err error
return nil, err
// 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("") b := bytes.NewBufferString("")
@ -241,7 +250,7 @@ func genDexConfig() ([]byte, error) {
logoPath := "" logoPath := ""
if len(challengeInfo.MainLogo) > 0 { 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) dexTmpl, err := template.New("dexcfg").Parse(dexcfgtpl)
@ -262,12 +271,20 @@ func genDexConfig() ([]byte, error) {
}, },
Teams: teams, Teams: teams,
LogoPath: logoPath, LogoPath: logoPath,
GRPC: !withTeams,
}) })
if err != nil { if err != nil {
return nil, fmt.Errorf("An error occurs during template execution: %w", err) return nil, fmt.Errorf("An error occurs during template execution: %w", err)
} }
// Also generate team associations // Also generate team associations
if !withTeams {
teams, err = fic.GetTeams()
if err != nil {
return nil, err
}
}
for _, team := range teams { for _, team := range teams {
if _, err := os.Stat(path.Join(TeamsDir, fmt.Sprintf("team%02d", team.Id))); err == nil { 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 { 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) { 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 { if teams, err := fic.GetTeams(); err != nil {
return nil, err return nil, err
} else { } else {
@ -299,7 +311,6 @@ func genDexPasswordTpl() ([]byte, error) {
return nil, fmt.Errorf("Cannot create template: %w", err) return nil, fmt.Errorf("Cannot create template: %w", err)
} else if err = dexTmpl.Execute(b, dexConfig{ } else if err = dexTmpl.Execute(b, dexConfig{
Teams: teams, Teams: teams,
Name: challengeInfo.Title,
}); err != nil { }); err != nil {
return nil, fmt.Errorf("An error occurs during template execution: %w", err) return nil, fmt.Errorf("An error occurs during template execution: %w", err)
} else { } else {

View file

@ -41,27 +41,5 @@ func declareRepositoriesRoutes(router *gin.RouterGroup) {
} }
c.JSON(http.StatusOK, mod) 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" "net/http"
"os" "os"
"path" "path"
"reflect"
"strconv" "strconv"
"time" "time"
@ -25,6 +26,7 @@ func declareSettingsRoutes(router *gin.RouterGroup) {
router.GET("/challenge.json", getChallengeInfo) router.GET("/challenge.json", getChallengeInfo)
router.PUT("/challenge.json", saveChallengeInfo) router.PUT("/challenge.json", saveChallengeInfo)
router.GET("/settings-ro.json", getROSettings)
router.GET("/settings.json", getSettings) router.GET("/settings.json", getSettings)
router.PUT("/settings.json", saveSettings) router.PUT("/settings.json", saveSettings)
router.DELETE("/settings.json", func(c *gin.Context) { 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) { func GetChallengeInfo() (*settings.ChallengeInfo, error) {
var challengeinfo string var challengeinfo string
var err error var err error
@ -310,7 +330,6 @@ func ApplySettings(config *settings.Settings) {
fic.SubmissionCostBase = config.SubmissionCostBase fic.SubmissionCostBase = config.SubmissionCostBase
fic.SubmissionUniqueness = config.SubmissionUniqueness fic.SubmissionUniqueness = config.SubmissionUniqueness
fic.CountOnlyNotGoodTries = config.CountOnlyNotGoodTries fic.CountOnlyNotGoodTries = config.CountOnlyNotGoodTries
fic.QuestionGainRatio = config.QuestionGainRatio
if config.DiscountedFactor != fic.DiscountedFactor { if config.DiscountedFactor != fic.DiscountedFactor {
fic.DiscountedFactor = config.DiscountedFactor fic.DiscountedFactor = config.DiscountedFactor
@ -330,7 +349,6 @@ func ResetSettings() error {
WChoiceCurCoefficient: 1, WChoiceCurCoefficient: 1,
GlobalScoreCoefficient: 1, GlobalScoreCoefficient: 1,
DiscountedFactor: 0, DiscountedFactor: 0,
QuestionGainRatio: 0,
UnlockedStandaloneExercices: 10, UnlockedStandaloneExercices: 10,
UnlockedStandaloneExercicesByThemeStepValidation: 1, UnlockedStandaloneExercicesByThemeStepValidation: 1,
UnlockedStandaloneExercicesByStandaloneExerciceValidation: 0, UnlockedStandaloneExercicesByStandaloneExerciceValidation: 0,

View file

@ -7,7 +7,6 @@ import (
"net/url" "net/url"
"os" "os"
"path" "path"
"reflect"
"strings" "strings"
"srs.epita.fr/fic-server/admin/generation" "srs.epita.fr/fic-server/admin/generation"
@ -18,8 +17,6 @@ import (
"go.uber.org/multierr" "go.uber.org/multierr"
) )
var lastSyncError = ""
func flatifySyncErrors(errs error) (ret []string) { func flatifySyncErrors(errs error) (ret []string) {
for _, err := range multierr.Errors(errs) { for _, err := range multierr.Errors(errs) {
ret = append(ret, err.Error()) ret = append(ret, err.Error())
@ -30,37 +27,12 @@ func flatifySyncErrors(errs error) (ret []string) {
func declareSyncRoutes(router *gin.RouterGroup) { func declareSyncRoutes(router *gin.RouterGroup) {
apiSyncRoutes := router.Group("/sync") 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. // Base sync checks if the local directory is in sync with remote one.
apiSyncRoutes.POST("/base", func(c *gin.Context) { apiSyncRoutes.POST("/base", func(c *gin.Context) {
err := sync.GlobalImporter.Sync() err := sync.GlobalImporter.Sync()
if err != nil { if err != nil {
lastSyncError = err.Error()
c.JSON(http.StatusExpectationFailed, gin.H{"errmsg": err.Error()}) c.JSON(http.StatusExpectationFailed, gin.H{"errmsg": err.Error()})
} else { } else {
lastSyncError = ""
c.JSON(http.StatusOK, true) 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). // Deep sync: a fully recursive synchronization (can be limited by theme).
apiSyncRoutes.POST("/deep", func(c *gin.Context) { apiSyncRoutes.GET("/deep", func(c *gin.Context) {
r := sync.SyncDeep(sync.GlobalImporter) if sync.DeepSyncProgress == 0 {
lastSyncError = "" c.AbortWithStatusJSON(http.StatusTooEarly, gin.H{"errmsg": "Pas de synchronisation en cours"})
c.JSON(http.StatusOK, r) 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 := apiSyncRoutes.Group("/deep/:thid")
apiSyncDeepRoutes.Use(ThemeHandler) apiSyncDeepRoutes.Use(ThemeHandler)
// Special route to handle standalone exercices // Special route to handle standalone exercices
apiSyncRoutes.POST("/deep/0", func(c *gin.Context) { apiSyncRoutes.POST("/deep/0", func(c *gin.Context) {
var st []string 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()) st = append(st, se.Error())
} }
sync.EditDeepReport(&sync.SyncReport{Exercices: st}, false) sync.EditDeepReport(&sync.SyncReport{Exercices: st}, false)
sync.DeepSyncProgress = 255 sync.DeepSyncProgress = 255
lastSyncError = ""
c.JSON(http.StatusOK, st) c.JSON(http.StatusOK, st)
}) })
apiSyncDeepRoutes.POST("", func(c *gin.Context) { 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.EditDeepReport(&sync.SyncReport{Themes: map[string][]string{theme.Name: st}}, false)
sync.DeepSyncProgress = 255 sync.DeepSyncProgress = 255
lastSyncError = ""
c.JSON(http.StatusOK, st) c.JSON(http.StatusOK, st)
}) })
@ -117,7 +90,6 @@ func declareSyncRoutes(router *gin.RouterGroup) {
apiSyncRoutes.POST("/themes", func(c *gin.Context) { apiSyncRoutes.POST("/themes", func(c *gin.Context) {
_, errs := sync.SyncThemes(sync.GlobalImporter) _, errs := sync.SyncThemes(sync.GlobalImporter)
lastSyncError = ""
c.JSON(http.StatusOK, flatifySyncErrors(errs)) c.JSON(http.StatusOK, flatifySyncErrors(errs))
}) })
@ -241,7 +213,7 @@ func declareSyncExercicesRoutes(router *gin.RouterGroup) {
exceptions := sync.LoadExerciceException(sync.GlobalImporter, theme, exercice, nil) 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) { apiSyncExercicesRoutes.POST("/fixurlid", func(c *gin.Context) {
exercice := c.MustGet("exercice").(*fic.Exercice) exercice := c.MustGet("exercice").(*fic.Exercice)
@ -286,12 +258,10 @@ func autoSync(c *gin.Context) {
if !IsProductionEnv { if !IsProductionEnv {
if err := sync.GlobalImporter.Sync(); err != nil { if err := sync.GlobalImporter.Sync(); err != nil {
lastSyncError = err.Error()
log.Println("Unable to sync.GI.Sync:", err.Error()) log.Println("Unable to sync.GI.Sync:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to perform the pull."}) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to perform the pull."})
return return
} }
lastSyncError = ""
} }
themes, err := fic.GetThemes() themes, err := fic.GetThemes()
@ -380,32 +350,3 @@ func autoSync(c *gin.Context) {
c.JSON(http.StatusOK, st) 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 package api
import ( import (
"encoding/json"
"fmt" "fmt"
"log" "log"
"math/rand"
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
@ -186,9 +186,6 @@ func declareTeamsRoutes(router *gin.RouterGroup) {
declareTeamsPasswordRoutes(apiTeamsRoutes) declareTeamsPasswordRoutes(apiTeamsRoutes)
declareTeamClaimsRoutes(apiTeamsRoutes) declareTeamClaimsRoutes(apiTeamsRoutes)
declareTeamCertificateRoutes(apiTeamsRoutes) declareTeamCertificateRoutes(apiTeamsRoutes)
// Import teams from cyberrange
router.POST("/cyberrange-teams.json", importTeamsFromCyberrange)
} }
func TeamHandler(c *gin.Context) { func TeamHandler(c *gin.Context) {
@ -295,11 +292,6 @@ func bindingTeams(c *gin.Context) {
c.String(http.StatusOK, ret) c.String(http.StatusOK, ret)
} }
type teamAssociation struct {
Association string `json:"association"`
TeamId int64 `json:"team_id"`
}
func allAssociations(c *gin.Context) { func allAssociations(c *gin.Context) {
teams, err := fic.GetTeams() teams, err := fic.GetTeams()
if err != nil { if err != nil {
@ -308,7 +300,7 @@ func allAssociations(c *gin.Context) {
return return
} }
var ret []teamAssociation var ret []string
for _, team := range teams { for _, team := range teams {
assocs, err := pki.GetTeamAssociations(TeamsDir, team.Id) assocs, err := pki.GetTeamAssociations(TeamsDir, team.Id)
@ -318,84 +310,13 @@ func allAssociations(c *gin.Context) {
} }
for _, a := range assocs { for _, a := range assocs {
ret = append(ret, teamAssociation{a, team.Id}) ret = append(ret, a)
} }
} }
c.JSON(http.StatusOK, ret) 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) { func createTeam(c *gin.Context) {
var ut fic.Team var ut fic.Team
err := c.ShouldBindJSON(&ut) err := c.ShouldBindJSON(&ut)
@ -405,7 +326,11 @@ func createTeam(c *gin.Context) {
} }
if ut.Color == 0 { 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) team, err := fic.CreateTeam(strings.TrimSpace(ut.Name), ut.Color, ut.ExternalId)

View file

@ -5,9 +5,7 @@ import (
"log" "log"
"net/http" "net/http"
"path" "path"
"reflect"
"strconv" "strconv"
"strings"
"srs.epita.fr/fic-server/admin/sync" "srs.epita.fr/fic-server/admin/sync"
"srs.epita.fr/fic-server/libfic" "srs.epita.fr/fic-server/libfic"
@ -50,8 +48,6 @@ func declareThemesRoutes(router *gin.RouterGroup) {
apiThemesRoutes.PUT("", updateTheme) apiThemesRoutes.PUT("", updateTheme)
apiThemesRoutes.DELETE("", deleteTheme) apiThemesRoutes.DELETE("", deleteTheme)
apiThemesRoutes.POST("/diff-sync", APIDiffThemeWithRemote)
apiThemesRoutes.GET("/exercices_stats.json", getThemedExercicesStats) apiThemesRoutes.GET("/exercices_stats.json", getThemedExercicesStats)
declareExercicesRoutes(apiThemesRoutes) declareExercicesRoutes(apiThemesRoutes)
@ -74,18 +70,14 @@ func ThemeHandler(c *gin.Context) {
return return
} }
if thid == 0 { theme, err := fic.GetTheme(thid)
c.Set("theme", &fic.StandaloneExercicesTheme) if err != nil {
} else { c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Theme not found"})
theme, err := fic.GetTheme(thid) return
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Theme not found"})
return
}
c.Set("theme", theme)
} }
c.Set("theme", theme)
c.Next() c.Next()
} }
@ -135,10 +127,6 @@ func listThemes(c *gin.Context) {
return return
} }
if has, _ := fic.HasStandaloneExercice(); has {
themes = append([]*fic.Theme{&fic.StandaloneExercicesTheme}, themes...)
}
c.JSON(http.StatusOK, themes) c.JSON(http.StatusOK, themes)
} }
@ -267,110 +255,3 @@ func getThemedExercicesStats(c *gin.Context) {
} }
c.JSON(http.StatusOK, ret) 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) { func treatDir(p string) {
var expath 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)) { if sync.GlobalImporter.Exists(path.Join(p, f)) {
expath = p expath = p
break break
@ -108,7 +108,7 @@ func treatExercice(expath string) {
paramsFiles, err := sync.GetExerciceFilesParams(sync.GlobalImporter, exercice) paramsFiles, err := sync.GetExerciceFilesParams(sync.GlobalImporter, exercice)
if err != nil { 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 return
} }

View file

@ -4,7 +4,7 @@ const indextpl = `<!DOCTYPE html>
<html ng-app="FICApp"> <html ng-app="FICApp">
<head> <head>
<meta charset="utf-8"> <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/bootstrap.min.css" type="text/css" rel="stylesheet">
<link href="{{.urlbase}}css/glyphicon.css" type="text/css" rel="stylesheet" media="screen"> <link href="{{.urlbase}}css/glyphicon.css" type="text/css" rel="stylesheet" media="screen">
<style> <style>
@ -86,7 +86,7 @@ const indextpl = `<!DOCTYPE html>
<body class="bg-light text-dark"> <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}"> <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="."> <a class="navbar-brand" href=".">
<img alt="{{ .title }}" src="{{ .logo }}" style="height: 30px"> <img alt="FIC" src="img/fic.png" style="height: 30px">
</a> </a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#adminMenu" aria-controls="adminMenu" aria-expanded="false" aria-label="Toggle navigation"> <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> <span class="navbar-toggler-icon"></span>
@ -95,7 +95,7 @@ const indextpl = `<!DOCTYPE html>
<div class="collapse navbar-collapse" id="adminMenu"> <div class="collapse navbar-collapse" id="adminMenu">
<ul class="navbar-nav mr-auto"> <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('/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('/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('/exercices')}"><a class="nav-link" href="exercices">Exercices</a></li>
<li class="nav-item" ng-class="{'active': $location.path().startsWith('/public')}"><a class="nav-link" href="public/0">Public</a></li> <li class="nav-item" ng-class="{'active': $location.path().startsWith('/public')}"><a class="nav-link" href="public/0">Public</a></li>
@ -124,7 +124,7 @@ const indextpl = `<!DOCTYPE html>
</div> </div>
<span id="clock" class="navbar-text" ng-controller="CountdownController" ng-cloak> <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="circle1" class="circle-anim border-danger"></div>
<div style="position: absolute;" id="circle2" class="circle-anim border-info"></div> <div style="position: absolute;" id="circle2" class="circle-anim border-info"></div>
</div> </div>

View file

@ -199,10 +199,10 @@ func main() {
} }
log.Println("Using", sync.GlobalImporter.Kind()) log.Println("Using", sync.GlobalImporter.Kind())
challengeinfo, err := sync.GetFileContent(sync.GlobalImporter, settings.ChallengeFile) // Update distributed challenge.json
if err == nil { if _, err := os.Stat(path.Join(settings.SettingsDir, settings.ChallengeFile)); os.IsNotExist(err) {
// Initial distribution of challenge.json challengeinfo, err := sync.GetFileContent(sync.GlobalImporter, settings.ChallengeFile)
if _, err := os.Stat(path.Join(settings.SettingsDir, settings.ChallengeFile)); os.IsNotExist(err) { if err == nil {
if fd, err := os.Create(path.Join(settings.SettingsDir, settings.ChallengeFile)); err != nil { if fd, err := os.Create(path.Join(settings.SettingsDir, settings.ChallengeFile)); err != nil {
log.Fatal("Unable to open SETTINGS/challenge.json:", err) log.Fatal("Unable to open SETTINGS/challenge.json:", err)
} else { } 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" "errors"
"log" "log"
"net/http" "net/http"
"os"
"path" "path"
"strings" "strings"
"text/template" "text/template"
@ -26,24 +25,10 @@ var assets embed.FS
var indexPage []byte var indexPage []byte
func genIndex(baseURL string) { 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("") b := bytes.NewBufferString("")
if indexTmpl, err := template.New("index").Parse(indextpl); err != nil { if indexTmpl, err := template.New("index").Parse(indextpl); err != nil {
log.Fatal("Cannot create template:", err) 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) log.Fatal("An error occurs during template execution:", err)
} else { } else {
indexPage = b.Bytes() indexPage = b.Bytes()
@ -65,9 +50,6 @@ func declareStaticRoutes(router *gin.RouterGroup, cfg *settings.Settings, baseUR
router.GET("/", func(c *gin.Context) { router.GET("/", func(c *gin.Context) {
serveIndex(c) serveIndex(c)
}) })
router.GET("/auth/*_", func(c *gin.Context) {
serveIndex(c)
})
router.GET("/claims/*_", func(c *gin.Context) { router.GET("/claims/*_", func(c *gin.Context) {
serveIndex(c) serveIndex(c)
}) })
@ -80,9 +62,6 @@ func declareStaticRoutes(router *gin.RouterGroup, cfg *settings.Settings, baseUR
router.GET("/files", func(c *gin.Context) { router.GET("/files", func(c *gin.Context) {
serveIndex(c) serveIndex(c)
}) })
router.GET("/forge-links", func(c *gin.Context) {
serveIndex(c)
})
router.GET("/public/*_", func(c *gin.Context) { router.GET("/public/*_", func(c *gin.Context) {
serveIndex(c) serveIndex(c)
}) })
@ -125,20 +104,8 @@ func declareStaticRoutes(router *gin.RouterGroup, cfg *settings.Settings, baseUR
}) })
router.GET("/files/*_", func(c *gin.Context) { router.GET("/files/*_", func(c *gin.Context) {
filepath := path.Join(fic.FilesDir, strings.TrimPrefix(strings.TrimPrefix(c.Request.URL.Path, baseURL), "/files")) // 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"))))
if st, err := os.Stat(filepath); os.IsNotExist(err) || st.Size() == 0 {
if st, err := os.Stat(filepath + ".gz"); err == nil {
if fd, err := os.Open(filepath + ".gz"); err == nil {
c.DataFromReader(http.StatusOK, st.Size(), "application/octet-stream", fd, map[string]string{
"Content-Encoding": "gzip",
})
return
}
}
}
c.File(filepath)
}) })
router.GET("/submissions/*_", func(c *gin.Context) { 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")))) 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") 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>
<div class="col-4"> <div class="col-4">
<div ng-controller="ExerciceFlagDepsController" ng-init="init(flag)"> <div ng-controller="ExerciceFlagDepsController" ng-init="init(flag)">
<strong>Dépendances&nbsp;:</strong> Dépendances&nbsp;:
<ul ng-if="deps.length > 0"> <ul ng-if="deps.length > 0">
<dependancy ng-repeat="dep in deps" dep="dep"></dependancy> <dependancy ng-repeat="dep in deps" dep="dep"></dependancy>
</ul> </ul>
<span ng-if="deps.length == 0"> sans</span> <span ng-if="deps.length == 0"> sans</span>
</div> </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>
<div class="col-4" ng-controller="ExerciceFlagChoicesController"> <div class="col-4" ng-controller="ExerciceFlagChoicesController">
<div class="btn-toolbar justify-content-end mb-2" role="toolbar"> <div class="btn-toolbar justify-content-end mb-2" role="toolbar">
@ -185,23 +168,6 @@
<dependancy ng-repeat="dep in deps" dep="dep"></dependancy> <dependancy ng-repeat="dep in deps" dep="dep"></dependancy>
</ul> </ul>
<span ng-if="deps.length == 0"> sans</span> <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> </div>
</form> </form>
</div> </div>

View file

@ -10,27 +10,11 @@
<div class="ml-auto d-flex flex-row-reverse text-nowrap"> <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}}/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> <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="ml-2 btn btn-sm btn-light"><span class="glyphicon glyphicon-refresh" aria-hidden="true"></span> Synchroniser</button>
<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>
<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> <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> </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"> <div class="row mb-5">
<form class="col-md-8" ng-submit="saveExercice()"> <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"> <form ng-submit="saveFile()" class="list-group-item bg-light text-dark" ng-repeat="file in files">
<div class="row form-group"> <div class="row form-group">
<input type="text" ng-model="file.name" class="col form-control form-control-sm" placeholder="Nom de fichier"> <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="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> <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> </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 }}#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> <a href="exercices/{{ row.primary }}#hint-{{ row.secondary }}" ng-if="row.kind == 'hint'">{{ row.secondary_title }}</a>
</span> </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 != 'solved'">: {{ 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>
</td> </td>
<td style="vertical-align: middle; padding: 0; background-color: {{ row.team_color }}" ng-show="logged"> <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> <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> <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="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="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> </h2>
<p><input type="search" class="form-control" placeholder="Search" ng-model="query" autofocus></p> <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> <div class="spinner-border spinner-border-sm" role="status" ng-if="file.gunzipWIP"></div>
</button> </button>
</td> </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] }} {{ file[field] }}
<span ng-if="field == 'id' && file.err !== undefined && file.err !== true" title="{{ file.err }}" class="glyphicon glyphicon-exclamation-sign"></span> <span ng-if="field == 'id' && file.err !== undefined && file.err !== true" title="{{ file.err }}" class="glyphicon glyphicon-exclamation-sign"></span>
</td> </td>
<td style="max-width: 100px"> <td>
<div class="text-truncate" title="{{ file.checksum | bto16 }}">{{ file.checksum | bto16 }}</div> {{ file.checksum | bto16 }}
<div class="text-truncate" ng-if="file.checksum_shown" title="{{ file.checksum_shown | bto16 }}">{{ file.checksum_shown | bto16 }}</div> <div ng-if="file.checksum_shown">{{ file.checksum_shown | bto16 }}</div>
</td> </td>
</tr> </tr>
</tbody> </tbody>

View file

@ -9,20 +9,16 @@
<tr> <tr>
<th>Chemin</th> <th>Chemin</th>
<th>Branche</th> <th>Branche</th>
<th>Commit <span class="text-muted">Plus récent</span></th> <th>Commit</th>
<th>Plus récent</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr ng-repeat="repository in repositories"> <tr ng-repeat="repository in repositories">
<td>{{ repository.path }}</td> <td>{{ repository.path }}</td>
<td>{{ repository.branch }}</td> <td>{{ repository.branch }}</td>
<td> <td>{{ repository.hash }}</td>
{{ repository.hash }}<br> <td><repository-uptodate repository="repository" /></td>
<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>
</tr> </tr>
</tbody> </tbody>
</table> </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> <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="col-sm-9">
<div class="input-group"> <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"> <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> <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> </div>
@ -46,14 +46,14 @@
<div class="form-group row"> <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> <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"> <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>
<div class="col-sm-1 text-right"> <div class="col-sm-1 text-right">
<label for="duration" class="col-form-label col-form-label-sm">Durée</label> <label for="duration" class="col-form-label col-form-label-sm">Durée</label>
</div> </div>
<div class="col-sm-2"> <div class="col-sm-2">
<div class="input-group input-group-sm"> <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"> <div class="input-group-append">
<span class="input-group-text">min</span> <span class="input-group-text">min</span>
</div> </div>
@ -64,60 +64,46 @@
<hr> <hr>
<div class="form-group row"> <div class="form-group row">
<div class="col-sm row"> <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>
<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-sm-1">
<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}">
<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> </div>
<div class="col-sm row"> <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>
<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-sm-1">
<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}">
<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> </div>
<div class="col-sm row"> <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>
<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-sm-1">
<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}">
<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> </div>
<div class="col-sm row"> <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>
<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-sm-1">
<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}">
<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>
</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"> <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"> <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}"> <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> </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."> <div class="col-sm row">
<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> <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"> <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}"> <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> </div>
<div class="col-sm row" title="Coefficient de base retiré pour chaque soumission invalide au delà de 10 soumissions"> <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 tentative</label> <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"> <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}"> <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> </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> </div>
<hr> <hr>
@ -320,7 +306,7 @@
</div> </div>
</form> </form>
<div class="card my-3"> <form ng-submit="addDelegatedQA()" class="card my-3">
<div class="card-header"> <div class="card-header">
<h3>Managers QA</h3> <h3>Managers QA</h3>
</div> </div>
@ -332,23 +318,22 @@
<span class="glyphicon glyphicon-remove" aria-hidden="true"></span> <span class="glyphicon glyphicon-remove" aria-hidden="true"></span>
</button> </button>
</li> </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> </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>
</div> </form>
<form ng-submit="saveChallengeInfo()" class="card my-3"> <form ng-submit="saveChallengeInfo()" class="card my-3">
<div class="card-header"> <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] }}"> <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" }} Dernier import&nbsp;: {{ syncReport._updated[syncReport._updated.length-1] | date:"medium" }}
</div> </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 Voir les dépôts
</a> </a>
</div> </div>
@ -32,25 +32,17 @@
<div class="card-body"> <div class="card-body">
<dl class="row"> <dl class="row">
<dt class="col-2">Type</dt> <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> <dt class="col-2">Synchronisation</dt>
<dd class="col-10" title="{{ syncStatus['sync'] }}" ng-bind="syncStatus.sync"></dd> <dd class="col-10" title="{{ configro['sync'] }}" ng-bind="configro.sync"></dd>
<dt class="col-2" ng-if="syncStatus['sync-id']">ID</dt> <dt class="col-2" ng-if="configro['sync-id']">ID</dt>
<dd class="col-10" ng-if="syncStatus['sync-id']"> <dd class="col-10" ng-if="configro['sync-id']">{{ configro['sync-id'] }}</dd>
{{ syncStatus['sync-id'] }} <dt class="col-2" ng-if="configro['sync']">Statut</dt>
<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 class="col-10" ng-if="configro['sync']">{{ syncProgress }}</dd>
</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>
</dl> </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="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="d-flex justify-content-around" ng-if="syncStatus.sync">
<div class="btn-group dropright"> <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" 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"> <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> </button>
<div class="dropdown-menu" ng-controller="ThemesListController" style="max-height: 45vh; overflow: auto"> <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(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>
</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> <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> </ul>
</div> </div>
</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>
<h2> &Eacute;quipes
&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>
</h2> <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>
<div> <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="show('new')" class="btn btn-sm btn-primary ml-1"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Ajouter une équipe</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>
<form class="d-inline"> <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>
<input id="crTeamsInput" type="file" file-model="selectedFile" class="d-none" /> </h2>
<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>
<p><input type="search" class="form-control" placeholder="Search" ng-model="query" ng-keypress="validateSearch($event)" autofocus></p> <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"> <table class="table table-hover table-bordered table-striped table-sm">

View file

@ -13,7 +13,7 @@
<th>Date</th> <th>Date</th>
</thead> </thead>
<tbody> <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> <td>
<a ng-repeat="exercice in exercices" ng-if="exercice.id == row.id_exercice" href="exercices/{{ row.id_exercice }}">{{ exercice.title }}</a> <a ng-repeat="exercice in exercices" ng-if="exercice.id == row.id_exercice" href="exercices/{{ row.id_exercice }}">{{ exercice.title }}</a>
</td> </td>
@ -23,12 +23,9 @@
<td> <td>
{{ row.points * row.coeff }} {{ row.points * row.coeff }}
</td> </td>
<td ng-if="!row.reason.startsWith('Response ')"> <td>
{{ row.points }} * {{ row.coeff }} {{ row.points }} * {{ row.coeff }}
</td> </td>
<td ng-if="row.reason.startsWith('Response ')">
{{ row.points }} * {{ settings.questionGainRatio }} / {{ settings.questionGainRatio / row.coeff }}
</td>
<td> <td>
<nobr title="{{ row.time }}">{{ row.time | date:"mediumTime" }}</nobr> <nobr title="{{ row.time }}">{{ row.time | date:"mediumTime" }}</nobr>
</td> </td>
@ -36,7 +33,7 @@
<tfoot> <tfoot>
<th></th> <th></th>
<th></th> <th></th>
<th>{{ my.score100 / 100 }}</th> <th>{{ my.score }}</th>
</thead> </thead>
</tbody> </tbody>
</table> </table>

View file

@ -37,7 +37,7 @@
<dt ng-bind="theme.name"></dt> <dt ng-bind="theme.name"></dt>
<dd> <dd>
<ul class="list-unstyled"> <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> </ul>
</dd> </dd>
</div> </div>

View file

@ -2,7 +2,6 @@
Thèmes 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="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> <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> </h2>
<p><input type="search" class="form-control" placeholder="Search" ng-model="query" ng-keypress="validateSearch($event)" autofocus></p> <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> </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"> <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"> <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'"> <input type="checkbox" class="form-check-input" id="{{ field }}" ng-model="theme[field]" ng-if="field == 'locked'">
<label for="{{ field }}">{{ field | capitalize }}</label> <label for="{{ field }}">{{ field | capitalize }}</label>
@ -38,14 +25,11 @@
</div> </div>
</form> </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> <h3>
Exercices ({{ exercices.length }}) 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> <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="float-right btn btn-sm btn-light ml-2"><span class="glyphicon glyphicon-refresh" aria-hidden="true"></span> Synchroniser</button>
<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>
</h3> </h3>
<p><input type="search" class="form-control form-control-sm" placeholder="Search" ng-model="query" autofocus></p> <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 { if theme == nil {
return &ThemeError{ return &ThemeError{
error: err, 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 { func (e *ChallengeTxtError) Error() string {
if e.ChallengeTxtLine != 0 { 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 { } 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 { 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 { type FlagError struct {
@ -144,5 +144,5 @@ func NewFlagError(exercice *fic.Exercice, flag *ExerciceFlag, line int, err erro
} }
func (e *FlagError) Error() string { 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:11:typo_guillemets_typographiques_doubles_fermants
0-exercice-1/resolution.md:spelling:cronjob 0-exercice-1/resolution.md:spelling:cronjob
0-exercice-1/resolution.md:spelling:Level 0-exercice-1/resolution.md:spelling:Level
challenge.toml:spelling:time challenge.txt:spelling:time
challenge.toml:spelling:ago challenge.txt:spelling:ago
0-exercice-1/resolution.md:spelling:SCL 0-exercice-1/resolution.md:spelling:SCL
challenge.toml:spelling:SCL` challenge.txt:spelling:SCL`
func TestLoadExceptions(t *testing.T) { func TestLoadExceptions(t *testing.T) {
exceptions := ParseExceptionString(sampleFile, nil) exceptions := ParseExceptionString(sampleFile, nil)
@ -47,7 +47,7 @@ func TestFilterExceptions(t *testing.T) {
t.Fatalf("Expected 1 exceptions, got %d", len(*filteredExceptions)) t.Fatalf("Expected 1 exceptions, got %d", len(*filteredExceptions))
} }
filteredExceptions = exceptions.GetFileExceptions("challenge.toml") filteredExceptions = exceptions.GetFileExceptions("challenge.txt")
if len(*filteredExceptions) != 3 { if len(*filteredExceptions) != 3 {
t.Fatalf("Expected 3 exceptions, got %d", len(*filteredExceptions)) 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 { if params, _, err = parseExerciceParams(i, exercice.Path); err != nil {
errs = multierr.Append(errs, NewChallengeTxtError(exercice, 0, err)) errs = multierr.Append(errs, NewChallengeTxtError(exercice, 0, err))
} else if len(params.Flags) == 0 && len(params.FlagsUCQ) == 0 && len(params.FlagsMCQ) == 0 { } 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 { } else {
// Treat legacy UCQ flags as ExerciceFlag // Treat legacy UCQ flags as ExerciceFlag
for _, flag := range params.FlagsUCQ { for _, flag := range params.FlagsUCQ {

View file

@ -60,7 +60,7 @@ func BuildFilesListInto(i Importer, exercice *fic.Exercice, into string) (files
// Parse DIGESTS.txt // Parse DIGESTS.txt
if digs, err := GetFileContent(i, path.Join(exercice.Path, into, "DIGESTS.txt")); err != nil { 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))) 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{} digests = map[string][]byte{}
for nline, d := range strings.Split(digs, "\n") { for nline, d := range strings.Split(digs, "\n") {
if dsplt := strings.SplitN(d, " ", 2); len(dsplt) < 2 { if dsplt := strings.SplitN(d, " ", 2); len(dsplt) < 2 {
@ -315,57 +315,9 @@ func DownloadExerciceFile(pf ExerciceFile, dest string, exercice *fic.Exercice,
return return
} }
type importedFile struct { // SyncExerciceFiles reads the content of files/ directory and import it as EFile for the given challenge.
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.
// It takes care of DIGESTS.txt and ensure imported files match. // 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 { if _, err := exercice.WipeFiles(); err != nil {
errs = multierr.Append(errs, err) errs = multierr.Append(errs, err)
} }
@ -376,41 +328,63 @@ func ImportExerciceFiles(i Importer, exercice *fic.Exercice, exceptions *CheckEx
return return
} }
actionAfterImport := func(fname string, digests map[string][]byte, filePath, origin string) (interface{}, error) { files, digests, berrs := BuildFilesListInto(i, exercice, "files")
var digest_shown []byte errs = multierr.Append(errs, berrs)
if strings.HasSuffix(fname, ".gz") {
if d, exists := digests[strings.TrimSuffix(fname, ".gz")]; exists {
digest_shown = d
}
}
published := true // Import standard files
disclaimer := "" for _, fname := range files {
if f, exists := paramsFiles[fname]; exists { actionAfterImport := func(filePath string, origin string) (interface{}, error) {
published = !f.Hidden var digest_shown []byte
if strings.HasSuffix(fname, ".gz") {
// Call checks hooks if d, exists := digests[strings.TrimSuffix(fname, ".gz")]; exists {
for _, hk := range hooks.mdTextHooks { digest_shown = d
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 { published := true
errs = multierr.Append(errs, NewFileError(exercice, fname, fmt.Errorf("error during markdown formating of disclaimer: %w", err))) 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) if pf, exists := paramsFiles[fname]; exists && pf.URL != "" {
errs = multierr.Append(errs, berrs) dest := GetDestinationFilePath(pf.URL, &pf.Filename)
// Import files in db if _, err := os.Stat(dest); !os.IsNotExist(err) {
for _, file := range files { if d, err := actionAfterImport(dest, pf.URL); err == nil {
fname := file.Name f = d
f := file.file }
}
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 { if f.(*fic.EFile).Size == 0 {
errs = multierr.Append(errs, NewFileError(exercice, fname, fmt.Errorf("imported file is empty!"))) 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 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. // ApiGetRemoteExerciceFiles is an accessor to remote exercice files list.
func ApiGetRemoteExerciceFiles(c *gin.Context) { func ApiGetRemoteExerciceFiles(c *gin.Context) {
files, err := GetRemoteExerciceFiles(c.Params.ByName("thid"), c.Params.ByName("exid")) theme, exceptions, errs := BuildTheme(GlobalImporter, c.Params.ByName("thid"))
if err != nil { if theme != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) 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 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.. // CheckExerciceHints checks if all hints are corrects..
func CheckExerciceHints(i Importer, exercice *fic.Exercice, exceptions *CheckExceptions) ([]importHint, error) { 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) 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 { if _, err := exercice.WipeHints(); err != nil {
errs = multierr.Append(errs, err) errs = multierr.Append(errs, err)
} else { } else {
exceptions = exceptions.GetFileExceptions("challenge.toml", "challenge.txt") exceptions = exceptions.GetFileExceptions("challenge.txt", "challenge.toml")
hints, berrs := buildExerciceHints(i, exercice, exceptions) hints, berrs := buildExerciceHints(i, exercice, exceptions)
errs = multierr.Append(errs, berrs) errs = multierr.Append(errs, berrs)
@ -169,32 +169,25 @@ func SyncExerciceHints(i Importer, exercice *fic.Exercice, flagsBindings map[int
return 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. // ApiListRemoteExerciceHints is an accessor letting foreign packages to access remote exercice hints.
func ApiGetRemoteExerciceHints(c *gin.Context) { func ApiGetRemoteExerciceHints(c *gin.Context) {
hints, errs := GetRemoteExerciceHints(c.Params.ByName("thid"), c.Params.ByName("exid")) theme, exceptions, errs := BuildTheme(GlobalImporter, c.Params.ByName("thid"))
if hints != nil { 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)) c.AbortWithStatusJSON(http.StatusInternalServerError, fmt.Errorf("%q", errs))
return 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 { type importFlag struct {
origin ExerciceFlag
Line int Line int
Flag fic.Flag Flag fic.Flag
JustifyOf *fic.MCQ_entry JustifyOf *fic.MCQ_entry
@ -356,10 +355,8 @@ func buildExerciceFlag(i Importer, exercice *fic.Exercice, flag ExerciceFlag, nl
flag.Type = "radio" flag.Type = "radio"
case "mcq": case "mcq":
flag.Type = "mcq" flag.Type = "mcq"
case "justified":
flag.Type = "justified"
default: 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 return
} }
@ -393,9 +390,8 @@ func buildExerciceFlag(i Importer, exercice *fic.Exercice, flag ExerciceFlag, nl
errs = multierr.Append(errs, berrs) errs = multierr.Append(errs, berrs)
if addedFlag != nil { if addedFlag != nil {
ret = append(ret, importFlag{ ret = append(ret, importFlag{
origin: flag, Line: nline + 1,
Line: nline + 1, Flag: addedFlag,
Flag: addedFlag,
}) })
} }
} else if flag.Type == "key" || strings.HasPrefix(flag.Type, "number") || flag.Type == "text" || flag.Type == "ucq" || flag.Type == "radio" || flag.Type == "vector" { } 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) errs = multierr.Append(errs, berrs)
if addedFlag != nil { if addedFlag != nil {
ret = append(ret, importFlag{ ret = append(ret, importFlag{
origin: flag,
Line: nline + 1, Line: nline + 1,
Flag: *addedFlag, Flag: *addedFlag,
Choices: choices, Choices: choices,
}) })
} }
} else if flag.Type == "mcq" || flag.Type == "justified" { } else if flag.Type == "mcq" {
addedFlag := fic.MCQ{ addedFlag := fic.MCQ{
IdExercice: exercice.Id, IdExercice: exercice.Id,
Order: int8(nline + 1), Order: int8(nline + 1),
@ -418,7 +413,7 @@ func buildExerciceFlag(i Importer, exercice *fic.Exercice, flag ExerciceFlag, nl
} }
hasOne := false hasOne := false
isJustified := flag.Type == "justified" isJustified := false
if len(flag.Variant) != 0 { 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"))) 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) errs = multierr.Append(errs, berrs)
if addedFlag != nil { if addedFlag != nil {
ret = append(ret, importFlag{ ret = append(ret, importFlag{
origin: flag,
Line: nline + 1, Line: nline + 1,
Flag: *addedFlag, Flag: *addedFlag,
JustifyOf: entry, JustifyOf: entry,
@ -483,9 +477,8 @@ func buildExerciceFlag(i Importer, exercice *fic.Exercice, flag ExerciceFlag, nl
} }
ret = append([]importFlag{importFlag{ ret = append([]importFlag{importFlag{
origin: flag, Line: nline + 1,
Line: nline + 1, Flag: &addedFlag,
Flag: &addedFlag,
}}, ret...) }}, ret...)
} }
return 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. // 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) { 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) flags, flagsids, berrs := buildExerciceFlags(i, exercice, exceptions)
errs = multierr.Append(errs, berrs) 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 { 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"))) 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 // Check dependency loop
@ -645,7 +634,7 @@ func SyncExerciceFlags(i Importer, exercice *fic.Exercice, exceptions *CheckExce
} else if _, err := exercice.WipeMCQs(); err != nil { } else if _, err := exercice.WipeMCQs(); err != nil {
errs = multierr.Append(errs, err) errs = multierr.Append(errs, err)
} else { } else {
exceptions = exceptions.GetFileExceptions("challenge.toml", "challenge.txt") exceptions = exceptions.GetFileExceptions("challenge.txt", "challenge.toml")
flags, flagids, berrs := buildExerciceFlags(i, exercice, exceptions) flags, flagids, berrs := buildExerciceFlags(i, exercice, exceptions)
errs = multierr.Append(errs, berrs) errs = multierr.Append(errs, berrs)
@ -699,32 +688,26 @@ func SyncExerciceFlags(i Importer, exercice *fic.Exercice, exceptions *CheckExce
return 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. // ApiListRemoteExerciceFlags is an accessor letting foreign packages to access remote exercice flags.
func ApiGetRemoteExerciceFlags(c *gin.Context) { func ApiGetRemoteExerciceFlags(c *gin.Context) {
flags, err := GetRemoteExerciceFlags(c.Params.ByName("thid"), c.Params.ByName("exid")) theme, exceptions, errs := BuildTheme(GlobalImporter, c.Params.ByName("thid"))
if err != nil { if theme != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) 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 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 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 var e *fic.Exercice
e, err = theme.GetExerciceByTitle(ename) e, err = theme.GetExerciceByTitle(ename)
if err != nil { if err != nil {
return dmap, fmt.Errorf("unable to GetExerciceByTitle(ename=%q, tid=%d): %w", ename, theme.Id, err) return
} }
dmap[int64(eid)] = e dmap[int64(eid)] = e
@ -287,7 +280,7 @@ func BuildExercice(i Importer, theme *fic.Theme, epath string, dmap *map[int64]*
e.WIP = p.WIP e.WIP = p.WIP
if p.WIP && !AllowWIPExercice { 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 { 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 len(e.Image) > 0 {
if _, err := i.importFile(e.Image, if _, err := i.importFile(e.Image,
func(filePath string, origin string) (interface{}, error) { 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 return nil, err
} }
@ -475,57 +468,59 @@ func SyncExercices(i Importer, theme *fic.Theme, exceptions *CheckExceptions) (e
return 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. // ApiListRemoteExercices is an accessor letting foreign packages to access remote exercices list.
func ApiListRemoteExercices(c *gin.Context) { func ApiListRemoteExercices(c *gin.Context) {
exercices, err := ListRemoteExercices(c.Params.ByName("thid")) if c.Params.ByName("thid") == "_" {
if err != nil { exercices, err := GetExercices(GlobalImporter, &fic.Theme{Path: StandaloneExercicesDirectory})
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.JSON(http.StatusOK, exercices)
return 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) { c.JSON(http.StatusOK, exercices)
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
} else { } 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 return
} }
} }

View file

@ -172,6 +172,10 @@ func GetFileContent(i Importer, URI string) (string, error) {
buf = append(buf, b) buf = append(buf, b)
} }
if len(buf) == 0 {
return "", fmt.Errorf("File is empty")
}
return strings.TrimSpace(string(buf)), nil 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) return path.Join(fic.FilesDir, strings.ToLower(base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(hash[:])), *filename)
} }
var fileWriter = fileWriterToFS func importFile(i Importer, URI string, dest string) error {
func SetWriteFileFunc(writerFunc func(dest string) (io.WriteCloser, error)) {
fileWriter = writerFunc
}
func fileWriterToFS(dest string) (io.WriteCloser, error) {
if err := os.MkdirAll(path.Dir(dest), 0751); err != nil { if err := os.MkdirAll(path.Dir(dest), 0751); err != nil {
return nil, err return err
} }
return os.Create(dest) // Write file
} if fdto, err := os.Create(dest); err != nil {
func importFile(i Importer, URI string, dest string) error {
if fdfrom, closer, err := GetFile(i, URI); err != nil {
os.Remove(dest)
return err return err
} else { } else {
defer closer()
fdto, err := fileWriter(dest)
if err != nil {
return err
}
defer fdto.Close() defer fdto.Close()
_, err = io.Copy(fdto, fdfrom) if fdfrom, closer, err := GetFile(i, URI); err != nil {
return err 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) 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%). // DeepSyncProgress expose the progression of the depp synchronization (0 = 0%, 255 = 100%).
var DeepSyncProgress uint8 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 { type SyncReport struct {
DateStart time.Time `json:"_started"` DateStart time.Time `json:"_started"`
DateEnd time.Time `json:"_ended"` DateEnd time.Time `json:"_ended"`
@ -72,9 +56,13 @@ func SpeedySyncDeep(i Importer) (errs SyncReport) {
errs.ThemesSync = append(errs.ThemesSync, sterr.Error()) errs.ThemesSync = append(errs.ThemesSync, sterr.Error())
} }
if themes, err := fic.GetThemesExtended(); err == nil { if themes, err := fic.GetThemes(); err == nil {
DeepSyncProgress = 2 DeepSyncProgress = 2
if i.Exists(StandaloneExercicesDirectory) {
themes = append(themes, &fic.Theme{Path: StandaloneExercicesDirectory})
}
var themeStep uint8 = uint8(250) / uint8(len(themes)) var themeStep uint8 = uint8(250) / uint8(len(themes))
for tid, theme := range themes { for tid, theme := range themes {
@ -139,9 +127,14 @@ func SyncDeep(i Importer) (errs SyncReport) {
} }
// Synchronize themes // Synchronize themes
if themes, err := fic.GetThemesExtended(); err == nil { if themes, err := fic.GetThemes(); err == nil {
DeepSyncProgress = 2 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)) var themeStep uint8 = uint8(250) / uint8(len(themes))
for tid, theme := range 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) 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 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 DeepSyncProgress += exerciceStep / 3
flagsBindings, ferrs := SyncExerciceFlags(i, exercice, ex_exceptions[eid]) 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 var oneGitPull sync.Mutex
func OneGitPullStatus() bool {
if oneGitPull.TryLock() {
oneGitPull.Unlock()
return true
}
return false
}
func countFileInDir(dirname string) (int, error) { func countFileInDir(dirname string) (int, error) {
files, err := os.ReadDir(dirname) files, err := os.ReadDir(dirname)
if err != nil { if err != nil {
@ -65,10 +57,6 @@ func (i GitImporter) Kind() string {
return "git originated from " + i.Remote + " on " + i.li.Kind() 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) { func getForgeBaseLink(remote string) (u *url.URL, err error) {
res := gitRemoteRe.FindStringSubmatch(remote) res := gitRemoteRe.FindStringSubmatch(remote)
u, err = url.Parse(res[2]) 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 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 return
} }
@ -241,7 +241,7 @@ func (i GitImporter) GetExerciceLink(e *fic.Exercice) (u *url.URL, err error) {
return 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 return
} }

View file

@ -113,11 +113,3 @@ func (i LocalImporter) ListDir(filename string) ([]string, error) {
func (i LocalImporter) Stat(filename string) (os.FileInfo, error) { func (i LocalImporter) Stat(filename string) (os.FileInfo, error) {
return os.Stat(path.Join(i.Base, filename)) 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 ( import (
"bytes" "bytes"
"encoding/base32" "encoding/base32"
"io"
"net/url" "net/url"
"os"
"path" "path"
"strings" "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) 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))) child.Destination = []byte(path.Join(t.absPath, string(child.Destination)))
err := importFile(t.importer, path.Join(t.rootDir, iPath), dPath) if err := os.MkdirAll(path.Dir(dPath), 0755); err != nil {
if err != nil {
return ast.WalkStop, err 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 return ast.WalkContinue, nil

View file

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

View file

@ -20,24 +20,3 @@ ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCUKevt/f1n2byv5oH43iQsZ7b4kAATHlHNUF6WMQjk
# ? # ?
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICo5yumHfQbMwhZAtEZByQR0xIVcoealS7g4MNTMEVaX roote@roote-VirtualBox 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' 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 ] if [ $# -gt 0 ]
then then
which jq > /dev/null 2> /dev/null || { echo "Please install jq" >&2; exit 1; } 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; include fic-auth.conf;
rewrite ^/.*$ /index.html; rewrite ^/.*$ /index.html;

View file

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

View file

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

View file

@ -21,8 +21,6 @@ OLD_KEY=$(cat /run/config/dm-crypt/key)
[ "${NEW_KEY}" != "${OLD_KEY}" ] && { [ "${NEW_KEY}" != "${OLD_KEY}" ] && {
read -p "DM-CRYPT key changed in metadata, are you sure you want to erase it? (y/N) " V 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 [ "$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
echo "Metadata drive not erased" echo "Metadata drive not erased"
echo echo

View file

@ -365,7 +365,7 @@
<div class="carousel slide" data-interval="12000" style="padding-bottom: 0px" autocarousel> <div class="carousel slide" data-interval="12000" style="padding-bottom: 0px" autocarousel>
<div class="carousel-inner"> <div class="carousel-inner">
<div class="carousel-item" ng-repeat="theme in themes" ng-class="{active: $first}"> <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> <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> <h3 class="text-left" ng-bind="theme.name"></h3>
<p class="text-justify" style="font-size: 111%" ng-bind-html="theme.headline"></p> <p class="text-justify" style="font-size: 111%" ng-bind-html="theme.headline"></p>
@ -377,8 +377,7 @@
</div> </div>
<div class="card niceborder bg-dark" ng-if="s.type == 'exercice' && !s.params.hide"> <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}}')"></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-body text-light"> <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> <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> <p ng-bind-html="my.exercices[s.params.exercice].overview"></p>
@ -398,8 +397,7 @@
Challenges à la une Challenges à la une
</span> </span>
</div> </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}}')"></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-body text-light"> <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> <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> <p ng-bind-html="my.exercices[lastExercice].overview"></p>

View file

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

View file

@ -1,54 +1,54 @@
kernel: kernel:
#image: nemunaire/kernel:5.10.62-0b705d955f5e283f62583c4e227d64a7924c138f-amd64 #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" cmdline: "console=ttyS0 console=tty0"
init: init:
- linuxkit/init:8eea386739975a43af558eec757a7dcb3a3d2e7b - linuxkit/init:v1.0.0
- linuxkit/runc:667e7ea2c426a2460ca21e3da065a57dbb3369c9 - linuxkit/runc:6062483d748609d505f2bcde4e52ee64a3329f5f
- linuxkit/containerd:a988a1a8bcbacc2c0390ca0c08f949e2b4b5915d - linuxkit/containerd:v1.0.0
- linuxkit/ca-certificates:7b32a26ca9c275d3ef32b11fe2a83dbd2aee2fdb - linuxkit/ca-certificates:v1.0.0
- linuxkit/getty:05eca453695984a69617f1f1f0bcdae7f7032967 - linuxkit/getty:v1.0.0
- nemunaire/mdadm:04814350d71ba9417e1f861be1685de26adf7a67 - nemunaire/mdadm:04814350d71ba9417e1f861be1685de26adf7a67
- nemunaire/kexec:839b4eedfce02a56c581dec2383dc6faff120855 - nemunaire/kexec:839b4eedfce02a56c581dec2383dc6faff120855
onboot: onboot:
- name: mod - 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"] command: ["/bin/sh", "-c", "modprobe xhci_pci ahci intel_lpss_pci i2c_i801 megaraid_sas tg3 bnxt_en"]
- name: sysctl - name: sysctl
image: linuxkit/sysctl:5f56434b81004b50b47ed629b222619168c2bcdf image: linuxkit/sysctl:v1.0.0
binds: binds:
- /etc/sysctl.d/01-fic.conf:/etc/sysctl.d/01-fic.conf:ro - /etc/sysctl.d/01-fic.conf:/etc/sysctl.d/01-fic.conf:ro
# Metadata # Metadata
- name: metadata - name: metadata
image: linuxkit/metadata:4f81c0c3a2b245567fd7d32d799018c9614a9907 image: linuxkit/metadata:v1.0.0
command: ["/usr/bin/metadata", "-v", "cdrom"] command: ["/usr/bin/metadata", "-v", "cdrom"]
# Filesystem # Filesystem
- name: swap - name: swap
image: linuxkit/swap:f4b8ffef87c8c72165bd8a92b790ac252ccf1821 image: linuxkit/swap:v1.0.0
command: ["/sbin/swapon", "/dev/sda3"] command: ["/sbin/swapon", "/dev/sda3"]
- name: dm-crypt - name: dm-crypt
image: linuxkit/dm-crypt:981fde241bb84616a5ba94c04cdefa1489431a25 image: linuxkit/dm-crypt:d49723bc9d10c5ada9e03b0670f4e57416d5d084
command: ["/usr/bin/crypto", "-l", "crypt_fic", "/dev/sda4"] command: ["/usr/bin/crypto", "-l", "crypt_fic", "/dev/sda4"]
binds: binds:
- /dev:/dev - /dev:/dev
- /run/config/dm-crypt:/etc/dm-crypt - /run/config/dm-crypt:/etc/dm-crypt
- name: mount - name: mount
image: linuxkit/mount:cb8caa72248f7082fc2074ce843d53cdc15df04a image: linuxkit/mount:v1.0.0
command: ["/usr/bin/mountie", "-device", "/dev/mapper/crypt_fic", "/var/lib/fic" ] command: ["/usr/bin/mountie", "-device", "/dev/mapper/crypt_fic", "/var/lib/fic" ]
# Network # Network
# - name: dhcpcd # - name: dhcpcd
# image: linuxkit/dhcpcd:157df9ef45a035f1542ec2270e374f18efef98a5 # image: linuxkit/dhcpcd:v1.0.0
# command: ["/sbin/dhcpcd", "--nobackground", "-f", "/dhcpcd.conf", "-1"] # command: ["/sbin/dhcpcd", "--nobackground", "-f", "/dhcpcd.conf", "-1"]
# - name: ntp # - name: ntp
# image: linuxkit/openntpd:f99c4117763480815553b72022b426639a13ce86 # image: linuxkit/openntpd:v1.0.0
- name: synchro-ip-setup - 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;" ] command: ["/bin/sh", "-c", "ip a add 10.10.10.1/29 dev eth2; ip link set eth2 up;" ]
net: new net: new
runtime: runtime:
@ -57,7 +57,7 @@ onboot:
bindNS: bindNS:
net: /run/netns/synchro net: /run/netns/synchro
- name: qa-ip-setup - 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" ] 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 net: new
runtime: runtime:
@ -69,7 +69,7 @@ onboot:
bindNS: bindNS:
net: /run/netns/fic-qa net: /run/netns/fic-qa
- name: admin-ip-setup - 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 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; };" ] 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 net: new
@ -85,7 +85,7 @@ onboot:
bindNS: bindNS:
net: /run/netns/fic-admin net: /run/netns/fic-admin
- name: checker-ip-setup - 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;" ] command: ["/bin/sh", "-c", "ip a add 172.17.0.3/24 dev vethin-checker; ip link set vethin-checker up;" ]
net: new net: new
runtime: runtime:
@ -96,7 +96,7 @@ onboot:
bindNS: bindNS:
net: /run/netns/fic-checker net: /run/netns/fic-checker
- name: generator-ip-setup - 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;" ] command: ["/bin/sh", "-c", "ip a add 172.17.0.5/24 dev vethin-generat; ip link set vethin-generat up;" ]
net: new net: new
runtime: runtime:
@ -107,7 +107,7 @@ onboot:
bindNS: bindNS:
net: /run/netns/fic-generator net: /run/netns/fic-generator
- name: mysql-ip-setup - 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;" ] command: ["/bin/sh", "-c", "ip a add 172.17.0.4/24 dev vethin-db; ip link set vethin-db up;" ]
net: new net: new
runtime: runtime:
@ -118,7 +118,7 @@ onboot:
bindNS: bindNS:
net: /run/netns/db net: /run/netns/db
- name: bridge-setup - 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;" ] 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: runtime:
interfaces: interfaces:
@ -126,7 +126,7 @@ onboot:
add: bridge add: bridge
- name: firewall-synchro - 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" ] command: ["/bin/bash", "-c", "/sbin/iptables-restore < /etc/iptables/rules-synchro.v4; /sbin/ip6tables-restore < /etc/iptables/rules.v6" ]
binds: binds:
- /etc/iptables/rules-synchro.v4:/etc/iptables/rules-synchro.v4:ro - /etc/iptables/rules-synchro.v4:/etc/iptables/rules-synchro.v4:ro
@ -136,7 +136,7 @@ onboot:
mkdir: mkdir:
- /var/lib/fic/teams - /var/lib/fic/teams
- name: firewall-admin - 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" ] command: ["/bin/bash", "-c", "/sbin/iptables-restore < /etc/iptables/rules-admin.v4; /sbin/ip6tables-restore < /etc/iptables/rules.v6" ]
binds: binds:
- /etc/iptables/rules-admin.v4:/etc/iptables/rules-admin.v4:ro - /etc/iptables/rules-admin.v4:/etc/iptables/rules-admin.v4:ro
@ -144,7 +144,7 @@ onboot:
net: /run/netns/fic-admin net: /run/netns/fic-admin
- name: create-secrets - name: create-secrets
image: alpine:3.21 image: alpine:3.19
command: ["/bin/init_secrets.sh"] command: ["/bin/init_secrets.sh"]
binds: binds:
- /bin/init_secrets.sh:/bin/init_secrets.sh:ro - /bin/init_secrets.sh:/bin/init_secrets.sh:ro
@ -153,26 +153,17 @@ onboot:
mkdir: mkdir:
- /var/lib/fic/secrets - /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: services:
# - name: getty # - name: getty
# image: linuxkit/getty:05eca453695984a69617f1f1f0bcdae7f7032967 # image: linuxkit/getty:v1.0.0
# env: # env:
# - INSECURE=true # - INSECURE=true
# Enable acpi to shutdown on power events # Enable acpi to shutdown on power events
- name: acpid - name: acpid
image: linuxkit/acpid:6cb5575e487a8fcbd4c3eb6721c23299e6ea452f image: linuxkit/acpid:v1.0.0
- name: rngd - name: rngd
image: linuxkit/rngd:1a18f2149e42a0a1cb9e7d37608a494342c26032 image: linuxkit/rngd:v1.0.0
- name: db - name: db
image: mariadb:11 image: mariadb:11
command: ["/bin/bash", "/usr/local/bin/docker-entrypoint.sh", "mariadbd"] 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/generator:/srv/GENERATOR:ro
- /var/lib/fic/pki:/srv/PKI - /var/lib/fic/pki:/srv/PKI
- /var/lib/fic/settings:/srv/SETTINGS - /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/sync:/srv/SYNC
- /var/lib/fic/teams:/srv/TEAMS - /var/lib/fic/teams:/srv/TEAMS
net: /run/netns/fic-admin net: /run/netns/fic-admin
@ -278,10 +269,7 @@ services:
binds: binds:
- /etc/hosts:/etc/hosts:ro - /etc/hosts:/etc/hosts:ro
- /var/lib/fic/generator:/srv/GENERATOR:ro - /var/lib/fic/generator:/srv/GENERATOR:ro
# Uncomment this to disallow registrations
- /var/lib/fic/teams:/srv/TEAMS:ro - /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/secrets/mysql_password:/run/secrets/mysql_password:ro
- /var/lib/fic/settingsdist:/srv/SETTINGSDIST:ro - /var/lib/fic/settingsdist:/srv/SETTINGSDIST:ro
- /var/lib/fic/submissions:/srv/submissions - /var/lib/fic/submissions:/srv/submissions
@ -373,6 +361,7 @@ services:
- /var/lib/fic/files - /var/lib/fic/files
- /var/lib/fic/pki/shared - /var/lib/fic/pki/shared
- /var/lib/fic/settingsdist - /var/lib/fic/settingsdist
- /var/lib/fic/ssh
- /var/lib/fic/submissions - /var/lib/fic/submissions
- /var/lib/fic/teams - /var/lib/fic/teams
- /var/log/frontend - /var/log/frontend

View file

@ -1,6 +1,6 @@
kernel: kernel:
#image: nemunaire/kernel:5.10.62-0b705d955f5e283f62583c4e227d64a7924c138f-amd64 #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" cmdline: "console=ttyS0 console=tty0"
init: init:

View file

@ -1,50 +1,50 @@
kernel: kernel:
#image: nemunaire/kernel:5.10.62-0b705d955f5e283f62583c4e227d64a7924c138f-amd64 #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" cmdline: "console=ttyS0 console=tty0"
init: init:
- linuxkit/init:8eea386739975a43af558eec757a7dcb3a3d2e7b - linuxkit/init:v1.0.0
- linuxkit/runc:667e7ea2c426a2460ca21e3da065a57dbb3369c9 - linuxkit/runc:6062483d748609d505f2bcde4e52ee64a3329f5f
- linuxkit/containerd:a988a1a8bcbacc2c0390ca0c08f949e2b4b5915d - linuxkit/containerd:v1.0.0
- linuxkit/ca-certificates:7b32a26ca9c275d3ef32b11fe2a83dbd2aee2fdb - linuxkit/ca-certificates:v1.0.0
- linuxkit/getty:05eca453695984a69617f1f1f0bcdae7f7032967 - linuxkit/getty:v1.0.0
- nemunaire/mdadm:04814350d71ba9417e1f861be1685de26adf7a67 - nemunaire/mdadm:04814350d71ba9417e1f861be1685de26adf7a67
- nemunaire/kexec:839b4eedfce02a56c581dec2383dc6faff120855 - nemunaire/kexec:839b4eedfce02a56c581dec2383dc6faff120855
- nemunaire/fic-frontend-ui:latest - nemunaire/fic-frontend-ui:latest
onboot: onboot:
- name: mod - 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"] command: ["/bin/sh", "-c", "modprobe xhci_pci ahci intel_lpss_pci i2c_i801 megaraid_sas tg3 bnxt_en"]
- name: sysctl - name: sysctl
image: linuxkit/sysctl:5f56434b81004b50b47ed629b222619168c2bcdf image: linuxkit/sysctl:v1.0.0
# Metadata # Metadata
- name: metadata - name: metadata
image: linuxkit/metadata:4f81c0c3a2b245567fd7d32d799018c9614a9907 image: linuxkit/metadata:v1.0.0
command: ["/usr/bin/metadata", "-v", "cdrom"] command: ["/usr/bin/metadata", "-v", "cdrom"]
# Filesystem # Filesystem
- name: swap - name: swap
image: linuxkit/swap:f4b8ffef87c8c72165bd8a92b790ac252ccf1821 image: linuxkit/swap:v1.0.0
command: ["/sbin/swapon", "/dev/sda3"] command: ["/sbin/swapon", "/dev/sda3"]
- name: dm-crypt - name: dm-crypt
image: linuxkit/dm-crypt:981fde241bb84616a5ba94c04cdefa1489431a25 image: linuxkit/dm-crypt:d49723bc9d10c5ada9e03b0670f4e57416d5d084
command: ["/usr/bin/crypto", "-l", "crypt_fic", "/dev/sda4"] command: ["/usr/bin/crypto", "-l", "crypt_fic", "/dev/sda4"]
binds: binds:
- /dev:/dev - /dev:/dev
- /run/config/dm-crypt:/etc/dm-crypt - /run/config/dm-crypt:/etc/dm-crypt
- name: mount - name: mount
image: linuxkit/mount:cb8caa72248f7082fc2074ce843d53cdc15df04a image: linuxkit/mount:v1.0.0
command: ["/usr/bin/mountie", "-device", "/dev/mapper/crypt_fic", "/var/lib/fic" ] command: ["/usr/bin/mountie", "-device", "/dev/mapper/crypt_fic", "/var/lib/fic" ]
# Network # Network
# - name: ntp # - name: ntp
# image: linuxkit/openntpd:f99c4117763480815553b72022b426639a13ce86 # image: linuxkit/openntpd:v1.0.0
- name: nginx-ip-setup - 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;" ] command: ["/bin/sh", "-c", "ip a add 172.17.1.2/24 dev vethin-nginx; ip link set vethin-nginx up;" ]
net: new net: new
runtime: runtime:
@ -55,7 +55,7 @@ onboot:
bindNS: bindNS:
net: /run/netns/nginx net: /run/netns/nginx
- name: frontal-ip-setup # without bonding - 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;" ] 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 net: /run/netns/nginx
binds: binds:
@ -67,7 +67,7 @@ onboot:
- name: eth3 - name: eth3
# - name: eth4 # - name: eth4
# - name: frontal-ip-setup # with bonding # - 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;" ] # 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 # net: /run/netns/nginx
# binds: # binds:
@ -81,7 +81,7 @@ onboot:
# - name: bond-frontal # - name: bond-frontal
# add: bond # add: bond
- name: receiver-ip-setup - 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;" ] command: ["/bin/sh", "-c", "ip a add 172.17.1.3/24 dev vethin-receiver; ip link set vethin-receiver up;" ]
net: new net: new
runtime: runtime:
@ -92,7 +92,7 @@ onboot:
bindNS: bindNS:
net: /run/netns/fic-receiver net: /run/netns/fic-receiver
- name: sshd-ip-setup - 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;" ] command: ["/bin/sh", "-c", "ip a add 10.10.10.2/29 dev eth2; ip link set eth2 up;" ]
net: new net: new
runtime: runtime:
@ -101,7 +101,7 @@ onboot:
bindNS: bindNS:
net: /run/netns/sshd net: /run/netns/sshd
- name: auth-ip-setup - 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;" ] command: ["/bin/sh", "-c", "ip a add 172.17.1.4/24 dev vethin-auth; ip link set vethin-auth up;" ]
net: new net: new
runtime: runtime:
@ -112,7 +112,7 @@ onboot:
bindNS: bindNS:
net: /run/netns/auth net: /run/netns/auth
- name: bridge-setup - 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;" ] 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: runtime:
interfaces: interfaces:
@ -120,7 +120,7 @@ onboot:
add: bridge add: bridge
- name: firewall-frontal - 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;" ] 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: binds:
- /etc/iptables/rules-frontal.v4:/etc/iptables/rules-frontal.v4:ro - /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 - /run/config/remote_sync/:/run/config/remote_sync/:ro
net: /run/netns/nginx net: /run/netns/nginx
- name: firewall-sshd - 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" ] command: ["/bin/bash", "-c", "/sbin/iptables-restore < /etc/iptables/rules-sshd.v4; /sbin/ip6tables-restore < /etc/iptables/rules.v6" ]
binds: binds:
- /etc/iptables/rules-sshd.v4:/etc/iptables/rules-sshd.v4:ro - /etc/iptables/rules-sshd.v4:/etc/iptables/rules-sshd.v4:ro
- /etc/iptables/rules.v6:/etc/iptables/rules.v6:ro - /etc/iptables/rules.v6:/etc/iptables/rules.v6:ro
net: /run/netns/sshd 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: services:
# - name: getty # - name: getty
# image: linuxkit/getty:05eca453695984a69617f1f1f0bcdae7f7032967 # image: linuxkit/getty:v1.0.0
# env: # env:
# - INSECURE=true # - INSECURE=true
# Enable acpi to shutdown on power events # Enable acpi to shutdown on power events
- name: acpid - name: acpid
image: linuxkit/acpid:6cb5575e487a8fcbd4c3eb6721c23299e6ea452f image: linuxkit/acpid:v1.0.0
- name: rngd - name: rngd
image: linuxkit/rngd:1a18f2149e42a0a1cb9e7d37608a494342c26032 image: linuxkit/rngd:v1.0.0
- name: dhcpcd - name: dhcpcd
image: linuxkit/dhcpcd:157df9ef45a035f1542ec2270e374f18efef98a5 image: linuxkit/dhcpcd:v1.0.0
net: /run/netns/nginx net: /run/netns/nginx
binds: binds:
- /etc/dhcpcd.conf:/dhcpcd.conf:ro - /etc/dhcpcd.conf:/dhcpcd.conf:ro
@ -266,6 +257,7 @@ services:
- /var/lib/fic/files - /var/lib/fic/files
- /var/lib/fic/pki - /var/lib/fic/pki
- /var/lib/fic/settingsdist - /var/lib/fic/settingsdist
- /var/lib/fic/ssh
- /var/lib/fic/submissions - /var/lib/fic/submissions
- /var/lib/fic/teams - /var/lib/fic/teams
@ -288,7 +280,7 @@ services:
# net: /run/netns/nginx # net: /run/netns/nginx
- name: dexidp - name: dexidp
image: ghcr.io/dexidp/dex:v2.42.0 image: ghcr.io/dexidp/dex:v2.39.0
net: /run/netns/auth net: /run/netns/auth
binds: binds:
- /etc/hosts:/etc/hosts:ro - /etc/hosts:/etc/hosts:ro
@ -302,7 +294,7 @@ services:
mkdir: mkdir:
- /var/lib/fic/dex - /var/lib/fic/dex
- name: vouch-proxy - name: vouch-proxy
image: quay.io/vouch/vouch-proxy:alpine-0.41 image: quay.io/vouch/vouch-proxy:alpine-0.39
env: env:
- VOUCH_CONFIG=/etc/vouch/config.yml - VOUCH_CONFIG=/etc/vouch/config.yml
net: /run/netns/auth net: /run/netns/auth

View file

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

View file

@ -1,12 +1,12 @@
kernel: kernel:
#image: nemunaire/kernel:5.10.62-0b705d955f5e283f62583c4e227d64a7924c138f-amd64 #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" cmdline: "console=ttyS0 console=tty0"
init: init:
- nemunaire/mdadm:04814350d71ba9417e1f861be1685de26adf7a67 - nemunaire/mdadm:04814350d71ba9417e1f861be1685de26adf7a67
- linuxkit/metadata:4f81c0c3a2b245567fd7d32d799018c9614a9907 - linuxkit/metadata:v1.0.0
- alpine:latest - 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/adapter-static": "^3.0.0",
"@sveltejs/kit": "^2.0.0", "@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0",
"@sveltestrap/sveltestrap": "^7.0.0", "@sveltestrap/sveltestrap": "^6.2.1",
"eslint": "^9.0.0", "eslint": "^8.4.2",
"eslint-config-prettier": "^10.0.0", "eslint-config-prettier": "^9.0.0",
"eslint-plugin-svelte": "^3.0.0", "eslint-plugin-svelte": "^2.35.1",
"prettier": "^3.0.0", "prettier": "^3.0.0",
"prettier-plugin-svelte": "^3.1.2", "prettier-plugin-svelte": "^3.1.2",
"sass": "^1.51.0", "sass": "^1.51.0",
"sass-loader": "^16.0.0", "sass-loader": "^14.0.0",
"svelte": "^4.0.0", "svelte": "^4.0.0",
"vite": "^5.0.0" "vite": "^5.0.0"
}, },

View file

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

View file

@ -9,7 +9,6 @@
ListGroupItem, ListGroupItem,
} from '@sveltestrap/sveltestrap'; } from '@sveltestrap/sveltestrap';
import { hasDownloaded } from '$lib/stores/downloaded.js';
import FileSize from './FileSize.svelte'; import FileSize from './FileSize.svelte';
export let files = []; export let files = [];
@ -28,15 +27,15 @@
</CardBody> </CardBody>
<ListGroup flush class="border-secondary"> <ListGroup flush class="border-secondary">
{#each files as file, index} {#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;})}> <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" class:text-info={!$hasDownloaded[file.path]}> <h1 class="me-3">
<Icon name="arrow-down-circle" /> <Icon name="arrow-down-circle" />
</h1> </h1>
<div style="min-width: 0"> <div style="min-width: 0">
<h4 class="fw-bold"><samp>{file.name}</samp></h4> <h4 class="fw-bold"><samp>{file.name}</samp></h4>
{#if file.disclaimer} {#if file.disclamer}
<div class="file-disclaimer text-warning"> <div class="file-disclamer text-warning">
{file.disclaimer} {file.disclamer}
</div> </div>
{/if} {/if}
<nobr> <nobr>
@ -62,10 +61,10 @@
{/if} {/if}
<style> <style>
.file-disclaimer { .file-disclamer {
display: none; display: none;
} }
:global(.list-group-item:hover .file-disclaimer) { :global(.list-group-item:hover .file-disclamer) {
display: block; display: block;
} }
</style> </style>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -10,7 +10,6 @@
import DateFormat from '$lib/components/DateFormat.svelte'; import DateFormat from '$lib/components/DateFormat.svelte';
import { my } from '$lib/stores/my.js'; import { my } from '$lib/stores/my.js';
import { settings } from '$lib/stores/settings.js';
import { themes, exercices_idx } from '$lib/stores/themes.js'; import { themes, exercices_idx } from '$lib/stores/themes.js';
let req = null; let req = null;
@ -56,10 +55,6 @@
{:else if row.reason == "Display choices"} {:else if row.reason == "Display choices"}
<Badge color="secondary"><Icon name="info-square" /></Badge> <Badge color="secondary"><Icon name="info-square" /></Badge>
Échange champ de texte contre liste de choix É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} {:else}
<Badge color="primary"><Icon name="question" /></Badge> <Badge color="primary"><Icon name="question" /></Badge>
{row.reason} {row.reason}
@ -71,16 +66,14 @@
{/if} {/if}
</Column> </Column>
<Column header="Détail"> <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>
<Column header="Points"> <Column header="Points">
{Math.trunc(10*row.points * row.coeff)/10} {Math.trunc(10*row.points * row.coeff)/10}
</Column> </Column>
</Table> </Table>
{:else} {:else}
<CardBody> Vous n'avez fait aucune action vous faisant gagner ou perdre des points.
Vous n'avez fait aucune action vous faisant gagner ou perdre des points.
</CardBody>
{/if} {/if}
<button class="btn btn-primary" on:click={refresh_scores}> <button class="btn btn-primary" on:click={refresh_scores}>
<Icon name="arrow-clockwise" /> <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) { } else if (res_my.status === 404) {
update((m) => (null)); 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 key in $themesStore) {
for (const exercice of $themesStore[key].exercices) { for (const exercice of $themesStore[key].exercices) {
exercice.tags.forEach((tag) => { exercice.tags.forEach((tag) => {
tag = tag.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
if (!tags[tag]) if (!tags[tag])
tags[tag] = {count: 1, solved: 0}; tags[tag] = {count: 1, solved: 0};
else else

View file

@ -153,7 +153,6 @@ export const time = readable({}, function start(set) {
_settings = settings; _settings = settings;
}); });
set(updateTime(_settings));
const interval = setInterval(() => { const interval = setInterval(() => {
set(updateTime(_settings)); set(updateTime(_settings));
}, 1000); }, 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 "bootstrap-icons/font/bootstrap-icons.css";
import { base } from '$app/paths'; import { base } from '$app/paths';
import { page } from '$app/stores';
import { import {
Alert,
Container, Container,
//Styles, //Styles,
} from '@sveltestrap/sveltestrap'; } from '@sveltestrap/sveltestrap';
@ -13,7 +11,6 @@
import Header from '$lib/components/Header.svelte'; import Header from '$lib/components/Header.svelte';
import { challengeInfo } from '$lib/stores/challengeinfo'; import { challengeInfo } from '$lib/stores/challengeinfo';
import { my } from '$lib/stores/my.js';
import { settings } from '$lib/stores/settings'; import { settings } from '$lib/stores/settings';
</script> </script>
@ -35,21 +32,6 @@
</div> </div>
{/if} {/if}
<Header /> <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> <slot></slot>
<style> <style>

View file

@ -26,7 +26,7 @@
document.body.style.backgroundColor = ""; document.body.style.backgroundColor = "";
let items = []; let items = [];
function refresh_items() { $: {
const tmpitems = []; const tmpitems = [];
for (const th of $themes) { for (const th of $themes) {
if (th.id == 0) continue; if (th.id == 0) continue;
@ -35,54 +35,54 @@
} }
if ($themesStore["0"] && !$themesStore["0"].locked && $themesStore["0"].exercices) { if ($themesStore["0"] && !$themesStore["0"].locked && $themesStore["0"].exercices) {
if (tmpitems.length) { let nb_ex_max = tmpitems.length;
let nb_ex_max = tmpitems.length; let i = 1;
let i = 1; let j = 0;
let j = 0; for (j = $themesStore["0"].exercices.length - 1; j >= 0 && i < tmpitems.length; j--) {
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]) {
if ($my && $my.team_id && !$my.exercices[$themesStore["0"].exercices[j].id]) { // Only apply after start
// Only apply after start if (!($time.startIn && j < nb_ex_max && j < $settings.unlockedStandaloneExercices))
if (!($time.startIn && j < nb_ex_max && j < $settings.unlockedStandaloneExercices))
continue;
} else if ($my && !$my.team_id && j >= nb_ex_max) {
continue; continue;
}
tmpitems.splice(i, 0, {id: tmpitems.length, theme: $themesStore["0"], exercice: $themesStore["0"].exercices[j]});
i += 2;
} }
if (j >= 0 || i == 1) { tmpitems.splice(i, 0, {id: tmpitems.length, theme: $themesStore["0"], exercice: $themesStore["0"].exercices[j]});
tmpitems.push({ i += 2;
id: tmpitems.length, }
theme: {
...$themesStore["0"], if (j >= 0 || i == 1) {
name: "Voir les autres défis", tmpitems.push({
headline: "Il y a " + ($themesStore["0"].exercices.length) + " défis à découvrir&nbsp;! Cliquez ici pour les afficher.", id: tmpitems.length,
locked: $themesStore["0"].locked || i == 1, theme: {
}, ...$themesStore["0"],
color: "light", 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,
} else { },
for (const j in $themesStore["0"].exercices) { color: "light",
tmpitems.push({id: tmpitems.length, theme: $themesStore["0"], exercice: $themesStore["0"].exercices[j]}); });
}
} }
} }
items = tmpitems; items = tmpitems;
} }
$: refresh_items($themes);
</script> </script>
<Container class="mt-3 mb-5"> <Container class="mt-3 mb-5">
{#if $my} {#if !$my}
{#if !($my.team_id)} {#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}> <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;! <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> </Alert>
{:else if $teams[$my.team_id]} {:else if $teams[$my.team_id]}
<Alert color="info" class="text-justify" fade={false}> <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>. <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} {#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. <strong>Les membres de votre équipe ne sont pas encore enregistrés.</strong> Passez voir l'équipe serveur pour corriger cela.
</Alert> </Alert>
{/if} {/if}
{/if}
{/if} {/if}
<Masonry <Masonry
{items} {items}
let:item let:item

View file

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

View file

@ -7,7 +7,6 @@
Container, Container,
Icon, Icon,
Row, Row,
Spinner,
} from '@sveltestrap/sveltestrap'; } from '@sveltestrap/sveltestrap';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
@ -24,10 +23,8 @@
let partJ = false; let partJ = false;
let messageClass; let messageClass;
let message; let message;
let registrationInProgress = false;
function gotoHomeOnDiff(i) { function gotoHomeOnDiff(i) {
registrationInProgress = true;
my.refresh((my) => { my.refresh((my) => {
if (my && my.team_id) { if (my && my.team_id) {
themesStore.refresh(() => { themesStore.refresh(() => {
@ -35,10 +32,6 @@
}); });
} else if (i > 0) { } else if (i > 0) {
setTimeout(gotoHomeOnDiff, 650, i-1); 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> <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. Veuillez vous signaler auprès de notre équipe afin de corriger ce problème.
</Alert> </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} {:else}
{#if !$settings.denyTeamCreation && !partJ} {#if !$settings.denyTeamCreation && !partJ}
<Card body class="niceborder my-3 text-white"> <Card body class="niceborder my-3">
<p> <p>
Votre équipe n'est pas encore enregistrée sur notre serveur. Afin de Votre équipe n'est pas encore enregistrée sur notre serveur. Afin de
pouvoir participer au challenge, nous vous remercions de bien vouloir pouvoir participer au challenge, nous vous remercions de bien vouloir
@ -129,7 +118,7 @@
</Card> </Card>
{/if} {/if}
{#if $settings.canJoinTeam && !partR} {#if $settings.canJoinTeam && !partR}
<Card body class="niceborder my-3 text-white"> <Card body class="niceborder my-3">
<p> <p>
{#if !$settings.denyTeamCreation} {#if !$settings.denyTeamCreation}
Si votre équipe est déjà créée, rejoignez-là&nbsp;! 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-group text-justify mb-5">
<div class="card niceborder"> <div class="card niceborder">
<div class="card-body text-indent text-white"> <div class="card-body text-indent">
<h2>Débloquage des challenges</h2> <h2>Débloquage des challenges</h2>
<p> <p>
Au début, seul le premier défi de chaque scénario est Au début, seul le premier défi de chaque scénario est
@ -31,7 +31,7 @@
{#if $settings.unlockedStandaloneExercicesByThemeStepValidation > 0 || $settings.unlockedStandaloneExercicesByStandaloneExerciceValidation > 0} {#if $settings.unlockedStandaloneExercicesByThemeStepValidation > 0 || $settings.unlockedStandaloneExercicesByStandaloneExerciceValidation > 0}
<p> <p>
Vous avez également accès à {$settings.unlockedStandaloneExercices} défis indépendants. 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.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} {#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> </p>
@ -55,21 +55,6 @@
proposés. Plus le challenge est compliqué, plus il rapporte de points. proposés. Plus le challenge est compliqué, plus il rapporte de points.
</p> </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> <h3>Coût des tentatives</h3>
<p> <p>
Vous disposez de 10&nbsp;tentatives pour trouver la/les solutions d'un Vous disposez de 10&nbsp;tentatives pour trouver la/les solutions d'un
@ -129,21 +114,20 @@
parmi ce nombre de tentatives. parmi ce nombre de tentatives.
</p> </p>
{/if} {/if}
{/if}
</div> </div>
</div> </div>
<div class="card niceborder"> <div class="card niceborder">
<div class="card-body text-indent text-white"> <div class="card-body text-indent">
{#if $settings.discountedFactor > 0} {#if $settings.discountedFactor > 0}
<h3>Décote des gains</h3> <h3>Décote des gains</h3>
<p> <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>
<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>
<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>
<p> <p>
Ainsi, pour un exercice d'une valeur initiale de {10*$settings.globalScoreCoefficient}&nbsp;points&nbsp;: Ainsi, pour un exercice d'une valeur initiale de {10*$settings.globalScoreCoefficient}&nbsp;points&nbsp;:
@ -162,19 +146,19 @@
</tr> </tr>
<tr> <tr>
<td>2</td> <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>
<tr> <tr>
<td>5</td> <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>
<tr> <tr>
<td>10</td> <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>
<tr> <tr>
<td>20</td> <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>
<tr> <tr>
<td>...</td> <td>...</td>
@ -208,12 +192,10 @@
défi. défi.
</p> </p>
{#if $settings.firstBlood} <h4>Prem's</h4>
<h4>Prem's</h4> <p>
<p> Un bonus de +{$settings.firstBlood * 100}&nbsp;% est attribué à la première équipe qui résout un défi.
Un bonus de +{$settings.firstBlood * 100}&nbsp;&percnt; est attribué à la première équipe qui résout un défi. </p>
</p>
{/if}
<h4>Bonus temporaires <small><Icon name="gift" aria-hidden="true" title="Des <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> 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