Compare commits
2 commits
master
...
f/grpc-pas
Author | SHA1 | Date | |
---|---|---|---|
9c50bc37d9 | |||
e23377329a |
165 changed files with 5547 additions and 9177 deletions
60
.drone.yml
60
.drone.yml
|
@ -20,7 +20,7 @@ steps:
|
|||
- mkdir deploy
|
||||
|
||||
- name: build qa ui
|
||||
image: node:23-alpine
|
||||
image: node:21-alpine
|
||||
commands:
|
||||
- cd qa/ui
|
||||
- npm install --network-timeout=100000
|
||||
|
@ -31,15 +31,15 @@ steps:
|
|||
image: golang:alpine
|
||||
commands:
|
||||
- apk --no-cache add build-base
|
||||
- go vet -buildvcs=false -tags gitgo ./...
|
||||
- go vet -buildvcs=false ./...
|
||||
- go vet -v -buildvcs=false -tags gitgo ./...
|
||||
- go vet -v -buildvcs=false ./...
|
||||
- go test ./...
|
||||
|
||||
- name: build admin
|
||||
image: golang:alpine
|
||||
commands:
|
||||
- go build -buildvcs=false -tags gitgo -o deploy/admin-gitgo-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/admin
|
||||
- go build -buildvcs=false -o deploy/admin-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/admin
|
||||
- go build -v -buildvcs=false -tags gitgo -o deploy/admin-gitgo-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/admin
|
||||
- go build -v -buildvcs=false -o deploy/admin-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/admin
|
||||
- tar chjf deploy/htdocs-admin.tar.bz2 htdocs-admin
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
|
@ -51,7 +51,7 @@ steps:
|
|||
- name: build checker
|
||||
image: golang:alpine
|
||||
commands:
|
||||
- go build -buildvcs=false -o deploy/checker-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/checker
|
||||
- go build -v -buildvcs=false -o deploy/checker-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/checker
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
when:
|
||||
|
@ -62,7 +62,7 @@ steps:
|
|||
- name: build evdist
|
||||
image: golang:alpine
|
||||
commands:
|
||||
- go build -buildvcs=false -o deploy/evdist-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/evdist
|
||||
- go build -v -buildvcs=false -o deploy/evdist-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/evdist
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
when:
|
||||
|
@ -73,7 +73,7 @@ steps:
|
|||
- name: build generator
|
||||
image: golang:alpine
|
||||
commands:
|
||||
- go build -buildvcs=false -o deploy/generator-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/generator
|
||||
- go build -v -buildvcs=false -o deploy/generator-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/generator
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
when:
|
||||
|
@ -84,7 +84,7 @@ steps:
|
|||
- name: build receiver
|
||||
image: golang:alpine
|
||||
commands:
|
||||
- go build -buildvcs=false -o deploy/receiver-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/receiver
|
||||
- go build -v -buildvcs=false -o deploy/receiver-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/receiver
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
when:
|
||||
|
@ -93,7 +93,7 @@ steps:
|
|||
- master
|
||||
|
||||
- name: build frontend fic ui
|
||||
image: node:23-alpine
|
||||
image: node:21-alpine
|
||||
commands:
|
||||
- cd frontend/fic
|
||||
- npm install --network-timeout=100000
|
||||
|
@ -107,7 +107,7 @@ steps:
|
|||
- name: build dashboard
|
||||
image: golang:alpine
|
||||
commands:
|
||||
- go build -buildvcs=false -o deploy/dashboard-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/dashboard
|
||||
- go build -v -buildvcs=false -o deploy/dashboard-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/dashboard
|
||||
- tar chjf deploy/htdocs-dashboard.tar.bz2 htdocs-dashboard
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
|
@ -120,12 +120,12 @@ steps:
|
|||
image: golang:alpine
|
||||
commands:
|
||||
- apk --no-cache add build-base
|
||||
- go build -buildvcs=false --tags checkupdate -o deploy/repochecker-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/repochecker
|
||||
- go build -buildvcs=false -buildmode=plugin -o deploy/repochecker-epita-rules-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}.so srs.epita.fr/fic-server/repochecker/epita
|
||||
- go build -buildvcs=false -buildmode=plugin -o deploy/repochecker-file-inspector-rules-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}.so srs.epita.fr/fic-server/repochecker/file-inspector
|
||||
- go build -buildvcs=false -buildmode=plugin -o deploy/repochecker-grammalecte-rules-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}.so srs.epita.fr/fic-server/repochecker/grammalecte
|
||||
- go build -buildvcs=false -buildmode=plugin -o deploy/repochecker-pcap-inspector-rules-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}.so srs.epita.fr/fic-server/repochecker/pcap-inspector
|
||||
- go build -buildvcs=false -buildmode=plugin -o deploy/repochecker-videos-rules-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}.so srs.epita.fr/fic-server/repochecker/videos
|
||||
- go build -buildvcs=false --tags checkupdate -v -o deploy/repochecker-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/repochecker
|
||||
- go build -buildvcs=false -buildmode=plugin -v -o deploy/repochecker-epita-rules-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}.so srs.epita.fr/fic-server/repochecker/epita
|
||||
- go build -buildvcs=false -buildmode=plugin -v -o deploy/repochecker-file-inspector-rules-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}.so srs.epita.fr/fic-server/repochecker/file-inspector
|
||||
- go build -buildvcs=false -buildmode=plugin -v -o deploy/repochecker-grammalecte-rules-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}.so srs.epita.fr/fic-server/repochecker/grammalecte
|
||||
- go build -buildvcs=false -buildmode=plugin -v -o deploy/repochecker-pcap-inspector-rules-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}.so srs.epita.fr/fic-server/repochecker/pcap-inspector
|
||||
- go build -buildvcs=false -buildmode=plugin -v -o deploy/repochecker-videos-rules-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}.so srs.epita.fr/fic-server/repochecker/videos
|
||||
- grep "const version" repochecker/update.go | sed -r 's/^.*=\s*(\S.*)$/\1/' > deploy/repochecker.version
|
||||
when:
|
||||
branch:
|
||||
|
@ -135,7 +135,7 @@ steps:
|
|||
- name: build qa
|
||||
image: golang:alpine
|
||||
commands:
|
||||
- go build -buildvcs=false -o deploy/qa-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/qa
|
||||
- go build -v -buildvcs=false -o deploy/qa-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/qa
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
when:
|
||||
|
@ -378,14 +378,14 @@ steps:
|
|||
image: golang:alpine
|
||||
commands:
|
||||
- apk --no-cache add git
|
||||
- go get -d ./...
|
||||
- go get -v -d ./...
|
||||
- mkdir deploy
|
||||
|
||||
- name: build admin
|
||||
image: golang:alpine
|
||||
commands:
|
||||
- apk --no-cache add build-base
|
||||
- go build -buildvcs=false -o deploy/admin-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/admin
|
||||
- go build -v -buildvcs=false -o deploy/admin-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/admin
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
when:
|
||||
|
@ -396,7 +396,7 @@ steps:
|
|||
- name: build checker
|
||||
image: golang:alpine
|
||||
commands:
|
||||
- go build -buildvcs=false -o deploy/checker-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/checker
|
||||
- go build -v -buildvcs=false -o deploy/checker-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/checker
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
when:
|
||||
|
@ -407,7 +407,7 @@ steps:
|
|||
- name: build evdist
|
||||
image: golang:alpine
|
||||
commands:
|
||||
- go build -buildvcs=false -o deploy/evdist-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/evdist
|
||||
- go build -v -buildvcs=false -o deploy/evdist-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/evdist
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
when:
|
||||
|
@ -418,7 +418,7 @@ steps:
|
|||
- name: build generator
|
||||
image: golang:alpine
|
||||
commands:
|
||||
- go build -buildvcs=false -o deploy/generator-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/generator
|
||||
- go build -v -buildvcs=false -o deploy/generator-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/generator
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
when:
|
||||
|
@ -429,7 +429,7 @@ steps:
|
|||
- name: build receiver
|
||||
image: golang:alpine
|
||||
commands:
|
||||
- go build -buildvcs=false -o deploy/receiver-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/receiver
|
||||
- go build -v -buildvcs=false -o deploy/receiver-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/receiver
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
when:
|
||||
|
@ -438,7 +438,7 @@ steps:
|
|||
- master
|
||||
|
||||
- name: build frontend fic ui
|
||||
image: node:23-alpine
|
||||
image: node:21-alpine
|
||||
commands:
|
||||
- cd frontend/fic
|
||||
- npm install --network-timeout=100000
|
||||
|
@ -451,7 +451,7 @@ steps:
|
|||
- name: build dashboard
|
||||
image: golang:alpine
|
||||
commands:
|
||||
- go build -buildvcs=false -o deploy/dashboard-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/dashboard
|
||||
- go build -v -buildvcs=false -o deploy/dashboard-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/dashboard
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
when:
|
||||
|
@ -463,7 +463,7 @@ steps:
|
|||
image: golang:alpine
|
||||
commands:
|
||||
- apk --no-cache add build-base
|
||||
- go build -buildvcs=false --tags checkupdate -o deploy/repochecker-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/repochecker
|
||||
- go build -buildvcs=false --tags checkupdate -v -o deploy/repochecker-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/repochecker
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
when:
|
||||
|
@ -475,7 +475,7 @@ steps:
|
|||
image: golang:alpine
|
||||
commands:
|
||||
- apk --no-cache add build-base
|
||||
- go build -buildvcs=false --tags checkupdate -o deploy/repochecker-darwin-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/repochecker
|
||||
- go build -buildvcs=false --tags checkupdate -v -o deploy/repochecker-darwin-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/repochecker
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
GOOS: darwin
|
||||
|
@ -486,7 +486,7 @@ steps:
|
|||
- master
|
||||
|
||||
- name: build qa ui
|
||||
image: node:23-alpine
|
||||
image: node:21-alpine
|
||||
commands:
|
||||
- cd qa/ui
|
||||
- npm install --network-timeout=100000
|
||||
|
@ -500,7 +500,7 @@ steps:
|
|||
- name: build qa
|
||||
image: golang:alpine
|
||||
commands:
|
||||
- go build -buildvcs=false -o deploy/qa-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/qa
|
||||
- go build -v -buildvcs=false -o deploy/qa-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/qa
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
when:
|
||||
|
|
|
@ -62,7 +62,7 @@ dependency_scanning:
|
|||
|
||||
get-deps:
|
||||
stage: deps
|
||||
image: golang:1-alpine
|
||||
image: golang:alpine3.18
|
||||
before_script:
|
||||
- export GOPATH="$CI_PROJECT_DIR/.go"
|
||||
- mkdir -p .go
|
||||
|
@ -75,7 +75,7 @@ vet:
|
|||
needs: ["build-qa-ui"]
|
||||
dependencies:
|
||||
- build-qa-ui
|
||||
image: golang:1-alpine
|
||||
image: golang:alpine3.18
|
||||
before_script:
|
||||
- export GOPATH="$CI_PROJECT_DIR/.go"
|
||||
- mkdir -p .go
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM golang:1-alpine AS gobuild
|
||||
FROM golang:1-alpine as gobuild
|
||||
|
||||
RUN apk add --no-cache git
|
||||
|
||||
|
@ -20,7 +20,7 @@ RUN go get -d -v ./admin && \
|
|||
go build -v -buildmode=plugin -o repochecker/videos-rules.so ./repochecker/videos
|
||||
|
||||
|
||||
FROM alpine:3.21
|
||||
FROM alpine:3.19
|
||||
|
||||
RUN apk add --no-cache \
|
||||
ca-certificates \
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM golang:1-alpine AS gobuild
|
||||
FROM golang:1-alpine as gobuild
|
||||
|
||||
RUN apk add --no-cache git
|
||||
|
||||
|
@ -13,7 +13,7 @@ RUN go get -d -v ./checker && \
|
|||
go build -v -buildvcs=false -o checker/checker ./checker
|
||||
|
||||
|
||||
FROM alpine:3.21
|
||||
FROM alpine:3.19
|
||||
|
||||
WORKDIR /srv
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM golang:1-alpine AS gobuild
|
||||
FROM golang:1-alpine as gobuild
|
||||
|
||||
RUN apk add --no-cache git
|
||||
|
||||
|
@ -13,7 +13,7 @@ RUN go get -d -v ./dashboard && \
|
|||
go build -v -buildvcs=false -o dashboard/dashboard ./dashboard
|
||||
|
||||
|
||||
FROM alpine:3.21
|
||||
FROM alpine:3.19
|
||||
|
||||
EXPOSE 8082
|
||||
|
||||
|
@ -27,6 +27,6 @@ COPY --from=gobuild /go/src/srs.epita.fr/fic-server/dashboard/dashboard /srv/das
|
|||
COPY dashboard/static/index.html /srv/htdocs-dashboard/
|
||||
COPY admin/static/css/bootstrap.min.css dashboard/static/css/fic.css admin/static/css/glyphicon.css /srv/htdocs-dashboard/css/
|
||||
COPY admin/static/fonts /srv/htdocs-dashboard/fonts
|
||||
COPY dashboard/static/img/srs.png /srv/htdocs-dashboard/img/
|
||||
COPY frontend/fic/static/img/ dashboard/static/img/srs.png /srv/htdocs-dashboard/img/
|
||||
COPY dashboard/static/js/dashboard.js admin/static/js/angular.min.js dashboard/static/js/angular-animate.min.js admin/static/js/angular-route.min.js admin/static/js/angular-sanitize.min.js admin/static/js/bootstrap.min.js admin/static/js/common.js admin/static/js/d3.v3.min.js admin/static/js/jquery.min.js /srv/htdocs-dashboard/js/
|
||||
COPY admin/static/js/i18n/* /srv/htdocs-dashboard/js/i18n/
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM alpine:3.21
|
||||
FROM alpine:3.19
|
||||
|
||||
EXPOSE 67/udp
|
||||
EXPOSE 69/udp
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM golang:1-alpine AS gobuild
|
||||
FROM golang:1-alpine as gobuild
|
||||
|
||||
RUN apk add --no-cache git
|
||||
|
||||
|
@ -12,7 +12,7 @@ RUN go get -d -v ./evdist && \
|
|||
go build -v -buildvcs=false -o evdist/evdist ./evdist
|
||||
|
||||
|
||||
FROM alpine:3.21
|
||||
FROM alpine:3.19
|
||||
|
||||
WORKDIR /srv
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM node:23-alpine AS nodebuild
|
||||
FROM node:21-alpine as nodebuild
|
||||
|
||||
WORKDIR /ui
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM golang:1-alpine AS gobuild
|
||||
FROM golang:1-alpine as gobuild
|
||||
|
||||
RUN apk add --no-cache git
|
||||
|
||||
|
@ -13,7 +13,7 @@ RUN go get -d -v ./generator && \
|
|||
go build -v -buildvcs=false -o generator/generator ./generator
|
||||
|
||||
|
||||
FROM alpine:3.21
|
||||
FROM alpine:3.19
|
||||
|
||||
WORKDIR /srv
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM golang:1-alpine AS gobuild
|
||||
FROM golang:1-alpine as gobuild
|
||||
|
||||
RUN apk add --no-cache git
|
||||
|
||||
|
@ -15,7 +15,7 @@ RUN go get -d -v ./admin && \
|
|||
go build -v -o get-remote-files ./admin/get-remote-files
|
||||
|
||||
|
||||
FROM alpine:3.21
|
||||
FROM alpine:3.19
|
||||
|
||||
RUN apk add --no-cache \
|
||||
ca-certificates
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM node:23-alpine AS nodebuild
|
||||
FROM node:21-alpine as nodebuild
|
||||
|
||||
WORKDIR /ui
|
||||
|
||||
|
@ -10,16 +10,9 @@ RUN npm install --network-timeout=100000 && \
|
|||
|
||||
FROM nginx:stable-alpine-slim
|
||||
|
||||
ENV FIC_BASEURL /
|
||||
ENV HOST_RECEIVER receiver:8080
|
||||
ENV HOST_ADMIN admin:8081
|
||||
ENV HOST_DASHBOARD dashboard:8082
|
||||
ENV HOST_QA qa:8083
|
||||
ENV PATH_FILES /srv/FILES
|
||||
ENV PATH_STARTINGBLOCK /srv/STARTINGBLOCK
|
||||
ENV PATH_STATIC /srv/htdocs-frontend
|
||||
ENV PATH_SETTINGS /srv/SETTINGSDIST
|
||||
ENV PATH_TEAMS /srv/TEAMS
|
||||
ENV FIC_BASEURL=/ \
|
||||
HOST_RECEIVER=receiver:8080 HOST_ADMIN=admin:8081 HOST_DASHBOARD=dashboard:8082 HOST_QA=qa:8083 \
|
||||
PATH_FILES=/srv/FILES PATH_STARTINGBLOCK=/srv/STARTINGBLOCK PATH_STATIC=/srv/htdocs-frontend PATH_SETTINGS=/srv/SETTINGSDIST PATH_TEAMS=/srv/TEAMS
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM node:23-alpine AS nodebuild
|
||||
FROM node:21-alpine as nodebuild
|
||||
|
||||
WORKDIR /ui
|
||||
|
||||
|
@ -8,7 +8,7 @@ RUN npm install --network-timeout=100000 && \
|
|||
npm run build
|
||||
|
||||
|
||||
FROM golang:1-alpine AS gobuild
|
||||
FROM golang:1-alpine as gobuild
|
||||
|
||||
RUN apk add --no-cache git
|
||||
|
||||
|
@ -25,7 +25,7 @@ RUN go get -d -v ./qa && \
|
|||
go build -v -buildvcs=false -o qa/qa ./qa
|
||||
|
||||
|
||||
FROM alpine:3.21
|
||||
FROM alpine:3.19
|
||||
|
||||
EXPOSE 8083
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM golang:1-alpine AS gobuild
|
||||
FROM golang:1-alpine as gobuild
|
||||
|
||||
RUN apk add --no-cache git
|
||||
|
||||
|
@ -13,7 +13,7 @@ RUN go get -d -v ./receiver && \
|
|||
go build -v -buildvcs=false -o ./receiver/receiver ./receiver
|
||||
|
||||
|
||||
FROM alpine:3.21
|
||||
FROM alpine:3.19
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM golang:1-alpine AS gobuild
|
||||
FROM golang:1-alpine as gobuild
|
||||
|
||||
RUN apk add --no-cache git
|
||||
|
||||
|
@ -13,7 +13,7 @@ RUN go get -d -v ./remote/challenge-sync-airbus && \
|
|||
go build -v -buildvcs=false -o ./challenge-sync-airbus ./remote/challenge-sync-airbus
|
||||
|
||||
|
||||
FROM alpine:3.21
|
||||
FROM alpine:3.19
|
||||
|
||||
RUN apk add --no-cache openssl ca-certificates
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM golang:1-alpine AS gobuild
|
||||
FROM golang:1-alpine as gobuild
|
||||
|
||||
RUN apk add --no-cache git
|
||||
|
||||
|
@ -13,7 +13,7 @@ RUN go get -d -v ./remote/scores-sync-zqds && \
|
|||
go build -v -buildvcs=false -o ./scores-sync-zqds ./remote/scores-sync-zqds
|
||||
|
||||
|
||||
FROM alpine:3.21
|
||||
FROM alpine:3.19
|
||||
|
||||
RUN apk add --no-cache openssl ca-certificates
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM golang:1-alpine AS gobuild
|
||||
FROM golang:1-alpine as gobuild
|
||||
|
||||
RUN apk add --no-cache git
|
||||
|
||||
|
@ -23,7 +23,7 @@ RUN go get -d -v ./repochecker && \
|
|||
|
||||
ENV GRAMMALECTE_VERSION 2.1.1
|
||||
|
||||
ADD https://web.archive.org/web/20240926154729if_/https://grammalecte.net/zip/Grammalecte-fr-v$GRAMMALECTE_VERSION.zip /srv/grammalecte.zip
|
||||
ADD https://grammalecte.net/zip/Grammalecte-fr-v$GRAMMALECTE_VERSION.zip /srv/grammalecte.zip
|
||||
|
||||
RUN mkdir /srv/grammalecte && cd /srv/grammalecte && unzip /srv/grammalecte.zip && sed -i 's/if sys.version_info.major < (3, 7):/if False:/' /srv/grammalecte/grammalecte-server.py
|
||||
|
||||
|
|
26
README.md
26
README.md
|
@ -6,32 +6,6 @@ to be robust, so it uses some uncommon technics like client certificate for
|
|||
authentication, lots of state of the art cryptographic methods and aims to be
|
||||
deployed in a DMZ network architecture.
|
||||
|
||||
## Features
|
||||
|
||||
- **Collaborative Challenge Design and Review:** Facilitates large team collaboration for challenge creation and review.
|
||||
- **Versatile Flag Formats:** Supports flags as strings, numbers, multiple-choice questions, unique-choice questions, selects, multiline inputs, and strings with capture regexp.
|
||||
- **Engaging Challenge Interface:** A visually appealing interface that incorporates images to illustrate exercises.
|
||||
- **Public Dashboard:** Allow spectators to follow the competition alongside players.
|
||||
- **Archival Mode:** Preserve past challenges and data in a static form, with no code. Your archive can lied on a S3 bucket.
|
||||
- **Export Capabilities:** Export challenges to other CTF platforms.
|
||||
- **Security-Focused:** Designed with security as a top priority. Each service aims to be isolated with right restrictions. Answers are not stored in the database, ...
|
||||
- **Choose your Authentication:** Authentication is not part of this project, integrate your own authentication methods.
|
||||
- **Extensible:** Easily extend and customize the platform. The main codebase in Golang is highly documented, each frontend part can be recreated in another language with ease.
|
||||
- **Comprehensive Settings:** A wide range of settings for challenge customization. You can have first blood or not, dynamic exercice gain, evenemential bonus, ...
|
||||
- **Git Integration:** Seamless verification and integration with Git.
|
||||
- **Infrastructure as Code (IaC):** Ensure read-only and reproducible infrastructure.
|
||||
- **Last-Minute Checks:** Ensure your challenge is ready with a comprehensive set of checks that can be performed anytime, verifying that downloadable files are as expected by the challenge creators.
|
||||
- **Lightweight:** Optimized for minimal resource consumption, supporting features like serving gzipped files directly to browsers without CPU usage.
|
||||
- **Scalable:** Designed to handle large-scale competitions with multiple receivers and frontend servers, smoothly queuing activity peaks on the backend.
|
||||
- **Offline Capability:** Run your challenges offline.
|
||||
- **Integrated Exercise Issue Ticketing System:** Manage and track issues related to exercises during the competition directly with teams. During designing phase, this transform in a complete dedicated QA platform.
|
||||
- **Detailed Statistics:** Provide administrators with insights into exercise preferences and complexity.
|
||||
- **Change Planning:** Schedule events in advance, such as new exercise availability or ephemeral bonuses, with second-by-second precision.
|
||||
- **Frontend Time Synchronization:** Ensure accurate remaining time and event synchronization between servers and players.
|
||||
|
||||
|
||||
## Overview
|
||||
|
||||
This is a [monorepo](https://danluu.com/monorepo/), containing several
|
||||
micro-services :
|
||||
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"path"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
@ -20,7 +18,6 @@ import (
|
|||
func declareGlobalExercicesRoutes(router *gin.RouterGroup) {
|
||||
router.GET("/resolutions.json", exportResolutionMovies)
|
||||
router.GET("/exercices_stats.json", getExercicesStats)
|
||||
router.GET("/exercices_forge_bindings.json", getExercicesForgeLinks)
|
||||
router.GET("/tags", listTags)
|
||||
}
|
||||
|
||||
|
@ -35,18 +32,9 @@ func declareExercicesRoutes(router *gin.RouterGroup) {
|
|||
apiExercicesRoutes.PATCH("", partUpdateExercice)
|
||||
apiExercicesRoutes.DELETE("", deleteExercice)
|
||||
|
||||
apiExercicesRoutes.POST("/diff-sync", APIDiffExerciceWithRemote)
|
||||
|
||||
apiExercicesRoutes.GET("/history.json", getExerciceHistory)
|
||||
|
||||
apiExercicesRoutes.GET("/stats.json", getExerciceStats)
|
||||
|
||||
apiExercicesRoutes.GET("/tries", listTries)
|
||||
|
||||
apiTriesRoutes := apiExercicesRoutes.Group("/tries/:trid")
|
||||
apiTriesRoutes.Use(ExerciceTryHandler)
|
||||
apiTriesRoutes.GET("", getExerciceTry)
|
||||
apiTriesRoutes.DELETE("", deleteExerciceTry)
|
||||
apiExercicesRoutes.GET("/history.json", getExerciceHistory)
|
||||
|
||||
apiHistoryRoutes := apiExercicesRoutes.Group("/history.json")
|
||||
apiHistoryRoutes.Use(AssigneeCookieHandler)
|
||||
|
@ -74,8 +62,6 @@ func declareExercicesRoutes(router *gin.RouterGroup) {
|
|||
apiFlagsRoutes.POST("/try", tryExerciceFlag)
|
||||
apiFlagsRoutes.DELETE("/", deleteExerciceFlag)
|
||||
apiFlagsRoutes.GET("/dependancies", showExerciceFlagDeps)
|
||||
apiFlagsRoutes.GET("/statistics", showExerciceFlagStats)
|
||||
apiFlagsRoutes.DELETE("/tries", deleteExerciceFlagTries)
|
||||
apiFlagsRoutes.GET("/choices/", listFlagChoices)
|
||||
apiFlagsChoicesRoutes := apiExercicesRoutes.Group("/choices/:cid")
|
||||
apiFlagsChoicesRoutes.Use(FlagChoiceHandler)
|
||||
|
@ -91,8 +77,6 @@ func declareExercicesRoutes(router *gin.RouterGroup) {
|
|||
apiQuizRoutes.PUT("", updateExerciceQuiz)
|
||||
apiQuizRoutes.DELETE("", deleteExerciceQuiz)
|
||||
apiQuizRoutes.GET("/dependancies", showExerciceQuizDeps)
|
||||
apiQuizRoutes.GET("/statistics", showExerciceQuizStats)
|
||||
apiQuizRoutes.DELETE("/tries", deleteExerciceQuizTries)
|
||||
|
||||
apiExercicesRoutes.GET("/tags", listExerciceTags)
|
||||
apiExercicesRoutes.POST("/tags", addExerciceTag)
|
||||
|
@ -103,8 +87,8 @@ func declareExercicesRoutes(router *gin.RouterGroup) {
|
|||
|
||||
// Remote
|
||||
router.GET("/remote/themes/:thid/exercices/:exid", sync.ApiGetRemoteExercice)
|
||||
router.GET("/remote/themes/:thid/exercices/:exid/flags", sync.ApiGetRemoteExerciceFlags)
|
||||
router.GET("/remote/themes/:thid/exercices/:exid/hints", sync.ApiGetRemoteExerciceHints)
|
||||
router.GET("/remote/themes/:thid/exercices/:exid/flags", sync.ApiGetRemoteExerciceFlags)
|
||||
}
|
||||
|
||||
type Exercice struct {
|
||||
|
@ -142,7 +126,7 @@ func ExerciceHandler(c *gin.Context) {
|
|||
|
||||
c.Set("theme", theme)
|
||||
} else {
|
||||
c.Set("theme", &fic.StandaloneExercicesTheme)
|
||||
c.Set("theme", &fic.Theme{Path: sync.StandaloneExercicesDirectory})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -488,71 +472,6 @@ func getExercicesStats(c *gin.Context) {
|
|||
c.JSON(http.StatusOK, ret)
|
||||
}
|
||||
|
||||
type themeForgeBinding struct {
|
||||
ThemeName string `json:"name"`
|
||||
ThemePath string `json:"path"`
|
||||
ForgeLink string `json:"forge_link"`
|
||||
Exercices []exerciceForgeBinding `json:"exercices"`
|
||||
}
|
||||
|
||||
type exerciceForgeBinding struct {
|
||||
ExerciceName string `json:"name"`
|
||||
ExercicePath string `json:"path"`
|
||||
ForgeLink string `json:"forge_link"`
|
||||
}
|
||||
|
||||
func getExercicesForgeLinks(c *gin.Context) {
|
||||
themes, err := fic.GetThemesExtended()
|
||||
if err != nil {
|
||||
log.Println("Unable to listThemes:", err.Error())
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during themes listing."})
|
||||
return
|
||||
}
|
||||
|
||||
fli, ok := sync.GlobalImporter.(sync.ForgeLinkedImporter)
|
||||
if !ok {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Current importer is not compatible with ForgeLinkedImporter"})
|
||||
return
|
||||
}
|
||||
|
||||
ret := []themeForgeBinding{}
|
||||
for _, theme := range themes {
|
||||
exercices, err := theme.GetExercices()
|
||||
if err != nil {
|
||||
log.Println("Unable to listExercices:", err.Error())
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during exercice listing."})
|
||||
return
|
||||
}
|
||||
|
||||
var exlinks []exerciceForgeBinding
|
||||
for _, exercice := range exercices {
|
||||
var forgelink string
|
||||
if u, _ := fli.GetExerciceLink(exercice); u != nil {
|
||||
forgelink = u.String()
|
||||
}
|
||||
|
||||
exlinks = append(exlinks, exerciceForgeBinding{
|
||||
ExerciceName: exercice.Title,
|
||||
ExercicePath: exercice.Path,
|
||||
ForgeLink: forgelink,
|
||||
})
|
||||
}
|
||||
|
||||
var forgelink string
|
||||
if u, _ := fli.GetThemeLink(theme); u != nil {
|
||||
forgelink = u.String()
|
||||
}
|
||||
ret = append(ret, themeForgeBinding{
|
||||
ThemeName: theme.Name,
|
||||
ThemePath: theme.Path,
|
||||
ForgeLink: forgelink,
|
||||
Exercices: exlinks,
|
||||
})
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, ret)
|
||||
}
|
||||
|
||||
func AssigneeCookieHandler(c *gin.Context) {
|
||||
myassignee, err := c.Cookie("myassignee")
|
||||
if err != nil {
|
||||
|
@ -933,60 +852,6 @@ func showExerciceFlagDeps(c *gin.Context) {
|
|||
c.JSON(http.StatusOK, deps)
|
||||
}
|
||||
|
||||
func showExerciceFlagStats(c *gin.Context) {
|
||||
exercice := c.MustGet("exercice").(*fic.Exercice)
|
||||
flag := c.MustGet("flag-key").(*fic.FlagKey)
|
||||
|
||||
history, err := exercice.GetHistory()
|
||||
if err != nil {
|
||||
log.Println("Unable to getExerciceHistory:", err.Error())
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when retrieving exercice history"})
|
||||
return
|
||||
}
|
||||
|
||||
var completed int64
|
||||
|
||||
for _, hline := range history {
|
||||
if hline["kind"].(string) == "flag_found" {
|
||||
if int(*hline["secondary"].(*int64)) == flag.Id {
|
||||
completed += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tries, err := flag.NbTries()
|
||||
if err != nil {
|
||||
log.Println("Unable to nbTries:", err.Error())
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when retrieving flag tries"})
|
||||
return
|
||||
}
|
||||
|
||||
teams, err := flag.TeamsOnIt()
|
||||
if err != nil {
|
||||
log.Println("Unable to teamsOnIt:", err.Error())
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when retrieving flag related teams"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"completed": completed,
|
||||
"tries": tries,
|
||||
"teams": teams,
|
||||
})
|
||||
}
|
||||
|
||||
func deleteExerciceFlagTries(c *gin.Context) {
|
||||
flag := c.MustGet("flag-key").(*fic.FlagKey)
|
||||
|
||||
err := flag.DeleteTries()
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.AbortWithStatusJSON(http.StatusOK, true)
|
||||
}
|
||||
|
||||
func tryExerciceFlag(c *gin.Context) {
|
||||
flag := c.MustGet("flag-key").(*fic.FlagKey)
|
||||
|
||||
|
@ -1030,23 +895,6 @@ func updateExerciceFlag(c *gin.Context) {
|
|||
flag.Help = uk.Help
|
||||
flag.IgnoreCase = uk.IgnoreCase
|
||||
flag.Multiline = uk.Multiline
|
||||
flag.ChoicesCost = uk.ChoicesCost
|
||||
flag.BonusGain = uk.BonusGain
|
||||
|
||||
if uk.CaptureRe != nil && len(*uk.CaptureRe) > 0 {
|
||||
if flag.CaptureRegexp != uk.CaptureRe && uk.Flag == "" {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Pour changer la capture_regexp, vous devez rentrer la réponse attendue à nouveau, car le flag doit être recalculé."})
|
||||
return
|
||||
}
|
||||
flag.CaptureRegexp = uk.CaptureRe
|
||||
} else {
|
||||
if flag.CaptureRegexp != nil && uk.Flag == "" {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Pour changer la capture_regexp, vous devez rentrer la réponse attendue à nouveau, car le flag doit être recalculé."})
|
||||
return
|
||||
}
|
||||
flag.CaptureRegexp = nil
|
||||
}
|
||||
|
||||
if len(uk.Flag) > 0 {
|
||||
var err error
|
||||
flag.Checksum, err = flag.ComputeChecksum([]byte(uk.Flag))
|
||||
|
@ -1058,6 +906,14 @@ func updateExerciceFlag(c *gin.Context) {
|
|||
} else {
|
||||
flag.Checksum = uk.Value
|
||||
}
|
||||
flag.ChoicesCost = uk.ChoicesCost
|
||||
flag.BonusGain = uk.BonusGain
|
||||
|
||||
if uk.CaptureRe != nil && len(*uk.CaptureRe) > 0 {
|
||||
flag.CaptureRegexp = uk.CaptureRe
|
||||
} else {
|
||||
flag.CaptureRegexp = nil
|
||||
}
|
||||
|
||||
if _, err := flag.Update(); err != nil {
|
||||
log.Println("Unable to updateExerciceFlag:", err.Error())
|
||||
|
@ -1166,60 +1022,6 @@ func showExerciceQuizDeps(c *gin.Context) {
|
|||
c.JSON(http.StatusOK, deps)
|
||||
}
|
||||
|
||||
func showExerciceQuizStats(c *gin.Context) {
|
||||
exercice := c.MustGet("exercice").(*fic.Exercice)
|
||||
quiz := c.MustGet("flag-quiz").(*fic.MCQ)
|
||||
|
||||
history, err := exercice.GetHistory()
|
||||
if err != nil {
|
||||
log.Println("Unable to getExerciceHistory:", err.Error())
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when retrieving exercice history"})
|
||||
return
|
||||
}
|
||||
|
||||
var completed int64
|
||||
|
||||
for _, hline := range history {
|
||||
if hline["kind"].(string) == "mcq_found" {
|
||||
if *hline["secondary"].(*int) == quiz.Id {
|
||||
completed += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tries, err := quiz.NbTries()
|
||||
if err != nil {
|
||||
log.Println("Unable to nbTries:", err.Error())
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when retrieving flag tries"})
|
||||
return
|
||||
}
|
||||
|
||||
teams, err := quiz.TeamsOnIt()
|
||||
if err != nil {
|
||||
log.Println("Unable to teamsOnIt:", err.Error())
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when retrieving flag related teams"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"completed": completed,
|
||||
"tries": tries,
|
||||
"teams": teams,
|
||||
})
|
||||
}
|
||||
|
||||
func deleteExerciceQuizTries(c *gin.Context) {
|
||||
quiz := c.MustGet("flag-quiz").(*fic.MCQ)
|
||||
|
||||
err := quiz.DeleteTries()
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.AbortWithStatusJSON(http.StatusOK, true)
|
||||
}
|
||||
|
||||
func updateExerciceQuiz(c *gin.Context) {
|
||||
quiz := c.MustGet("flag-quiz").(*fic.MCQ)
|
||||
|
||||
|
@ -1352,398 +1154,3 @@ func updateExerciceTags(c *gin.Context) {
|
|||
exercice.WipeTags()
|
||||
addExerciceTag(c)
|
||||
}
|
||||
|
||||
type syncDiff struct {
|
||||
Field string `json:"field"`
|
||||
Link string `json:"link"`
|
||||
Before interface{} `json:"be"`
|
||||
After interface{} `json:"af"`
|
||||
}
|
||||
|
||||
func diffExerciceWithRemote(exercice *fic.Exercice, theme *fic.Theme) ([]syncDiff, error) {
|
||||
var diffs []syncDiff
|
||||
|
||||
// Compare exercice attributes
|
||||
thid := exercice.Path[:strings.Index(exercice.Path, "/")]
|
||||
exid := exercice.Path[strings.Index(exercice.Path, "/")+1:]
|
||||
exercice_remote, err := sync.GetRemoteExercice(thid, exid, theme)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, field := range reflect.VisibleFields(reflect.TypeOf(*exercice)) {
|
||||
if ((field.Name == "Image") && path.Base(reflect.ValueOf(*exercice_remote).FieldByName(field.Name).String()) != path.Base(reflect.ValueOf(*exercice).FieldByName(field.Name).String())) || ((field.Name == "Depend") && (((exercice_remote.Depend == nil || exercice.Depend == nil) && exercice.Depend != exercice_remote.Depend) || (exercice_remote.Depend != nil && exercice.Depend != nil && *exercice.Depend != *exercice_remote.Depend))) || (field.Name != "Image" && field.Name != "Depend" && !reflect.ValueOf(*exercice_remote).FieldByName(field.Name).Equal(reflect.ValueOf(*exercice).FieldByName(field.Name))) {
|
||||
if !field.IsExported() || field.Name == "Id" || field.Name == "IdTheme" || field.Name == "IssueKind" || field.Name == "Coefficient" || field.Name == "BackgroundColor" {
|
||||
continue
|
||||
}
|
||||
|
||||
diffs = append(diffs, syncDiff{
|
||||
Field: field.Name,
|
||||
Link: fmt.Sprintf("exercices/%d", exercice.Id),
|
||||
Before: reflect.ValueOf(*exercice).FieldByName(field.Name).Interface(),
|
||||
After: reflect.ValueOf(*exercice_remote).FieldByName(field.Name).Interface(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Compare files
|
||||
files, err := exercice.GetFiles()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to GetFiles: %w", err)
|
||||
}
|
||||
|
||||
files_remote, err := sync.GetRemoteExerciceFiles(thid, exid)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to GetRemoteFiles: %w", err)
|
||||
}
|
||||
|
||||
for i, file_remote := range files_remote {
|
||||
if len(files) <= i {
|
||||
diffs = append(diffs, syncDiff{
|
||||
Field: fmt.Sprintf("files[%d]", i),
|
||||
Link: fmt.Sprintf("exercices/%d", exercice.Id),
|
||||
Before: nil,
|
||||
After: file_remote,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
for _, field := range reflect.VisibleFields(reflect.TypeOf(*file_remote)) {
|
||||
if !field.IsExported() || field.Name == "Id" || field.Name == "IdExercice" {
|
||||
continue
|
||||
}
|
||||
if ((field.Name == "Path") && path.Base(reflect.ValueOf(*file_remote).FieldByName(field.Name).String()) != path.Base(reflect.ValueOf(*files[i]).FieldByName(field.Name).String())) || ((field.Name == "Checksum" || field.Name == "ChecksumShown") && !bytes.Equal(reflect.ValueOf(*file_remote).FieldByName(field.Name).Bytes(), reflect.ValueOf(*files[i]).FieldByName(field.Name).Bytes())) || (field.Name != "Checksum" && field.Name != "ChecksumShown" && field.Name != "Path" && !reflect.ValueOf(*file_remote).FieldByName(field.Name).Equal(reflect.ValueOf(*files[i]).FieldByName(field.Name))) {
|
||||
diffs = append(diffs, syncDiff{
|
||||
Field: fmt.Sprintf("files[%d].%s", i, field.Name),
|
||||
Link: fmt.Sprintf("exercices/%d", exercice.Id),
|
||||
Before: reflect.ValueOf(*files[i]).FieldByName(field.Name).Interface(),
|
||||
After: reflect.ValueOf(*file_remote).FieldByName(field.Name).Interface(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compare flags
|
||||
flags, err := exercice.GetFlags()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to GetFlags: %w", err)
|
||||
}
|
||||
|
||||
flags_remote, err := sync.GetRemoteExerciceFlags(thid, exid)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to GetRemoteFlags: %w", err)
|
||||
}
|
||||
|
||||
var flags_not_found []interface{}
|
||||
var flags_extra_found []interface{}
|
||||
|
||||
for i, flag_remote := range flags_remote {
|
||||
if key_remote, ok := flag_remote.(*fic.FlagKey); ok {
|
||||
found := false
|
||||
|
||||
for _, flag := range flags {
|
||||
if key, ok := flag.(*fic.FlagKey); ok && (key.Label == key_remote.Label || key.Order == key_remote.Order) {
|
||||
found = true
|
||||
|
||||
// Parse flag label
|
||||
if len(key.Label) > 3 && key.Label[0] == '%' {
|
||||
spl := strings.Split(key.Label, "%")
|
||||
key.Label = strings.Join(spl[2:], "%")
|
||||
}
|
||||
|
||||
for _, field := range reflect.VisibleFields(reflect.TypeOf(*key_remote)) {
|
||||
if !field.IsExported() || field.Name == "Id" || field.Name == "IdExercice" {
|
||||
continue
|
||||
}
|
||||
if (field.Name == "Checksum" && !bytes.Equal(key.Checksum, key_remote.Checksum)) || (field.Name == "CaptureRegexp" && ((key.CaptureRegexp == nil || key_remote.CaptureRegexp == nil) && key.CaptureRegexp != key_remote.CaptureRegexp) || (key.CaptureRegexp != nil && key_remote.CaptureRegexp != nil && *key.CaptureRegexp != *key_remote.CaptureRegexp)) || (field.Name != "Checksum" && field.Name != "CaptureRegexp" && !reflect.ValueOf(*key_remote).FieldByName(field.Name).Equal(reflect.ValueOf(*key).FieldByName(field.Name))) {
|
||||
diffs = append(diffs, syncDiff{
|
||||
Field: fmt.Sprintf("flags[%d].%s", i, field.Name),
|
||||
Link: fmt.Sprintf("exercices/%d/flags#flag-%d", exercice.Id, key.Id),
|
||||
Before: reflect.ValueOf(*key).FieldByName(field.Name).Interface(),
|
||||
After: reflect.ValueOf(*key_remote).FieldByName(field.Name).Interface(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
flags_not_found = append(flags_not_found, key_remote)
|
||||
}
|
||||
} else if mcq_remote, ok := flag_remote.(*fic.MCQ); ok {
|
||||
found := false
|
||||
|
||||
for _, flag := range flags {
|
||||
if mcq, ok := flag.(*fic.MCQ); ok && (mcq.Title == mcq_remote.Title || mcq.Order == mcq_remote.Order) {
|
||||
found = true
|
||||
|
||||
for _, field := range reflect.VisibleFields(reflect.TypeOf(*mcq_remote)) {
|
||||
if !field.IsExported() || field.Name == "Id" || field.Name == "IdExercice" {
|
||||
continue
|
||||
}
|
||||
if field.Name == "Entries" {
|
||||
var not_found []*fic.MCQ_entry
|
||||
var extra_found []*fic.MCQ_entry
|
||||
|
||||
for i, entry_remote := range mcq_remote.Entries {
|
||||
found := false
|
||||
|
||||
for j, entry := range mcq.Entries {
|
||||
if entry.Label == entry_remote.Label {
|
||||
for _, field := range reflect.VisibleFields(reflect.TypeOf(*entry_remote)) {
|
||||
if field.Name == "Id" {
|
||||
continue
|
||||
}
|
||||
|
||||
if !reflect.ValueOf(*entry_remote).FieldByName(field.Name).Equal(reflect.ValueOf(*entry).FieldByName(field.Name)) {
|
||||
diffs = append(diffs, syncDiff{
|
||||
Field: fmt.Sprintf("flags[%d].entries[%d].%s", i, j, field.Name),
|
||||
Link: fmt.Sprintf("exercices/%d/flags#quiz-%d", exercice.Id, mcq.Id),
|
||||
Before: reflect.ValueOf(*mcq.Entries[j]).FieldByName(field.Name).Interface(),
|
||||
After: reflect.ValueOf(*entry_remote).FieldByName(field.Name).Interface(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
not_found = append(not_found, entry_remote)
|
||||
}
|
||||
}
|
||||
|
||||
for _, entry := range mcq.Entries {
|
||||
found := false
|
||||
for _, entry_remote := range mcq_remote.Entries {
|
||||
if entry.Label == entry_remote.Label {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
extra_found = append(extra_found, entry)
|
||||
}
|
||||
}
|
||||
|
||||
if len(not_found) > 0 || len(extra_found) > 0 {
|
||||
diffs = append(diffs, syncDiff{
|
||||
Field: fmt.Sprintf("flags[%d].entries", i),
|
||||
Link: fmt.Sprintf("exercices/%d/flags", exercice.Id),
|
||||
Before: extra_found,
|
||||
After: not_found,
|
||||
})
|
||||
}
|
||||
} else if !reflect.ValueOf(*mcq_remote).FieldByName(field.Name).Equal(reflect.ValueOf(*mcq).FieldByName(field.Name)) {
|
||||
diffs = append(diffs, syncDiff{
|
||||
Field: fmt.Sprintf("flags[%d].%s", i, field.Name),
|
||||
Link: fmt.Sprintf("exercices/%d/flags", exercice.Id),
|
||||
Before: reflect.ValueOf(*mcq).FieldByName(field.Name).Interface(),
|
||||
After: reflect.ValueOf(*mcq_remote).FieldByName(field.Name).Interface(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
flags_not_found = append(flags_not_found, mcq_remote)
|
||||
}
|
||||
} else if label_remote, ok := flag_remote.(*fic.FlagLabel); ok {
|
||||
found := false
|
||||
|
||||
for _, flag := range flags {
|
||||
if label, ok := flag.(*fic.FlagLabel); ok && (label.Label == label_remote.Label || label.Order == label_remote.Order) {
|
||||
found = true
|
||||
|
||||
for _, field := range reflect.VisibleFields(reflect.TypeOf(*label_remote)) {
|
||||
if !field.IsExported() || field.Name == "Id" || field.Name == "IdExercice" {
|
||||
continue
|
||||
}
|
||||
if !reflect.ValueOf(*label_remote).FieldByName(field.Name).Equal(reflect.ValueOf(*label).FieldByName(field.Name)) {
|
||||
diffs = append(diffs, syncDiff{
|
||||
Field: fmt.Sprintf("flags[%d].%s", i, field.Name),
|
||||
Link: fmt.Sprintf("exercices/%d/flags#flag-%d", exercice.Id, label.Id),
|
||||
Before: reflect.ValueOf(*label).FieldByName(field.Name).Interface(),
|
||||
After: reflect.ValueOf(*label_remote).FieldByName(field.Name).Interface(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
flags_not_found = append(flags_not_found, label_remote)
|
||||
}
|
||||
} else {
|
||||
log.Printf("unknown flag type: %T", flag_remote)
|
||||
}
|
||||
}
|
||||
|
||||
for _, flag := range flags {
|
||||
if key, ok := flag.(*fic.FlagKey); ok {
|
||||
found := false
|
||||
|
||||
for _, flag_remote := range flags_remote {
|
||||
if key_remote, ok := flag_remote.(*fic.FlagKey); ok && (key.Label == key_remote.Label || key.Order == key_remote.Order) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
flags_extra_found = append(flags_extra_found, flag)
|
||||
}
|
||||
} else if mcq, ok := flag.(*fic.MCQ); ok {
|
||||
found := false
|
||||
|
||||
for _, flag_remote := range flags_remote {
|
||||
if mcq_remote, ok := flag_remote.(*fic.MCQ); ok && (mcq.Title == mcq_remote.Title || mcq.Order == mcq_remote.Order) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
flags_extra_found = append(flags_extra_found, flag)
|
||||
}
|
||||
} else if label, ok := flag.(*fic.FlagLabel); ok {
|
||||
found := false
|
||||
|
||||
for _, flag_remote := range flags_remote {
|
||||
if label_remote, ok := flag_remote.(*fic.FlagLabel); ok && (label.Label == label_remote.Label || label.Order == label_remote.Order) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
flags_extra_found = append(flags_extra_found, flag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(flags_not_found) > 0 || len(flags_extra_found) > 0 {
|
||||
diffs = append(diffs, syncDiff{
|
||||
Field: "flags",
|
||||
Link: fmt.Sprintf("exercices/%d/flags", exercice.Id),
|
||||
Before: flags_extra_found,
|
||||
After: flags_not_found,
|
||||
})
|
||||
}
|
||||
|
||||
// Compare hints
|
||||
hints, err := exercice.GetHints()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to GetHints: %w", err)
|
||||
}
|
||||
|
||||
hints_remote, err := sync.GetRemoteExerciceHints(thid, exid)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to GetRemoteHints: %w", err)
|
||||
}
|
||||
|
||||
for i, hint_remote := range hints_remote {
|
||||
hint_remote.Hint.TreatHintContent()
|
||||
|
||||
for _, field := range reflect.VisibleFields(reflect.TypeOf(*hint_remote.Hint)) {
|
||||
if !field.IsExported() || field.Name == "Id" || field.Name == "IdExercice" {
|
||||
continue
|
||||
}
|
||||
if len(hints) <= i {
|
||||
diffs = append(diffs, syncDiff{
|
||||
Field: fmt.Sprintf("hints[%d].%s", i, field.Name),
|
||||
Link: fmt.Sprintf("exercices/%d", exercice.Id),
|
||||
Before: nil,
|
||||
After: reflect.ValueOf(*hint_remote.Hint).FieldByName(field.Name).Interface(),
|
||||
})
|
||||
} else if !reflect.ValueOf(*hint_remote.Hint).FieldByName(field.Name).Equal(reflect.ValueOf(*hints[i]).FieldByName(field.Name)) {
|
||||
diffs = append(diffs, syncDiff{
|
||||
Field: fmt.Sprintf("hints[%d].%s", i, field.Name),
|
||||
Link: fmt.Sprintf("exercices/%d", exercice.Id),
|
||||
Before: reflect.ValueOf(*hints[i]).FieldByName(field.Name).Interface(),
|
||||
After: reflect.ValueOf(*hint_remote.Hint).FieldByName(field.Name).Interface(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return diffs, err
|
||||
}
|
||||
|
||||
func APIDiffExerciceWithRemote(c *gin.Context) {
|
||||
theme := c.MustGet("theme").(*fic.Theme)
|
||||
exercice := c.MustGet("exercice").(*fic.Exercice)
|
||||
|
||||
diffs, err := diffExerciceWithRemote(exercice, theme)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, diffs)
|
||||
}
|
||||
|
||||
func listTries(c *gin.Context) {
|
||||
exercice := c.MustGet("exercice").(*fic.Exercice)
|
||||
|
||||
tries, err := exercice.TriesList()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, tries)
|
||||
}
|
||||
|
||||
func ExerciceTryHandler(c *gin.Context) {
|
||||
trid, err := strconv.ParseInt(string(c.Params.ByName("trid")), 10, 32)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid try identifier"})
|
||||
return
|
||||
}
|
||||
|
||||
exercice := c.MustGet("exercice").(*fic.Exercice)
|
||||
try, err := exercice.GetTry(trid)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Try not found"})
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("try", try)
|
||||
|
||||
c.Next()
|
||||
}
|
||||
|
||||
func getExerciceTry(c *gin.Context) {
|
||||
try := c.MustGet("try").(*fic.ExerciceTry)
|
||||
|
||||
err := try.FillDetails()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, try)
|
||||
}
|
||||
|
||||
func deleteExerciceTry(c *gin.Context) {
|
||||
try := c.MustGet("try").(*fic.ExerciceTry)
|
||||
|
||||
_, err := try.Delete()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
|
|
@ -3,12 +3,9 @@ package api
|
|||
import (
|
||||
"archive/zip"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
"srs.epita.fr/fic-server/admin/sync"
|
||||
"srs.epita.fr/fic-server/libfic"
|
||||
"srs.epita.fr/fic-server/settings"
|
||||
|
||||
|
@ -34,9 +31,6 @@ func declareExportRoutes(router *gin.RouterGroup) {
|
|||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
|
||||
return
|
||||
}
|
||||
s.End = nil
|
||||
s.NextChangeTime = nil
|
||||
s.DelegatedQA = []string{}
|
||||
|
||||
teams, err := fic.ExportTeams(false)
|
||||
if err != nil {
|
||||
|
@ -62,41 +56,6 @@ func declareExportRoutes(router *gin.RouterGroup) {
|
|||
json.NewEncoder(f).Encode(challengeinfo)
|
||||
}
|
||||
|
||||
// Include partners' logos from challenge.json
|
||||
if sync.GlobalImporter != nil {
|
||||
if len(challengeinfo.MainLogo) > 0 {
|
||||
for _, logo := range challengeinfo.MainLogo {
|
||||
fd, closer, err := sync.OpenOrGetFile(sync.GlobalImporter, logo)
|
||||
if err != nil {
|
||||
log.Printf("Unable to archive main logo %q: %s", logo, err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
f, err := w.Create(path.Join("logo", path.Base(logo)))
|
||||
if err == nil {
|
||||
io.Copy(f, fd)
|
||||
}
|
||||
closer()
|
||||
}
|
||||
}
|
||||
|
||||
if len(challengeinfo.Partners) > 0 {
|
||||
for _, partner := range challengeinfo.Partners {
|
||||
fd, closer, err := sync.OpenOrGetFile(sync.GlobalImporter, partner.Src)
|
||||
if err != nil {
|
||||
log.Printf("Unable to archive partner logo %q: %s", partner.Src, err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
f, err := w.Create(path.Join("partner", path.Base(partner.Src)))
|
||||
if err == nil {
|
||||
io.Copy(f, fd)
|
||||
}
|
||||
closer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// my.json
|
||||
f, err = w.Create("my.json")
|
||||
if err == nil {
|
||||
|
|
|
@ -5,7 +5,6 @@ import (
|
|||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
|
@ -144,27 +143,12 @@ func listFiles(c *gin.Context) {
|
|||
}
|
||||
|
||||
func clearFiles(c *gin.Context) {
|
||||
err := os.RemoveAll(fic.FilesDir)
|
||||
_, err := fic.ClearFiles()
|
||||
if err != nil {
|
||||
log.Println("Unable to remove files:", err.Error())
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
err = os.MkdirAll(fic.FilesDir, 0751)
|
||||
if err != nil {
|
||||
log.Println("Unable to create FILES:", err.Error())
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
_, err = fic.ClearFiles()
|
||||
if err != nil {
|
||||
log.Println("Unable to clean DB files:", err.Error())
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Les fichiers ont bien été effacés. Mais il n'a pas été possible d'effacer la base de données. Refaites une synchronisation maintenant. " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, true)
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"text/template"
|
||||
"unicode"
|
||||
|
||||
|
@ -33,13 +34,9 @@ func declarePasswordRoutes(router *gin.RouterGroup) {
|
|||
|
||||
c.JSON(http.StatusOK, gin.H{"password": passwd})
|
||||
})
|
||||
router.GET("/oauth-status", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"secret_defined": OidcSecret != "",
|
||||
})
|
||||
})
|
||||
router.GET("/dex.yaml", func(c *gin.Context) {
|
||||
cfg, err := genDexConfig()
|
||||
_, staticpassword := c.Request.URL.Query()["staticpassword"]
|
||||
cfg, err := genDexConfig(staticpassword)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
|
||||
return
|
||||
|
@ -48,7 +45,8 @@ func declarePasswordRoutes(router *gin.RouterGroup) {
|
|||
c.String(http.StatusOK, string(cfg))
|
||||
})
|
||||
router.POST("/dex.yaml", func(c *gin.Context) {
|
||||
if dexcfg, err := genDexConfig(); err != nil {
|
||||
_, staticpassword := c.Request.URL.Query()["staticpassword"]
|
||||
if dexcfg, err := genDexConfig(staticpassword); err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
|
||||
return
|
||||
} else if err := ioutil.WriteFile(path.Join(pki.PKIDir, "shared", "dex-config.yaml"), []byte(dexcfg), 0644); err != nil {
|
||||
|
@ -139,8 +137,12 @@ storage:
|
|||
file: /var/dex/dex.db
|
||||
web:
|
||||
http: 0.0.0.0:5556
|
||||
{{ if .GRPC }}
|
||||
grpc:
|
||||
addr: 127.0.0.1:5557
|
||||
{{ end }}
|
||||
frontend:
|
||||
issuer: {{ .Name }}
|
||||
issuer: Challenge forensic
|
||||
logoURL: {{ .LogoPath }}
|
||||
dir: /srv/dex/web/
|
||||
oauth2:
|
||||
|
@ -214,16 +216,23 @@ type dexConfig struct {
|
|||
Clients []dexConfigClient
|
||||
Teams []*fic.Team
|
||||
LogoPath string
|
||||
GRPC bool
|
||||
}
|
||||
|
||||
func genDexConfig() ([]byte, error) {
|
||||
func genDexConfig(withTeams bool) ([]byte, error) {
|
||||
if OidcSecret == "" {
|
||||
return nil, fmt.Errorf("Unable to generate dex configuration: OIDC Secret not defined. Please define FICOIDC_SECRET in your environment.")
|
||||
}
|
||||
|
||||
teams, err := fic.GetTeams()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
var teams []*fic.Team
|
||||
var err error
|
||||
|
||||
// Should teams be included as static passwords, instead of being managed by GRPC
|
||||
if withTeams {
|
||||
teams, err = fic.GetTeams()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
b := bytes.NewBufferString("")
|
||||
|
@ -241,7 +250,7 @@ func genDexConfig() ([]byte, error) {
|
|||
|
||||
logoPath := ""
|
||||
if len(challengeInfo.MainLogo) > 0 {
|
||||
logoPath = path.Join("../../files", "logo", path.Base(challengeInfo.MainLogo[len(challengeInfo.MainLogo)-1]))
|
||||
logoPath = strings.Replace(challengeInfo.MainLogo[len(challengeInfo.MainLogo)-1], "$FILES$", fic.FilesDir, -1)
|
||||
}
|
||||
|
||||
dexTmpl, err := template.New("dexcfg").Parse(dexcfgtpl)
|
||||
|
@ -262,12 +271,20 @@ func genDexConfig() ([]byte, error) {
|
|||
},
|
||||
Teams: teams,
|
||||
LogoPath: logoPath,
|
||||
GRPC: !withTeams,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("An error occurs during template execution: %w", err)
|
||||
}
|
||||
|
||||
// Also generate team associations
|
||||
if !withTeams {
|
||||
teams, err = fic.GetTeams()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
for _, team := range teams {
|
||||
if _, err := os.Stat(path.Join(TeamsDir, fmt.Sprintf("team%02d", team.Id))); err == nil {
|
||||
if err = os.Remove(path.Join(TeamsDir, fmt.Sprintf("team%02d", team.Id))); err != nil {
|
||||
|
@ -285,11 +302,6 @@ func genDexConfig() ([]byte, error) {
|
|||
}
|
||||
|
||||
func genDexPasswordTpl() ([]byte, error) {
|
||||
challengeInfo, err := GetChallengeInfo()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Cannot create template: %w", err)
|
||||
}
|
||||
|
||||
if teams, err := fic.GetTeams(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
|
@ -299,7 +311,6 @@ func genDexPasswordTpl() ([]byte, error) {
|
|||
return nil, fmt.Errorf("Cannot create template: %w", err)
|
||||
} else if err = dexTmpl.Execute(b, dexConfig{
|
||||
Teams: teams,
|
||||
Name: challengeInfo.Title,
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("An error occurs during template execution: %w", err)
|
||||
} else {
|
||||
|
|
|
@ -41,27 +41,5 @@ func declareRepositoriesRoutes(router *gin.RouterGroup) {
|
|||
}
|
||||
c.JSON(http.StatusOK, mod)
|
||||
})
|
||||
|
||||
router.DELETE("/repositories/*repopath", func(c *gin.Context) {
|
||||
di, ok := sync.GlobalImporter.(sync.DeletableImporter)
|
||||
if !ok {
|
||||
c.AbortWithStatusJSON(http.StatusNotImplemented, gin.H{"errmsg": "Not implemented"})
|
||||
return
|
||||
}
|
||||
|
||||
if strings.Contains(c.Param("repopath"), "..") {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Repopath contains invalid characters"})
|
||||
return
|
||||
}
|
||||
|
||||
repopath := strings.TrimPrefix(c.Param("repopath"), "/")
|
||||
|
||||
err := di.DeleteDir(repopath)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, true)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
|
@ -25,6 +26,7 @@ func declareSettingsRoutes(router *gin.RouterGroup) {
|
|||
router.GET("/challenge.json", getChallengeInfo)
|
||||
router.PUT("/challenge.json", saveChallengeInfo)
|
||||
|
||||
router.GET("/settings-ro.json", getROSettings)
|
||||
router.GET("/settings.json", getSettings)
|
||||
router.PUT("/settings.json", saveSettings)
|
||||
router.DELETE("/settings.json", func(c *gin.Context) {
|
||||
|
@ -96,6 +98,24 @@ func fullGeneration(c *gin.Context) {
|
|||
})
|
||||
}
|
||||
|
||||
func getROSettings(c *gin.Context) {
|
||||
syncMtd := "Disabled"
|
||||
if sync.GlobalImporter != nil {
|
||||
syncMtd = sync.GlobalImporter.Kind()
|
||||
}
|
||||
|
||||
var syncId *string
|
||||
if sync.GlobalImporter != nil {
|
||||
syncId = sync.GlobalImporter.Id()
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"sync-type": reflect.TypeOf(sync.GlobalImporter).Name(),
|
||||
"sync-id": syncId,
|
||||
"sync": syncMtd,
|
||||
})
|
||||
}
|
||||
|
||||
func GetChallengeInfo() (*settings.ChallengeInfo, error) {
|
||||
var challengeinfo string
|
||||
var err error
|
||||
|
@ -310,7 +330,6 @@ func ApplySettings(config *settings.Settings) {
|
|||
fic.SubmissionCostBase = config.SubmissionCostBase
|
||||
fic.SubmissionUniqueness = config.SubmissionUniqueness
|
||||
fic.CountOnlyNotGoodTries = config.CountOnlyNotGoodTries
|
||||
fic.QuestionGainRatio = config.QuestionGainRatio
|
||||
|
||||
if config.DiscountedFactor != fic.DiscountedFactor {
|
||||
fic.DiscountedFactor = config.DiscountedFactor
|
||||
|
@ -330,7 +349,6 @@ func ResetSettings() error {
|
|||
WChoiceCurCoefficient: 1,
|
||||
GlobalScoreCoefficient: 1,
|
||||
DiscountedFactor: 0,
|
||||
QuestionGainRatio: 0,
|
||||
UnlockedStandaloneExercices: 10,
|
||||
UnlockedStandaloneExercicesByThemeStepValidation: 1,
|
||||
UnlockedStandaloneExercicesByStandaloneExerciceValidation: 0,
|
||||
|
|
|
@ -7,7 +7,6 @@ import (
|
|||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"srs.epita.fr/fic-server/admin/generation"
|
||||
|
@ -18,8 +17,6 @@ import (
|
|||
"go.uber.org/multierr"
|
||||
)
|
||||
|
||||
var lastSyncError = ""
|
||||
|
||||
func flatifySyncErrors(errs error) (ret []string) {
|
||||
for _, err := range multierr.Errors(errs) {
|
||||
ret = append(ret, err.Error())
|
||||
|
@ -30,37 +27,12 @@ func flatifySyncErrors(errs error) (ret []string) {
|
|||
func declareSyncRoutes(router *gin.RouterGroup) {
|
||||
apiSyncRoutes := router.Group("/sync")
|
||||
|
||||
// Return the global sync status
|
||||
apiSyncRoutes.GET("/status", func(c *gin.Context) {
|
||||
syncMtd := "Disabled"
|
||||
if sync.GlobalImporter != nil {
|
||||
syncMtd = sync.GlobalImporter.Kind()
|
||||
}
|
||||
|
||||
var syncId *string
|
||||
if sync.GlobalImporter != nil {
|
||||
syncId = sync.GlobalImporter.Id()
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"sync-type": reflect.TypeOf(sync.GlobalImporter).Name(),
|
||||
"sync-id": syncId,
|
||||
"sync": syncMtd,
|
||||
"pullMutex": !sync.OneGitPullStatus(),
|
||||
"syncMutex": !sync.OneDeepSyncStatus() && !sync.OneThemeDeepSyncStatus(),
|
||||
"progress": sync.DeepSyncProgress,
|
||||
"lastError": lastSyncError,
|
||||
})
|
||||
})
|
||||
|
||||
// Base sync checks if the local directory is in sync with remote one.
|
||||
apiSyncRoutes.POST("/base", func(c *gin.Context) {
|
||||
err := sync.GlobalImporter.Sync()
|
||||
if err != nil {
|
||||
lastSyncError = err.Error()
|
||||
c.JSON(http.StatusExpectationFailed, gin.H{"errmsg": err.Error()})
|
||||
} else {
|
||||
lastSyncError = ""
|
||||
c.JSON(http.StatusOK, true)
|
||||
}
|
||||
})
|
||||
|
@ -73,25 +45,27 @@ func declareSyncRoutes(router *gin.RouterGroup) {
|
|||
})
|
||||
|
||||
// Deep sync: a fully recursive synchronization (can be limited by theme).
|
||||
apiSyncRoutes.POST("/deep", func(c *gin.Context) {
|
||||
r := sync.SyncDeep(sync.GlobalImporter)
|
||||
lastSyncError = ""
|
||||
c.JSON(http.StatusOK, r)
|
||||
apiSyncRoutes.GET("/deep", func(c *gin.Context) {
|
||||
if sync.DeepSyncProgress == 0 {
|
||||
c.AbortWithStatusJSON(http.StatusTooEarly, gin.H{"errmsg": "Pas de synchronisation en cours"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"progress": sync.DeepSyncProgress})
|
||||
})
|
||||
apiSyncRoutes.POST("/deep", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, sync.SyncDeep(sync.GlobalImporter))
|
||||
})
|
||||
|
||||
apiSyncRoutes.POST("/local-diff", APIDiffDBWithRemote)
|
||||
|
||||
apiSyncDeepRoutes := apiSyncRoutes.Group("/deep/:thid")
|
||||
apiSyncDeepRoutes.Use(ThemeHandler)
|
||||
// Special route to handle standalone exercices
|
||||
apiSyncRoutes.POST("/deep/0", func(c *gin.Context) {
|
||||
var st []string
|
||||
for _, se := range multierr.Errors(sync.SyncThemeDeep(sync.GlobalImporter, &fic.StandaloneExercicesTheme, 0, 250, nil)) {
|
||||
for _, se := range multierr.Errors(sync.SyncThemeDeep(sync.GlobalImporter, &fic.Theme{Path: sync.StandaloneExercicesDirectory}, 0, 250, nil)) {
|
||||
st = append(st, se.Error())
|
||||
}
|
||||
sync.EditDeepReport(&sync.SyncReport{Exercices: st}, false)
|
||||
sync.DeepSyncProgress = 255
|
||||
lastSyncError = ""
|
||||
c.JSON(http.StatusOK, st)
|
||||
})
|
||||
apiSyncDeepRoutes.POST("", func(c *gin.Context) {
|
||||
|
@ -105,7 +79,6 @@ func declareSyncRoutes(router *gin.RouterGroup) {
|
|||
}
|
||||
sync.EditDeepReport(&sync.SyncReport{Themes: map[string][]string{theme.Name: st}}, false)
|
||||
sync.DeepSyncProgress = 255
|
||||
lastSyncError = ""
|
||||
c.JSON(http.StatusOK, st)
|
||||
})
|
||||
|
||||
|
@ -117,7 +90,6 @@ func declareSyncRoutes(router *gin.RouterGroup) {
|
|||
|
||||
apiSyncRoutes.POST("/themes", func(c *gin.Context) {
|
||||
_, errs := sync.SyncThemes(sync.GlobalImporter)
|
||||
lastSyncError = ""
|
||||
c.JSON(http.StatusOK, flatifySyncErrors(errs))
|
||||
})
|
||||
|
||||
|
@ -241,7 +213,7 @@ func declareSyncExercicesRoutes(router *gin.RouterGroup) {
|
|||
|
||||
exceptions := sync.LoadExerciceException(sync.GlobalImporter, theme, exercice, nil)
|
||||
|
||||
c.JSON(http.StatusOK, flatifySyncErrors(sync.ImportExerciceFiles(sync.GlobalImporter, exercice, exceptions)))
|
||||
c.JSON(http.StatusOK, flatifySyncErrors(sync.SyncExerciceFiles(sync.GlobalImporter, exercice, exceptions)))
|
||||
})
|
||||
apiSyncExercicesRoutes.POST("/fixurlid", func(c *gin.Context) {
|
||||
exercice := c.MustGet("exercice").(*fic.Exercice)
|
||||
|
@ -286,12 +258,10 @@ func autoSync(c *gin.Context) {
|
|||
|
||||
if !IsProductionEnv {
|
||||
if err := sync.GlobalImporter.Sync(); err != nil {
|
||||
lastSyncError = err.Error()
|
||||
log.Println("Unable to sync.GI.Sync:", err.Error())
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to perform the pull."})
|
||||
return
|
||||
}
|
||||
lastSyncError = ""
|
||||
}
|
||||
|
||||
themes, err := fic.GetThemes()
|
||||
|
@ -380,32 +350,3 @@ func autoSync(c *gin.Context) {
|
|||
|
||||
c.JSON(http.StatusOK, st)
|
||||
}
|
||||
|
||||
func diffDBWithRemote() (map[string][]syncDiff, error) {
|
||||
diffs := map[string][]syncDiff{}
|
||||
|
||||
themes, err := fic.GetThemesExtended()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Compare inner themes
|
||||
for _, theme := range themes {
|
||||
diffs[theme.Name], err = diffThemeWithRemote(theme)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to diffThemeWithRemote: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return diffs, err
|
||||
}
|
||||
|
||||
func APIDiffDBWithRemote(c *gin.Context) {
|
||||
diffs, err := diffDBWithRemote()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, diffs)
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
@ -186,9 +186,6 @@ func declareTeamsRoutes(router *gin.RouterGroup) {
|
|||
declareTeamsPasswordRoutes(apiTeamsRoutes)
|
||||
declareTeamClaimsRoutes(apiTeamsRoutes)
|
||||
declareTeamCertificateRoutes(apiTeamsRoutes)
|
||||
|
||||
// Import teams from cyberrange
|
||||
router.POST("/cyberrange-teams.json", importTeamsFromCyberrange)
|
||||
}
|
||||
|
||||
func TeamHandler(c *gin.Context) {
|
||||
|
@ -295,11 +292,6 @@ func bindingTeams(c *gin.Context) {
|
|||
c.String(http.StatusOK, ret)
|
||||
}
|
||||
|
||||
type teamAssociation struct {
|
||||
Association string `json:"association"`
|
||||
TeamId int64 `json:"team_id"`
|
||||
}
|
||||
|
||||
func allAssociations(c *gin.Context) {
|
||||
teams, err := fic.GetTeams()
|
||||
if err != nil {
|
||||
|
@ -308,7 +300,7 @@ func allAssociations(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
var ret []teamAssociation
|
||||
var ret []string
|
||||
|
||||
for _, team := range teams {
|
||||
assocs, err := pki.GetTeamAssociations(TeamsDir, team.Id)
|
||||
|
@ -318,84 +310,13 @@ func allAssociations(c *gin.Context) {
|
|||
}
|
||||
|
||||
for _, a := range assocs {
|
||||
ret = append(ret, teamAssociation{a, team.Id})
|
||||
ret = append(ret, a)
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, ret)
|
||||
}
|
||||
|
||||
func importTeamsFromCyberrange(c *gin.Context) {
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"errmsg": "Failed to get file: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"errmsg": "Failed to open file: " + err.Error()})
|
||||
return
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
var ut []fic.CyberrangeTeamBase
|
||||
err = json.NewDecoder(src).Decode(&fic.CyberrangeAPIResponse{Data: &ut})
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
teams, err := fic.GetTeams()
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Impossible de récupérer la liste des équipes actuelles: %s", err.Error())})
|
||||
return
|
||||
}
|
||||
|
||||
for _, crteam := range ut {
|
||||
var exist_team *fic.Team
|
||||
for _, team := range teams {
|
||||
if team.Name == crteam.Name || team.ExternalId == crteam.UUID {
|
||||
exist_team = team
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if exist_team != nil {
|
||||
exist_team.Name = crteam.Name
|
||||
exist_team.ExternalId = crteam.UUID
|
||||
_, err = exist_team.Update()
|
||||
} else {
|
||||
exist_team, err = fic.CreateTeam(crteam.Name, fic.RandomColor().ToRGB(), crteam.UUID)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Impossible d'ajouter/de modifier l'équipe %v: %s", crteam, err.Error())})
|
||||
return
|
||||
}
|
||||
|
||||
// Import members
|
||||
if c.DefaultQuery("nomembers", "0") != "" && len(crteam.Members) > 0 {
|
||||
exist_team.ClearMembers()
|
||||
|
||||
for _, member := range crteam.Members {
|
||||
_, err = exist_team.AddMember(member.Name, "", member.Nickname, exist_team.Name)
|
||||
if err != nil {
|
||||
log.Printf("Unable to add member %q to team %s (tid=%d): %s", member.UUID, exist_team.Name, exist_team.Id, err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
teams, err = fic.GetTeams()
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Impossible de récupérer la liste des équipes après import: %s", err.Error())})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, teams)
|
||||
}
|
||||
|
||||
func createTeam(c *gin.Context) {
|
||||
var ut fic.Team
|
||||
err := c.ShouldBindJSON(&ut)
|
||||
|
@ -405,7 +326,11 @@ func createTeam(c *gin.Context) {
|
|||
}
|
||||
|
||||
if ut.Color == 0 {
|
||||
ut.Color = fic.RandomColor().ToRGB()
|
||||
ut.Color = fic.HSL{
|
||||
H: rand.Float64(),
|
||||
S: 1,
|
||||
L: 0.5,
|
||||
}.ToRGB()
|
||||
}
|
||||
|
||||
team, err := fic.CreateTeam(strings.TrimSpace(ut.Name), ut.Color, ut.ExternalId)
|
||||
|
|
|
@ -5,9 +5,7 @@ import (
|
|||
"log"
|
||||
"net/http"
|
||||
"path"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"srs.epita.fr/fic-server/admin/sync"
|
||||
"srs.epita.fr/fic-server/libfic"
|
||||
|
@ -50,8 +48,6 @@ func declareThemesRoutes(router *gin.RouterGroup) {
|
|||
apiThemesRoutes.PUT("", updateTheme)
|
||||
apiThemesRoutes.DELETE("", deleteTheme)
|
||||
|
||||
apiThemesRoutes.POST("/diff-sync", APIDiffThemeWithRemote)
|
||||
|
||||
apiThemesRoutes.GET("/exercices_stats.json", getThemedExercicesStats)
|
||||
|
||||
declareExercicesRoutes(apiThemesRoutes)
|
||||
|
@ -74,18 +70,14 @@ func ThemeHandler(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
if thid == 0 {
|
||||
c.Set("theme", &fic.StandaloneExercicesTheme)
|
||||
} else {
|
||||
theme, err := fic.GetTheme(thid)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Theme not found"})
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("theme", theme)
|
||||
theme, err := fic.GetTheme(thid)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Theme not found"})
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("theme", theme)
|
||||
|
||||
c.Next()
|
||||
}
|
||||
|
||||
|
@ -135,10 +127,6 @@ func listThemes(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
if has, _ := fic.HasStandaloneExercice(); has {
|
||||
themes = append([]*fic.Theme{&fic.StandaloneExercicesTheme}, themes...)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, themes)
|
||||
}
|
||||
|
||||
|
@ -267,110 +255,3 @@ func getThemedExercicesStats(c *gin.Context) {
|
|||
}
|
||||
c.JSON(http.StatusOK, ret)
|
||||
}
|
||||
|
||||
func diffThemeWithRemote(theme *fic.Theme) ([]syncDiff, error) {
|
||||
var diffs []syncDiff
|
||||
|
||||
// Compare theme attributes
|
||||
theme_remote, err := sync.GetRemoteTheme(theme.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, field := range reflect.VisibleFields(reflect.TypeOf(*theme)) {
|
||||
if ((field.Name == "Image") && path.Base(reflect.ValueOf(*theme_remote).FieldByName(field.Name).String()) != path.Base(reflect.ValueOf(*theme).FieldByName(field.Name).String())) || (field.Name != "Image" && !reflect.ValueOf(*theme_remote).FieldByName(field.Name).Equal(reflect.ValueOf(*theme).FieldByName(field.Name))) {
|
||||
if !field.IsExported() || field.Name == "Id" || field.Name == "IdTheme" || field.Name == "IssueKind" || field.Name == "BackgroundColor" {
|
||||
continue
|
||||
}
|
||||
|
||||
diffs = append(diffs, syncDiff{
|
||||
Field: field.Name,
|
||||
Link: fmt.Sprintf("themes/%d", theme.Id),
|
||||
Before: reflect.ValueOf(*theme).FieldByName(field.Name).Interface(),
|
||||
After: reflect.ValueOf(*theme_remote).FieldByName(field.Name).Interface(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Compare exercices list
|
||||
exercices, err := theme.GetExercices()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to GetExercices: %w", err)
|
||||
}
|
||||
|
||||
exercices_remote, err := sync.ListRemoteExercices(theme.Path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to ListRemoteExercices: %w", err)
|
||||
}
|
||||
|
||||
var not_found []string
|
||||
var extra_found []string
|
||||
|
||||
for _, exercice_remote := range exercices_remote {
|
||||
found := false
|
||||
for _, exercice := range exercices {
|
||||
if exercice.Path[strings.Index(exercice.Path, "/")+1:] == exercice_remote {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
not_found = append(not_found, exercice_remote)
|
||||
}
|
||||
}
|
||||
|
||||
for _, exercice := range exercices {
|
||||
found := false
|
||||
for _, exercice_remote := range exercices_remote {
|
||||
if exercice.Path[strings.Index(exercice.Path, "/")+1:] == exercice_remote {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
extra_found = append(extra_found, exercice.Path[strings.Index(exercice.Path, "/")+1:])
|
||||
}
|
||||
}
|
||||
|
||||
if len(not_found) > 0 || len(extra_found) > 0 {
|
||||
diffs = append(diffs, syncDiff{
|
||||
Field: "theme.Exercices",
|
||||
Link: fmt.Sprintf("themes/%d", theme.Id),
|
||||
Before: strings.Join(extra_found, ", "),
|
||||
After: strings.Join(not_found, ", "),
|
||||
})
|
||||
}
|
||||
|
||||
// Compare inner exercices
|
||||
for i, exercice := range exercices {
|
||||
exdiffs, err := diffExerciceWithRemote(exercice, theme)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to diffExerciceWithRemote: %w", err)
|
||||
}
|
||||
|
||||
for _, exdiff := range exdiffs {
|
||||
if theme.Id == 0 {
|
||||
exdiff.Field = fmt.Sprintf("exercices[%d].%s", exercice.Id, exdiff.Field)
|
||||
} else {
|
||||
exdiff.Field = fmt.Sprintf("exercices[%d].%s", i, exdiff.Field)
|
||||
}
|
||||
diffs = append(diffs, exdiff)
|
||||
}
|
||||
}
|
||||
|
||||
return diffs, err
|
||||
}
|
||||
|
||||
func APIDiffThemeWithRemote(c *gin.Context) {
|
||||
theme := c.MustGet("theme").(*fic.Theme)
|
||||
|
||||
diffs, err := diffThemeWithRemote(theme)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, diffs)
|
||||
}
|
||||
|
|
|
@ -73,7 +73,7 @@ func main() {
|
|||
func treatDir(p string) {
|
||||
var expath string
|
||||
|
||||
for _, f := range []string{"challenge.toml", "challenge.txt"} {
|
||||
for _, f := range []string{"challenge.txt", "challenge.toml"} {
|
||||
if sync.GlobalImporter.Exists(path.Join(p, f)) {
|
||||
expath = p
|
||||
break
|
||||
|
@ -108,7 +108,7 @@ func treatExercice(expath string) {
|
|||
|
||||
paramsFiles, err := sync.GetExerciceFilesParams(sync.GlobalImporter, exercice)
|
||||
if err != nil {
|
||||
log.Printf("Unable to read challenge.toml %q: %s", expath, err.Error())
|
||||
log.Printf("Unable to read challenge.txt %q: %s", expath, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ const indextpl = `<!DOCTYPE html>
|
|||
<html ng-app="FICApp">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>{{ .title }} - Administration</title>
|
||||
<title>Challenge Forensic - Administration</title>
|
||||
<link href="{{.urlbase}}css/bootstrap.min.css" type="text/css" rel="stylesheet">
|
||||
<link href="{{.urlbase}}css/glyphicon.css" type="text/css" rel="stylesheet" media="screen">
|
||||
<style>
|
||||
|
@ -86,7 +86,7 @@ const indextpl = `<!DOCTYPE html>
|
|||
<body class="bg-light text-dark">
|
||||
<nav class="navbar sticky-top navbar-expand-lg navbar-dark text-light" ng-class="{'bg-dark': settings.wip, 'bg-danger': !settings.wip}">
|
||||
<a class="navbar-brand" href=".">
|
||||
<img alt="{{ .title }}" src="{{ .logo }}" style="height: 30px">
|
||||
<img alt="FIC" src="img/fic.png" style="height: 30px">
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#adminMenu" aria-controls="adminMenu" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
|
@ -95,7 +95,7 @@ const indextpl = `<!DOCTYPE html>
|
|||
<div class="collapse navbar-collapse" id="adminMenu">
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item" ng-class="{'active': $location.path().startsWith('/teams')}"><a class="nav-link" href="teams">Équipes</a></li>
|
||||
<li class="nav-item" ng-class="{'active': $location.path().startsWith('/auth')}"><a class="nav-link" href="auth">Authentification</a></li>
|
||||
<li class="nav-item" ng-class="{'active': $location.path().startsWith('/pki')}"><a class="nav-link" href="pki">PKI</a></li>
|
||||
<li class="nav-item" ng-class="{'active': $location.path().startsWith('/themes')}"><a class="nav-link" href="themes">Thèmes</a></li>
|
||||
<li class="nav-item" ng-class="{'active': $location.path().startsWith('/exercices')}"><a class="nav-link" href="exercices">Exercices</a></li>
|
||||
<li class="nav-item" ng-class="{'active': $location.path().startsWith('/public')}"><a class="nav-link" href="public/0">Public</a></li>
|
||||
|
@ -124,7 +124,7 @@ const indextpl = `<!DOCTYPE html>
|
|||
</div>
|
||||
|
||||
<span id="clock" class="navbar-text" ng-controller="CountdownController" ng-cloak>
|
||||
<div style="pointer-events: none; position: absolute;">
|
||||
<div style="position: absolute;">
|
||||
<div style="position: absolute;" id="circle1" class="circle-anim border-danger"></div>
|
||||
<div style="position: absolute;" id="circle2" class="circle-anim border-info"></div>
|
||||
</div>
|
||||
|
|
|
@ -199,10 +199,10 @@ func main() {
|
|||
}
|
||||
log.Println("Using", sync.GlobalImporter.Kind())
|
||||
|
||||
challengeinfo, err := sync.GetFileContent(sync.GlobalImporter, settings.ChallengeFile)
|
||||
if err == nil {
|
||||
// Initial distribution of challenge.json
|
||||
if _, err := os.Stat(path.Join(settings.SettingsDir, settings.ChallengeFile)); os.IsNotExist(err) {
|
||||
// Update distributed challenge.json
|
||||
if _, err := os.Stat(path.Join(settings.SettingsDir, settings.ChallengeFile)); os.IsNotExist(err) {
|
||||
challengeinfo, err := sync.GetFileContent(sync.GlobalImporter, settings.ChallengeFile)
|
||||
if err == nil {
|
||||
if fd, err := os.Create(path.Join(settings.SettingsDir, settings.ChallengeFile)); err != nil {
|
||||
log.Fatal("Unable to open SETTINGS/challenge.json:", err)
|
||||
} else {
|
||||
|
@ -213,10 +213,6 @@ func main() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ci, err := settings.ReadChallengeInfo(challengeinfo); err == nil {
|
||||
fic.StandaloneExercicesTheme.Authors = ci.Authors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,6 @@ import (
|
|||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
@ -26,24 +25,10 @@ var assets embed.FS
|
|||
var indexPage []byte
|
||||
|
||||
func genIndex(baseURL string) {
|
||||
tplcfg := map[string]string{
|
||||
"logo": "img/logo.png",
|
||||
"title": "Challenge",
|
||||
"urlbase": path.Clean(path.Join(baseURL+"/", "nuke"))[:len(path.Clean(path.Join(baseURL+"/", "nuke")))-4],
|
||||
}
|
||||
|
||||
ci, err := api.GetChallengeInfo()
|
||||
if err == nil && ci != nil {
|
||||
tplcfg["title"] = ci.Title
|
||||
if len(ci.MainLogo) > 0 {
|
||||
tplcfg["logo"] = "/files/logo/" + path.Base(ci.MainLogo[0])
|
||||
}
|
||||
}
|
||||
|
||||
b := bytes.NewBufferString("")
|
||||
if indexTmpl, err := template.New("index").Parse(indextpl); err != nil {
|
||||
log.Fatal("Cannot create template:", err)
|
||||
} else if err = indexTmpl.Execute(b, tplcfg); err != nil {
|
||||
} else if err = indexTmpl.Execute(b, map[string]string{"urlbase": path.Clean(path.Join(baseURL+"/", "nuke"))[:len(path.Clean(path.Join(baseURL+"/", "nuke")))-4]}); err != nil {
|
||||
log.Fatal("An error occurs during template execution:", err)
|
||||
} else {
|
||||
indexPage = b.Bytes()
|
||||
|
@ -65,9 +50,6 @@ func declareStaticRoutes(router *gin.RouterGroup, cfg *settings.Settings, baseUR
|
|||
router.GET("/", func(c *gin.Context) {
|
||||
serveIndex(c)
|
||||
})
|
||||
router.GET("/auth/*_", func(c *gin.Context) {
|
||||
serveIndex(c)
|
||||
})
|
||||
router.GET("/claims/*_", func(c *gin.Context) {
|
||||
serveIndex(c)
|
||||
})
|
||||
|
@ -80,9 +62,6 @@ func declareStaticRoutes(router *gin.RouterGroup, cfg *settings.Settings, baseUR
|
|||
router.GET("/files", func(c *gin.Context) {
|
||||
serveIndex(c)
|
||||
})
|
||||
router.GET("/forge-links", func(c *gin.Context) {
|
||||
serveIndex(c)
|
||||
})
|
||||
router.GET("/public/*_", func(c *gin.Context) {
|
||||
serveIndex(c)
|
||||
})
|
||||
|
@ -125,20 +104,8 @@ func declareStaticRoutes(router *gin.RouterGroup, cfg *settings.Settings, baseUR
|
|||
})
|
||||
|
||||
router.GET("/files/*_", func(c *gin.Context) {
|
||||
filepath := path.Join(fic.FilesDir, strings.TrimPrefix(strings.TrimPrefix(c.Request.URL.Path, baseURL), "/files"))
|
||||
|
||||
if st, err := os.Stat(filepath); os.IsNotExist(err) || st.Size() == 0 {
|
||||
if st, err := os.Stat(filepath + ".gz"); err == nil {
|
||||
if fd, err := os.Open(filepath + ".gz"); err == nil {
|
||||
c.DataFromReader(http.StatusOK, st.Size(), "application/octet-stream", fd, map[string]string{
|
||||
"Content-Encoding": "gzip",
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.File(filepath)
|
||||
// TODO: handle .gz file here
|
||||
http.ServeFile(c.Writer, c.Request, path.Join(fic.FilesDir, strings.TrimPrefix(c.Request.URL.Path, path.Join(baseURL, "files"))))
|
||||
})
|
||||
router.GET("/submissions/*_", func(c *gin.Context) {
|
||||
http.ServeFile(c.Writer, c.Request, path.Join(api.TimestampCheck, strings.TrimPrefix(c.Request.URL.Path, path.Join(baseURL, "submissions"))))
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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")
|
||||
|
|
|
@ -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">⬌</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>
|
|
@ -73,29 +73,12 @@
|
|||
</div>
|
||||
<div class="col-4">
|
||||
<div ng-controller="ExerciceFlagDepsController" ng-init="init(flag)">
|
||||
<strong>Dépendances :</strong>
|
||||
Dépendances :
|
||||
<ul ng-if="deps.length > 0">
|
||||
<dependancy ng-repeat="dep in deps" dep="dep"></dependancy>
|
||||
</ul>
|
||||
<span ng-if="deps.length == 0"> sans</span>
|
||||
</div>
|
||||
<hr>
|
||||
<div ng-controller="ExerciceFlagStatsController" ng-init="init(flag)">
|
||||
<strong>Statistiques</strong>
|
||||
<ul>
|
||||
<li>ID : {{ flag.id }}</li>
|
||||
<li>Validés : {{ stats["completed"] }}</li>
|
||||
<li>
|
||||
Tentés : {{ stats["tries"] }}
|
||||
<button type="button" ng-click="deleteTries()" class="btn btn-sm btn-danger" ng-if="stats['tries'] > 0"><span class="glyphicon glyphicon-trash" aria-hidden="true"></span></button>
|
||||
</li>
|
||||
<li>
|
||||
Équipes :
|
||||
<span ng-if="stats['teams'].length == 0">aucune</span>
|
||||
<team-link ng-repeat="team in stats['teams']" id-team="team"></team-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4" ng-controller="ExerciceFlagChoicesController">
|
||||
<div class="btn-toolbar justify-content-end mb-2" role="toolbar">
|
||||
|
@ -185,23 +168,6 @@
|
|||
<dependancy ng-repeat="dep in deps" dep="dep"></dependancy>
|
||||
</ul>
|
||||
<span ng-if="deps.length == 0"> sans</span>
|
||||
<hr>
|
||||
<div ng-controller="ExerciceMCQStatsController" ng-init="init(q)">
|
||||
<strong>Statistiques</strong>
|
||||
<ul>
|
||||
<li>ID : {{ q.id }}</li>
|
||||
<li>Validés : {{ stats["completed"] }}</li>
|
||||
<li>
|
||||
Tentés : {{ stats["tries"] }}
|
||||
<button type="button" ng-click="deleteTries()" class="btn btn-sm btn-danger" ng-if="stats['tries'] > 0"><span class="glyphicon glyphicon-trash" aria-hidden="true"></span></button>
|
||||
</li>
|
||||
<li>
|
||||
Équipes :
|
||||
<span ng-if="stats['teams'].length == 0">aucune</span>
|
||||
<team-link ng-repeat="team in stats['teams']" id-team="team"></team-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -10,27 +10,11 @@
|
|||
<div class="ml-auto d-flex flex-row-reverse text-nowrap">
|
||||
<a href="exercices/{{exercice.id}}/resolution" ng-disabled="!exercice.videoURI" class="ml-2 btn btn-sm btn-info"><span class="glyphicon glyphicon-facetime-video" aria-hidden="true"></span> Vidéo</a>
|
||||
<a href="exercices/{{exercice.id}}/flags" class="ml-2 btn btn-sm btn-success"><span class="glyphicon glyphicon-flag" aria-hidden="true"></span> Flags</a>
|
||||
<div class="btn-group ml-2" role="group">
|
||||
<button type="button" ng-click="syncExo()" ng-class="{'disabled': inSync}" class="btn btn-sm btn-light"><span class="glyphicon glyphicon-refresh" aria-hidden="true"></span> Synchroniser</button>
|
||||
<button type="button" ng-click="checkExoSync()" ng-class="{'disabled': inSync}" class="btn btn-sm btn-light" title="Exporter l'exercice actuel"><span class="glyphicon glyphicon-thumbs-up" aria-hidden="true"></span></button>
|
||||
</div>
|
||||
<button type="button" ng-click="syncExo()" ng-class="{'disabled': inSync}" class="ml-2 btn btn-sm btn-light"><span class="glyphicon glyphicon-refresh" aria-hidden="true"></span> Synchroniser</button>
|
||||
<a href="{{exercice.forge_link}}" target="_blank" class="ml-2 btn btn-sm btn-dark" ng-if="exercice.forge_link"><span class="glyphicon glyphicon-folder-open" aria-hidden="true"></span> Voir sur la forge</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="diff">
|
||||
<h3>Différences par rapport au dépôt</h3>
|
||||
<div ng-repeat="diffline in diff" class="row">
|
||||
<a ng-href="{{ diffline.link }}" class="col-2 d-flex justify-content-end align-items-center text-monospace" title="{{ diffline.field }}">{{ diffline.field }}</a>
|
||||
<div class="col">
|
||||
<div class="text-danger"><span class="text-monospace">-</span>{{ diffline.be }}</div>
|
||||
<div class="text-success"><span class="text-monospace">+</span>{{ diffline.af }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr ng-if="diff" class="my-3">
|
||||
|
||||
<div class="row mb-5">
|
||||
|
||||
<form class="col-md-8" ng-submit="saveExercice()">
|
||||
|
@ -116,7 +100,7 @@
|
|||
<form ng-submit="saveFile()" class="list-group-item bg-light text-dark" ng-repeat="file in files">
|
||||
<div class="row form-group">
|
||||
<input type="text" ng-model="file.name" class="col form-control form-control-sm" placeholder="Nom de fichier">
|
||||
<a href="../files{{file.path}}" target="_self" class="btn btn-sm btn-secondary col-auto"><span class="glyphicon glyphicon-download" aria-hidden="true"></span></a>
|
||||
<a href="../files{{file.path}}" class="btn btn-sm btn-secondary col-auto"><span class="glyphicon glyphicon-download" aria-hidden="true"></span></a>
|
||||
<button type="submit" class="btn btn-sm btn-success col-auto"><span class="glyphicon glyphicon-ok" aria-hidden="true"></span></button><br>
|
||||
<button type="button" ng-click="deleteFile()" class="btn btn-sm btn-danger col-auto"><span class="glyphicon glyphicon-remove" aria-hidden="true"></span></button>
|
||||
</div>
|
||||
|
@ -347,8 +331,7 @@
|
|||
<a href="exercices/{{ row.primary }}#quizz-{{ row.secondary }}" ng-if="row.kind == 'mcq_found'">{{ row.secondary_title }}</a>
|
||||
<a href="exercices/{{ row.primary }}#hint-{{ row.secondary }}" ng-if="row.kind == 'hint'">{{ row.secondary_title }}</a>
|
||||
</span>
|
||||
<span ng-if="!row.secondary_title && row.secondary && row.kind != 'solved' && row.kind != 'tries'">: {{ row.secondary }}</span>
|
||||
<span ng-if="!row.secondary_title && row.secondary && row.kind == 'tries'" ng-controller="SearchTryController"><br><span ng-repeat="line in tr.details"><span ng-if="!$first">, </span>{{ line.kind }}<span ng-if="line.related">#{{ line.related }}</span></span></span>
|
||||
<span ng-if="!row.secondary_title && row.secondary && row.kind != 'solved'">: {{ row.secondary }}</span>
|
||||
</td>
|
||||
<td style="vertical-align: middle; padding: 0; background-color: {{ row.team_color }}" ng-show="logged">
|
||||
<button type="button" data-toggle="modal" data-target="#updHistory" ng-if="row.kind != 'flag_found' && row.kind != 'tries' && row.kind != 'mcq_found'" data-idteam="{{ row.team_id }}" data-kind="{{ row.kind }}" data-time="{{ row.time }}" data-secondary="{{ row.secondary }}" data-coeff="{{ row.coefficient }}" class="float-right btn btn-sm btn-info"><span class="glyphicon glyphicon-edit" aria-hidden="true"></span></button>
|
||||
|
|
|
@ -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> : <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> : <span class="text-monospace">{{ exercice.path }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
|
@ -4,7 +4,6 @@
|
|||
<small class="text-muted" ng-if="errzip > 0"><span class="glyphicon glyphicon-exclamation-sign"></span> <ng-pluralize count="errzip" when="{'one': '{} décompression problématique', 'other': '{} décompressions problématiques'}"></ng-pluralize></small>
|
||||
<button type="button" ng-click="checksumAll()" class="float-right btn btn-sm" ng-class="{'btn-secondary': errfnd === null, 'btn-success': errfnd === 0, 'btn-danger': errfnd > 0}"><span class="glyphicon glyphicon-flash" aria-hidden="true"></span> Vérifier les fichiers</button>
|
||||
<button type="button" ng-click="gunzipFiles()" class="float-right btn btn-sm mx-1" ng-class="{'btn-secondary': errzip === null, 'btn-success': errzip === 0, 'btn-danger': errzip > 0}" title="Décompresse tous les fichiers compressés afin d'afficher la bonne taille au moment du téléchargement"><span class="glyphicon glyphicon-compressed" aria-hidden="true"></span> Gunzip</button>
|
||||
<button type="button" ng-click="clearFilesDir()" class="float-right btn btn-danger btn-sm mx-1" title="Supprime l'arborescence des fichiers"><span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Nuke files</button>
|
||||
</h2>
|
||||
|
||||
<p><input type="search" class="form-control" placeholder="Search" ng-model="query" autofocus></p>
|
||||
|
@ -30,13 +29,13 @@
|
|||
<div class="spinner-border spinner-border-sm" role="status" ng-if="file.gunzipWIP"></div>
|
||||
</button>
|
||||
</td>
|
||||
<td ng-repeat="field in fields" class="text-truncate" style="max-width: 30vw" title="{{ file[field] }}">
|
||||
<td ng-repeat="field in fields">
|
||||
{{ file[field] }}
|
||||
<span ng-if="field == 'id' && file.err !== undefined && file.err !== true" title="{{ file.err }}" class="glyphicon glyphicon-exclamation-sign"></span>
|
||||
</td>
|
||||
<td style="max-width: 100px">
|
||||
<div class="text-truncate" title="{{ file.checksum | bto16 }}">{{ file.checksum | bto16 }}</div>
|
||||
<div class="text-truncate" ng-if="file.checksum_shown" title="{{ file.checksum_shown | bto16 }}">{{ file.checksum_shown | bto16 }}</div>
|
||||
<td>
|
||||
{{ file.checksum | bto16 }}
|
||||
<div ng-if="file.checksum_shown">{{ file.checksum_shown | bto16 }}</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
|
|
@ -9,20 +9,16 @@
|
|||
<tr>
|
||||
<th>Chemin</th>
|
||||
<th>Branche</th>
|
||||
<th>Commit <span class="text-muted">Plus récent</span></th>
|
||||
<th>Commit</th>
|
||||
<th>Plus récent</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="repository in repositories">
|
||||
<td>{{ repository.path }}</td>
|
||||
<td>{{ repository.branch }}</td>
|
||||
<td>
|
||||
{{ repository.hash }}<br>
|
||||
<repository-uptodate repository="repository" />
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" ng-click="deleteRepository(repository)" class="btn btn-sm btn-danger"><span class="glyphicon glyphicon-trash" aria-hidden="true"></span></button>
|
||||
</td>
|
||||
<td>{{ repository.hash }}</td>
|
||||
<td><repository-uptodate repository="repository" /></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
<label for="startTime" class="col-sm-3 col-form-label col-form-label-sm" ng-class="{'text-primary font-weight-bold': config.start != dist_config.start}">Début du challenge</label>
|
||||
<div class="col-sm-9">
|
||||
<div class="input-group">
|
||||
<input type="datetime-local" class="form-control form-control-sm" id="startTime" ng-model="config.start" ng-change="durationChange()" ng-class="{'border-primary': config.start != dist_config.start}">
|
||||
<input type="datetime-local" class="form-control form-control-sm" id="startTime" ng-model="config.start" ng-class="{'border-primary': config.start != dist_config.start}">
|
||||
<div class="input-group-append">
|
||||
<button ng-click="launchChallenge()" class="btn btn-sm btn-secondary" type="button"><span class="glyphicon glyphicon-play" aria-hidden="true"></span> Lancer le challenge</button>
|
||||
</div>
|
||||
|
@ -46,14 +46,14 @@
|
|||
<div class="form-group row">
|
||||
<label for="endTime" class="col-sm-3 col-form-label col-form-label-sm" ng-class="{'text-primary font-weight-bold': config.end != dist_config.end}">Fin du challenge</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="datetime-local" class="form-control form-control-sm" id="endTime" ng-model="config.end" ng-change="durationChange(true)" ng-class="{'border-primary': config.end != dist_config.end}">
|
||||
<input type="datetime-local" class="form-control form-control-sm" id="endTime" ng-model="config.end" ng-class="{'border-primary': config.end != dist_config.end}">
|
||||
</div>
|
||||
<div class="col-sm-1 text-right">
|
||||
<label for="duration" class="col-form-label col-form-label-sm">Durée</label>
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="number" class="form-control form-control-sm" id="duration" ng-model="duration" ng-change="durationChange()" integer>
|
||||
<input type="text" class="form-control form-control-sm" id="duration" ng-model="duration" integer>
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text">min</span>
|
||||
</div>
|
||||
|
@ -64,60 +64,46 @@
|
|||
<hr>
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="col-sm row">
|
||||
<label for="globalScoreCoefficient" class="col-form-label col-form-label-sm" ng-class="{'text-primary': config.globalScoreCoefficient != dist_config.globalScoreCoefficient}"><strong>Coefficients</strong></label>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control form-control-sm" id="globalScoreCoefficient" ng-model="config.globalScoreCoefficient" float title="Coefficient multiplicateur global du score final (le coefficient est appliqué dans la fonction et vaut pour tout le challenge, présent/passé/futur, sans effet de bord)" ng-class="{'border-primary': config.globalScoreCoefficient != dist_config.globalScoreCoefficient}">
|
||||
</div>
|
||||
<label for="globalScoreCoefficient" class="col-sm-2 col-form-label col-form-label-sm" ng-class="{'text-primary': config.globalScoreCoefficient != dist_config.globalScoreCoefficient}"><strong>Coefficients</strong></label>
|
||||
<div class="col-sm-1">
|
||||
<input type="text" class="form-control form-control-sm" id="globalScoreCoefficient" ng-model="config.globalScoreCoefficient" float title="Coefficient multiplicateur global du score final (le coefficient est appliqué dans la fonction et vaut pour tout le challenge, présent/passé/futur, sans effet de bord)" ng-class="{'border-primary': config.globalScoreCoefficient != dist_config.globalScoreCoefficient}">
|
||||
</div>
|
||||
|
||||
<div class="col-sm row">
|
||||
<label for="hintcoefficient" class="col-form-label col-form-label-sm text-right" ng-class="{'text-primary font-weight-bold': config.hintCurrentCoefficient != dist_config.hintCurrentCoefficient}">incides</label>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control form-control-sm" id="hintcoefficient" ng-model="config.hintCurrentCoefficient" float title="Coefficient multiplicateur temporaire du nombre de points que fait perdre un indice (le coefficient est enregistré au moment où l'équipe demande un indice, ce n'est pas global)" ng-class="{'border-primary': config.hintCurrentCoefficient != dist_config.hintCurrentCoefficient}">
|
||||
</div>
|
||||
<label for="hintcoefficient" class="col-sm-2 col-form-label col-form-label-sm text-right" ng-class="{'text-primary font-weight-bold': config.hintCurrentCoefficient != dist_config.hintCurrentCoefficient}">incides</label>
|
||||
<div class="col-sm-1">
|
||||
<input type="text" class="form-control form-control-sm" id="hintcoefficient" ng-model="config.hintCurrentCoefficient" float title="Coefficient multiplicateur temporaire du nombre de points que fait perdre un indice (le coefficient est enregistré au moment où l'équipe demande un indice, ce n'est pas global)" ng-class="{'border-primary': config.hintCurrentCoefficient != dist_config.hintCurrentCoefficient}">
|
||||
</div>
|
||||
|
||||
<div class="col-sm row">
|
||||
<label for="wchoicescoefficient" class="col-form-label col-form-label-sm text-right" ng-class="{'text-primary font-weight-bold': config.wchoiceCurrentCoefficient != dist_config.wchoiceCurrentCoefficient}">WChoices</label>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control form-control-sm" id="wchoicescoefficient" ng-model="config.wchoiceCurrentCoefficient" float title="Coefficient multiplicateur temporaire du nombre de points que fait perdre une demande de liste de choix (le coefficient est enregistré au moment où l'équipe demande la liste de choix, ce n'est pas global)" ng-class="{'border-primary': config.wchoiceCurrentCoefficient != dist_config.wchoiceCurrentCoefficient}">
|
||||
</div>
|
||||
<label for="wchoicescoefficient" class="col-sm-2 col-form-label col-form-label-sm text-right" ng-class="{'text-primary font-weight-bold': config.wchoiceCurrentCoefficient != dist_config.wchoiceCurrentCoefficient}">WChoices</label>
|
||||
<div class="col-sm-1">
|
||||
<input type="text" class="form-control form-control-sm" id="wchoicescoefficient" ng-model="config.wchoiceCurrentCoefficient" float title="Coefficient multiplicateur temporaire du nombre de points que fait perdre une demande de liste de choix (le coefficient est enregistré au moment où l'équipe demande la liste de choix, ce n'est pas global)" ng-class="{'border-primary': config.wchoiceCurrentCoefficient != dist_config.wchoiceCurrentCoefficient}">
|
||||
</div>
|
||||
|
||||
<div class="col-sm row">
|
||||
<label for="exercicecurcoefficient" class="col-form-label col-form-label-sm text-right" ng-class="{'text-primary font-weight-bold': config.exerciceCurrentCoefficient != dist_config.exerciceCurrentCoefficient}">défis</label>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control form-control-sm" id="exercicecurcoefficient" ng-model="config.exerciceCurrentCoefficient" float title="Coefficient multiplicateur temporaire du nombre de points que fait gagner un exercice validé (le coefficient est enregistré au moment où l'équipe valide l'exercice, ce n'est pas global)" ng-class="{'border-primary': config.exerciceCurrentCoefficient != dist_config.exerciceCurrentCoefficient}">
|
||||
</div>
|
||||
<label for="exercicecurcoefficient" class="col-sm-2 col-form-label col-form-label-sm text-right" ng-class="{'text-primary font-weight-bold': config.exerciceCurrentCoefficient != dist_config.exerciceCurrentCoefficient}">défis</label>
|
||||
<div class="col-sm-1">
|
||||
<input type="text" class="form-control form-control-sm" id="exercicecurcoefficient" ng-model="config.exerciceCurrentCoefficient" float title="Coefficient multiplicateur temporaire du nombre de points que fait gagner un exercice validé (le coefficient est enregistré au moment où l'équipe valide l'exercice, ce n'est pas global)" ng-class="{'border-primary': config.exerciceCurrentCoefficient != dist_config.exerciceCurrentCoefficient}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row" title="Attribuer ce pourcentage de points bonus supplémentaire à la première équipe qui valide un exercice">
|
||||
<div class="form-group row">
|
||||
<div class="col-sm row">
|
||||
<label for="firstBlood" class="col-sm-8 col-form-label col-form-label-sm" ng-class="{'text-primary font-weight-bold': config.firstBlood != dist_config.firstBlood}">Premier sang</label>
|
||||
<label for="firstBlood" class="col-sm-8 col-form-label col-form-label-sm" ng-class="{'text-primary font-weight-bold': config.firstBlood != dist_config.firstBlood}">Bonus premier sang</label>
|
||||
<div class="col-sm-4">
|
||||
<input type="text" class="form-control form-control-sm" id="firstBlood" ng-model="config.firstBlood" float ng-class="{'border-primary': config.firstBlood != dist_config.firstBlood}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm row" title="Pour chaque validation supplémentaire d'un exercice donné, on retire ce pourcentage de points à l'exercice. Les points rapportés par un exercice sont alors dynamiques : ils baissent pour toutes les équipes y compris celles ayant validé cet exercice il y a longtemps.">
|
||||
<label for="discountFactor" class="col-sm-8 col-form-label col-form-label-sm text-truncate" ng-class="{'text-primary font-weight-bold': config.discountedFactor != dist_config.discountedFactor}">Décote exercices</label>
|
||||
<div class="col-sm row">
|
||||
<label for="discountFactor" class="col-sm-8 col-form-label col-form-label-sm" ng-class="{'text-primary font-weight-bold': config.discountedFactor != dist_config.discountedFactor}">Décote des exercices</label>
|
||||
<div class="col-sm-4">
|
||||
<input type="text" class="form-control form-control-sm" id="discountFactor" ng-model="config.discountedFactor" float ng-class="{'border-primary': config.discountedFactor != dist_config.discountedFactor}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm row" title="Coefficient de base retiré pour chaque soumission invalide au delà de 10 soumissions">
|
||||
<label for="submissionCostBase" class="col-sm-8 col-form-label col-form-label-sm text-right" ng-class="{'text-primary font-weight-bold': config.submissionCostBase != dist_config.submissionCostBase}">Coût tentative</label>
|
||||
<div class="col-sm row">
|
||||
<label for="submissionCostBase" class="col-sm-8 col-form-label col-form-label-sm text-right" ng-class="{'text-primary font-weight-bold': config.submissionCostBase != dist_config.submissionCostBase}">Coût de base tentative</label>
|
||||
<div class="col-sm-4">
|
||||
<input type="text" class="form-control form-control-sm" id="submissionCostBase" ng-model="config.submissionCostBase" float ng-class="{'border-primary': config.submissionCostBase != dist_config.submissionCostBase}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm row" title="Accorder des points aux exercices partiellement résolu, par questions validée. Ce champ est le pourcentage de points que peut rapporter la complétion de toutes les questions d'un exercice. Par exemple avec 25%, un exercice avec 10 questions, chaque question validée rapportera GAIN * 25% / 10">
|
||||
<label for="questionGainRatio" class="col-sm-8 col-form-label col-form-label-sm text-right text-truncate" ng-class="{'text-primary font-weight-bold': config.questionGainRatio != dist_config.questionGainRatio}">Gain par questions</label>
|
||||
<div class="col-sm-4">
|
||||
<input type="text" class="form-control form-control-sm" id="questionGainRatio" ng-model="config.questionGainRatio" float ng-class="{'border-primary': config.questionGainRatio != dist_config.questionGainRatio}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
@ -320,7 +306,7 @@
|
|||
</div>
|
||||
</form>
|
||||
|
||||
<div class="card my-3">
|
||||
<form ng-submit="addDelegatedQA()" class="card my-3">
|
||||
<div class="card-header">
|
||||
<h3>Managers QA</h3>
|
||||
</div>
|
||||
|
@ -332,23 +318,22 @@
|
|||
<span class="glyphicon glyphicon-remove" aria-hidden="true"></span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="row">
|
||||
<div class="col" ng-controller="AllTeamAssociationsController">
|
||||
<select class="form-control form-control-sm" ng-model="newdqa">
|
||||
<option ng-repeat="(i,m) in allAssociations" ng-value="m">{{ m }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col input-group">
|
||||
<input type="text" class="form-control form-control-sm" ng-model="newdqa" placeholder="Nouveau manager QA">
|
||||
<span class="input-group-append">
|
||||
<button class="btn btn-sm btn-success" ng-disabled="!newdqa.length"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span></button>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<form class="row" ng-controller="AllTeamAssociationsController" ng-submit="addDelegatedQA()">
|
||||
<div class="col">
|
||||
<select class="form-control form-control-sm" ng-model="newdqa">
|
||||
<option ng-selected="newdqa == m.association" ng-repeat="(i,m) in allAssociations" ng-value="m.association">{{ m.association }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col input-group">
|
||||
<input type="text" class="form-control form-control-sm" ng-model="newdqa" placeholder="Nouveau manager QA">
|
||||
<span class="input-group-append">
|
||||
<button class="btn btn-sm btn-success" ng-disabled="!newdqa.length"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span></button>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form ng-submit="saveChallengeInfo()" class="card my-3">
|
||||
<div class="card-header">
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
<div class="badge badge-success align-self-center" ng-if="syncReport" title="{{ syncReport._updated[syncReport._updated.length-1] }}">
|
||||
Dernier import : {{ syncReport._updated[syncReport._updated.length-1] | date:"medium" }}
|
||||
</div>
|
||||
<a ng-if="syncStatus['sync-type'] === 'GitImporter'" href="repositories" class="btn btn-secondary">
|
||||
<a ng-if="configro['sync-type'] === 'GitImporter'" href="repositories" class="btn btn-secondary">
|
||||
Voir les dépôts
|
||||
</a>
|
||||
</div>
|
||||
|
@ -32,25 +32,17 @@
|
|||
<div class="card-body">
|
||||
<dl class="row">
|
||||
<dt class="col-2">Type</dt>
|
||||
<dd class="col-10" ng-bind="syncStatus['sync-type']"></dd>
|
||||
<dd class="col-10" ng-bind="configro['sync-type']"></dd>
|
||||
<dt class="col-2">Synchronisation</dt>
|
||||
<dd class="col-10" title="{{ syncStatus['sync'] }}" ng-bind="syncStatus.sync"></dd>
|
||||
<dt class="col-2" ng-if="syncStatus['sync-id']">ID</dt>
|
||||
<dd class="col-10" ng-if="syncStatus['sync-id']">
|
||||
{{ syncStatus['sync-id'] }}
|
||||
<button ng-if="syncStatus['sync-type'] === 'GitImporter'" type="button" class="btn btn-sm btn-dark" ng-click="baseSync()" ng-disabled="deepSyncInProgress"><span class="glyphicon glyphicon-repeat" aria-hidden="true"></span> Pull</button>
|
||||
</dd>
|
||||
<dt class="col-2" ng-if="syncStatus['sync']">Statut</dt>
|
||||
<dd class="col-10" ng-if="syncStatus['sync']">
|
||||
<span ng-if="(syncStatus.syncMutex !== undefined && syncStatus.syncMutex) || (syncPercent > 0 && syncPercent < 100)">{{ syncPercent }} %</span>
|
||||
<span class="badge badge-pill" ng-class="{'badge-success': !syncStatus.pullMutex, 'badge-danger': syncStatus.pullMutex}" ng-if="syncStatus.pullMutex !== undefined">Pull</span>
|
||||
<span class="badge badge-pill" ng-class="{'badge-success': !syncStatus.syncMutex, 'badge-danger': syncStatus.syncMutex}" ng-if="syncStatus.syncMutex !== undefined">Synchronisation</span>
|
||||
</dd>
|
||||
<dd class="col-10" title="{{ configro['sync'] }}" ng-bind="configro.sync"></dd>
|
||||
<dt class="col-2" ng-if="configro['sync-id']">ID</dt>
|
||||
<dd class="col-10" ng-if="configro['sync-id']">{{ configro['sync-id'] }}</dd>
|
||||
<dt class="col-2" ng-if="configro['sync']">Statut</dt>
|
||||
<dd class="col-10" ng-if="configro['sync']">{{ syncProgress }}</dd>
|
||||
</dl>
|
||||
|
||||
<pre style="background: black; color: white;" class="p-2" ng-if="syncStatus.lastError">{{ syncStatus.lastError }}</pre>
|
||||
|
||||
<div class="d-flex justify-content-around" ng-if="syncStatus.sync">
|
||||
<div class="d-flex justify-content-around" ng-if="configro.sync">
|
||||
<button ng-if="configro['sync-type'] === 'GitImporter'" type="button" class="btn btn-info" ng-click="baseSync()" ng-disabled="deepSyncInProgress"><span class="glyphicon glyphicon-repeat" aria-hidden="true"></span> Pull</button>
|
||||
<div class="btn-group dropright">
|
||||
<button type="button" class="btn btn-secondary" ng-click="deepSync()" ng-disabled="deepSyncInProgress"><span class="glyphicon glyphicon-import" aria-hidden="true"></span> Synchronisation intégrale</button>
|
||||
<button type="button" class="btn btn-secondary dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" ng-disabled="deepSyncInProgress">
|
||||
|
@ -58,6 +50,7 @@
|
|||
</button>
|
||||
<div class="dropdown-menu" ng-controller="ThemesListController" style="max-height: 45vh; overflow: auto">
|
||||
<a class="dropdown-item" ng-click="deepSync(theme)" ng-repeat="theme in themes" ng-bind="theme.name"></a>
|
||||
<a class="dropdown-item" ng-click="deepSync({name: 'Exercices indépendants', id: 0})">Exercices indépendants</a>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-secondary" ng-click="speedyDeepSync()" ng-disabled="deepSyncInProgress"><span class="glyphicon glyphicon-import" aria-hidden="true"></span> Synchronisation sans fichiers</button>
|
||||
|
@ -119,41 +112,3 @@
|
|||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-5" ng-controller="ThemesListController">
|
||||
<div class="card-header">
|
||||
<button type="button" class="btn btn-primary float-right mx-1" ng-click="diffWithRepo()" title="Calculer les différences avec le dépôt">
|
||||
<span class="glyphicon glyphicon-refresh" aria-hidden="true"></span>
|
||||
</button>
|
||||
<h3 class="mb-0">
|
||||
Différences avec le dépôts
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body" ng-if="!diff">
|
||||
<div class="alert alert-info">Lancez la génération du rapport pour lister les différences.</div>
|
||||
</div>
|
||||
<div ng-repeat="(th, lines) in diff" class="card-body" ng-if="diff">
|
||||
<div class="d-flex">
|
||||
<h3>
|
||||
{{ th }}
|
||||
</h3>
|
||||
<div class="d-inline-block" ng-repeat="theme in themes" ng-if="theme.name == th">
|
||||
<a href="themes/{{ theme.id }}" class="btn btn-link" title="Voir le thème">
|
||||
<span class="glyphicon glyphicon-hand-right" aria-hidden="true"></span>
|
||||
</a>
|
||||
<button class="btn btn-light" title="Resynchroniser uniquement ce thème" ng-click="deepSync(theme)" ng-if="settings.wip || !timeProgression || displayDangerousActions">
|
||||
<span class="glyphicon glyphicon-hdd" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ul>
|
||||
<li ng-repeat="diffline in lines" class="row">
|
||||
<a ng-href="{{ diffline.link }}" class="col-2 d-flex align-items-center text-truncate text-monospace" title="{{ diffline.field }}">{{ diffline.field }}</a>
|
||||
<div class="col">
|
||||
<div class="text-danger"><span class="text-monospace">-</span>{{ diffline.be }}</div>
|
||||
<div class="text-success"><span class="text-monospace">+</span>{{ diffline.af }}</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,19 +1,11 @@
|
|||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h2>
|
||||
Équipes
|
||||
</h2>
|
||||
<div>
|
||||
<button type="button" ng-click="show('new')" class="btn btn-sm btn-primary ml-1"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Ajouter une équipe</button>
|
||||
<form class="d-inline">
|
||||
<input id="crTeamsInput" type="file" file-model="selectedFile" class="d-none" />
|
||||
<button type="button" ng-click="triggerTeamsImport()" class="btn btn-sm btn-secondary ml-1"><span class="glyphicon glyphicon-import" aria-hidden="true"></span> Import Cyberrange</button>
|
||||
</form>
|
||||
<button type="button" ng-click="show('print')" class="btn btn-sm btn-secondary ml-1" title="Imprimer les équipes et leurs membres"><span class="glyphicon glyphicon-print" aria-hidden="true"></span></button>
|
||||
<button type="button" ng-click="refineTeamsColors()" class="btn btn-sm btn-secondary ml-1" title="Réarranger automatiquement les couleurs des équipes pour maximiser le spectre utilisé"><span class="glyphicon glyphicon-adjust" aria-hidden="true"></span></button>
|
||||
<button type="button" ng-click="show('export')" class="btn btn-sm btn-secondary ml-1"><span class="glyphicon glyphicon-export" aria-hidden="true"></span> Statistiques générales</button>
|
||||
<button type="button" ng-click="desactiveTeams()" class="btn btn-sm btn-danger ml-1" title="Cliquer pour marquer les équipes sans certificat comme inactives (et ainsi éviter que ses fichiers ne soient générés)"><span class="glyphicon glyphicon-leaf" aria-hidden="true"></span> Désactiver les équipes inactives</button>
|
||||
</div>
|
||||
</div>
|
||||
<h2>
|
||||
Équipes
|
||||
<button type="button" ng-click="show('new')" class="float-right btn btn-sm btn-primary"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Ajouter une équipe</button>
|
||||
<button type="button" ng-click="show('print')" class="float-right btn btn-sm btn-secondary mr-2"><span class="glyphicon glyphicon-print" aria-hidden="true"></span> Imprimer les équipes</button>
|
||||
<button type="button" ng-click="show('export')" class="float-right btn btn-sm btn-secondary mr-2"><span class="glyphicon glyphicon-export" aria-hidden="true"></span> Statistiques générales</button>
|
||||
<button type="button" ng-click="desactiveTeams()" class="float-right btn btn-sm btn-danger mr-2" title="Cliquer pour marquer les équipes sans certificat comme inactives (et ainsi éviter que ses fichiers ne soient générés)"><span class="glyphicon glyphicon-leaf" aria-hidden="true"></span> Désactiver les équipes inactives</button>
|
||||
<button type="button" ng-click="genDexCfg()" class="float-right btn btn-sm btn-success mr-2"><span class="glyphicon glyphicon-refresh" aria-hidden="true"></span> DexIdP</button>
|
||||
</h2>
|
||||
|
||||
<p><input type="search" class="form-control" placeholder="Search" ng-model="query" ng-keypress="validateSearch($event)" autofocus></p>
|
||||
<table class="table table-hover table-bordered table-striped table-sm">
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
<th>Date</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="row in scores | filter: query | orderBy:'time'" ng-class="{'bg-danger': row.reason == 'Bonus flag', 'bg-ffound': row.reason == 'First blood', 'bg-wchoices': row.reason == 'Display choices', 'bg-success': row.reason == 'Validation', 'bg-info': row.reason == 'Hint', 'bg-secondary': row.reason.startsWith('Response '), 'bg-warning': row.reason == 'Tries'}">
|
||||
<tr ng-repeat="row in scores | filter: query | orderBy:'time'" ng-class="{'bg-danger': row.reason == 'Bonus flag', 'bg-ffound': row.reason == 'First blood', 'bg-wchoices': row.reason == 'Display choices', 'bg-success': row.reason == 'Validation', 'bg-info': row.reason == 'Hint', 'bg-warning': row.reason == 'Tries'}">
|
||||
<td>
|
||||
<a ng-repeat="exercice in exercices" ng-if="exercice.id == row.id_exercice" href="exercices/{{ row.id_exercice }}">{{ exercice.title }}</a>
|
||||
</td>
|
||||
|
@ -23,12 +23,9 @@
|
|||
<td>
|
||||
{{ row.points * row.coeff }}
|
||||
</td>
|
||||
<td ng-if="!row.reason.startsWith('Response ')">
|
||||
<td>
|
||||
{{ row.points }} * {{ row.coeff }}
|
||||
</td>
|
||||
<td ng-if="row.reason.startsWith('Response ')">
|
||||
{{ row.points }} * {{ settings.questionGainRatio }} / {{ settings.questionGainRatio / row.coeff }}
|
||||
</td>
|
||||
<td>
|
||||
<nobr title="{{ row.time }}">{{ row.time | date:"mediumTime" }}</nobr>
|
||||
</td>
|
||||
|
@ -36,7 +33,7 @@
|
|||
<tfoot>
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th>{{ my.score100 / 100 }}</th>
|
||||
<th>{{ my.score }}</th>
|
||||
</thead>
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
<dt ng-bind="theme.name"></dt>
|
||||
<dd>
|
||||
<ul class="list-unstyled">
|
||||
<li ng-repeat="exercice in theme.exercices" ng-if="my.exercices[exercice.id] && my.exercices[exercice.id].solved_rank"><a href="/{{ my.exercices[exercice.id].theme_id }}/{{ exercice.id }}" target="_blank"><abbr title="{{ my.exercices[exercice.id].statement }}">{{ exercice.title }}</abbr></a> (<abbr title="{{ my.exercices[exercice.id].solved_time | date:'mediumDate' }} à {{ my.exercices[exercice.id].solved_time | date:'mediumTime' }}">{{ my.exercices[exercice.id].solved_rank }}<sup>e</sup></abbr>)</li>
|
||||
<li ng-repeat="(eid,exercice) in theme.exercices" ng-if="my.exercices[eid] && my.exercices[eid].solved_rank"><a href="/{{ my.exercices[eid].theme_id }}/{{ eid }}" target="_blank"><abbr title="{{ my.exercices[eid].statement }}">{{ exercice.title }}</abbr></a> (<abbr title="{{ my.exercices[eid].solved_time | date:'mediumDate' }} à {{ my.exercices[eid].solved_time | date:'mediumTime' }}">{{ my.exercices[eid].solved_rank }}<sup>e</sup></abbr>)</li>
|
||||
</ul>
|
||||
</dd>
|
||||
</div>
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
Thèmes
|
||||
<button type="button" ng-click="show('new')" class="float-right btn btn-sm btn-primary ml-2"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Ajouter un thème</button>
|
||||
<button type="button" ng-click="sync()" ng-class="{'disabled': inSync}" class="float-right btn btn-sm btn-secondary ml-2"><span class="glyphicon glyphicon-refresh" aria-hidden="true"></span> Synchroniser</button>
|
||||
<a href="forge-links" class="float-right btn btn-sm btn-dark ml-2"><span class="glyphicon glyphicon-folder-open" aria-hidden="true"></span> Liens d'accès à la forge</a>
|
||||
</h2>
|
||||
|
||||
<p><input type="search" class="form-control" placeholder="Search" ng-model="query" ng-keypress="validateSearch($event)" autofocus></p>
|
||||
|
|
|
@ -7,21 +7,8 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="diff">
|
||||
<h3>Différences par rapport au dépôt</h3>
|
||||
<div ng-repeat="diffline in diff" class="row">
|
||||
<a ng-href="{{ diffline.link }}" class="col-3 d-flex align-items-center text-truncate text-monospace" title="{{ diffline.field }}">{{ diffline.field }}</a>
|
||||
<div class="col">
|
||||
<div class="text-danger"><span class="text-monospace">-</span>{{ diffline.be }}</div>
|
||||
<div class="text-success"><span class="text-monospace">+</span>{{ diffline.af }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr ng-if="diff" class="my-3">
|
||||
|
||||
<div class="row">
|
||||
<form ng-submit="saveTheme()" class="col-4" ng-if="!(theme.id === 0 && theme.path)">
|
||||
<form ng-submit="saveTheme()" class="col-4">
|
||||
<div ng-class="{'form-group': field != 'locked', 'form-check': field == 'locked'}" ng-repeat="field in fields">
|
||||
<input type="checkbox" class="form-check-input" id="{{ field }}" ng-model="theme[field]" ng-if="field == 'locked'">
|
||||
<label for="{{ field }}">{{ field | capitalize }}</label>
|
||||
|
@ -38,14 +25,11 @@
|
|||
</div>
|
||||
</form>
|
||||
|
||||
<div ng-if="theme.id || theme.path" class="col-md-8" ng-class="{'offset-md-2': theme.id === 0 && theme.path}" ng-controller="ExercicesListController">
|
||||
<div ng-if="theme.id" class="col-md-8" ng-controller="ExercicesListController">
|
||||
<h3>
|
||||
Exercices ({{ exercices.length }})
|
||||
<button type="button" ng-click="show('new')" class="float-right btn btn-sm btn-primary ml-2"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Ajouter un exercice</button>
|
||||
<div class="float-right btn-group ml-2" role="group">
|
||||
<button type="button" ng-click="syncExo()" ng-class="{'disabled': inSync}" class="btn btn-sm btn-light ml-2"><span class="glyphicon glyphicon-refresh" aria-hidden="true"></span> Synchroniser</button>
|
||||
<button type="button" ng-click="checkExoSync()" ng-class="{'disabled': inSync}" class="btn btn-sm btn-light" title="Exporter l'exercice actuel"><span class="glyphicon glyphicon-thumbs-up" aria-hidden="true"></span></button>
|
||||
</div>
|
||||
<button type="button" ng-click="syncExo()" ng-class="{'disabled': inSync}" class="float-right btn btn-sm btn-light ml-2"><span class="glyphicon glyphicon-refresh" aria-hidden="true"></span> Synchroniser</button>
|
||||
</h3>
|
||||
|
||||
<p><input type="search" class="form-control form-control-sm" placeholder="Search" ng-model="query" autofocus></p>
|
||||
|
|
|
@ -23,7 +23,7 @@ func NewThemeError(theme *fic.Theme, err error) *ThemeError {
|
|||
if theme == nil {
|
||||
return &ThemeError{
|
||||
error: err,
|
||||
ThemePath: fic.StandaloneExercicesDirectory,
|
||||
ThemePath: StandaloneExercicesDirectory,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -106,9 +106,9 @@ func NewChallengeTxtError(exercice *fic.Exercice, line uint, err error, theme ..
|
|||
|
||||
func (e *ChallengeTxtError) Error() string {
|
||||
if e.ChallengeTxtLine != 0 {
|
||||
return fmt.Sprintf("%s:%d: %s", path.Join(e.ExercicePath, "challenge.toml"), e.ChallengeTxtLine, e.ThemeError.error.Error())
|
||||
return fmt.Sprintf("%s:%d: %s", path.Join(e.ExercicePath, "challenge.txt"), e.ChallengeTxtLine, e.ThemeError.error.Error())
|
||||
} else {
|
||||
return fmt.Sprintf("%s: %s", path.Join(e.ExercicePath, "challenge.toml"), e.ThemeError.error.Error())
|
||||
return fmt.Sprintf("%s: %s", path.Join(e.ExercicePath, "challenge.txt"), e.ThemeError.error.Error())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -127,7 +127,7 @@ func NewHintError(exercice *fic.Exercice, hint *fic.EHint, line int, err error,
|
|||
}
|
||||
|
||||
func (e *HintError) Error() string {
|
||||
return fmt.Sprintf("%s: hint#%d (%s): %s", path.Join(e.ExercicePath, "challenge.toml"), e.HintId, e.HintTitle, e.ThemeError.error.Error())
|
||||
return fmt.Sprintf("%s: hint#%d (%s): %s", path.Join(e.ExercicePath, "challenge.txt"), e.HintId, e.HintTitle, e.ThemeError.error.Error())
|
||||
}
|
||||
|
||||
type FlagError struct {
|
||||
|
@ -144,5 +144,5 @@ func NewFlagError(exercice *fic.Exercice, flag *ExerciceFlag, line int, err erro
|
|||
}
|
||||
|
||||
func (e *FlagError) Error() string {
|
||||
return fmt.Sprintf("%s: flag#%d: %s", path.Join(e.ExercicePath, "challenge.toml"), e.FlagId, e.ThemeError.error.Error())
|
||||
return fmt.Sprintf("%s: flag#%d: %s", path.Join(e.ExercicePath, "challenge.txt"), e.FlagId, e.ThemeError.error.Error())
|
||||
}
|
||||
|
|
|
@ -26,10 +26,10 @@ const sampleFile = `0-exercice-1/overview.md:spelling:Sterik
|
|||
0-exercice-1/resolution.md:11:typo_guillemets_typographiques_doubles_fermants
|
||||
0-exercice-1/resolution.md:spelling:cronjob
|
||||
0-exercice-1/resolution.md:spelling:Level
|
||||
challenge.toml:spelling:time
|
||||
challenge.toml:spelling:ago
|
||||
challenge.txt:spelling:time
|
||||
challenge.txt:spelling:ago
|
||||
0-exercice-1/resolution.md:spelling:SCL
|
||||
challenge.toml:spelling:SCL`
|
||||
challenge.txt:spelling:SCL`
|
||||
|
||||
func TestLoadExceptions(t *testing.T) {
|
||||
exceptions := ParseExceptionString(sampleFile, nil)
|
||||
|
@ -47,7 +47,7 @@ func TestFilterExceptions(t *testing.T) {
|
|||
t.Fatalf("Expected 1 exceptions, got %d", len(*filteredExceptions))
|
||||
}
|
||||
|
||||
filteredExceptions = exceptions.GetFileExceptions("challenge.toml")
|
||||
filteredExceptions = exceptions.GetFileExceptions("challenge.txt")
|
||||
if len(*filteredExceptions) != 3 {
|
||||
t.Fatalf("Expected 3 exceptions, got %d", len(*filteredExceptions))
|
||||
}
|
||||
|
|
|
@ -173,9 +173,7 @@ func getExerciceParams(i Importer, exercice *fic.Exercice) (params ExerciceParam
|
|||
if params, _, err = parseExerciceParams(i, exercice.Path); err != nil {
|
||||
errs = multierr.Append(errs, NewChallengeTxtError(exercice, 0, err))
|
||||
} else if len(params.Flags) == 0 && len(params.FlagsUCQ) == 0 && len(params.FlagsMCQ) == 0 {
|
||||
if !params.WIP {
|
||||
errs = multierr.Append(errs, NewChallengeTxtError(exercice, 0, fmt.Errorf("has no flag")))
|
||||
}
|
||||
errs = multierr.Append(errs, NewChallengeTxtError(exercice, 0, fmt.Errorf("has no flag")))
|
||||
} else {
|
||||
// Treat legacy UCQ flags as ExerciceFlag
|
||||
for _, flag := range params.FlagsUCQ {
|
||||
|
|
|
@ -60,7 +60,7 @@ func BuildFilesListInto(i Importer, exercice *fic.Exercice, into string) (files
|
|||
// Parse DIGESTS.txt
|
||||
if digs, err := GetFileContent(i, path.Join(exercice.Path, into, "DIGESTS.txt")); err != nil {
|
||||
errs = multierr.Append(errs, NewExerciceError(exercice, fmt.Errorf("unable to read %s: %w", path.Join(into, "DIGESTS.txt"), err)))
|
||||
} else if len(digs) > 0 {
|
||||
} else {
|
||||
digests = map[string][]byte{}
|
||||
for nline, d := range strings.Split(digs, "\n") {
|
||||
if dsplt := strings.SplitN(d, " ", 2); len(dsplt) < 2 {
|
||||
|
@ -315,57 +315,9 @@ func DownloadExerciceFile(pf ExerciceFile, dest string, exercice *fic.Exercice,
|
|||
return
|
||||
}
|
||||
|
||||
type importedFile struct {
|
||||
file interface{}
|
||||
Name string
|
||||
}
|
||||
|
||||
func SyncExerciceFiles(i Importer, exercice *fic.Exercice, paramsFiles map[string]ExerciceFile, actionAfterImport func(fname string, digests map[string][]byte, filePath, origin string) (interface{}, error)) (ret []*importedFile, errs error) {
|
||||
files, digests, berrs := BuildFilesListInto(i, exercice, "files")
|
||||
errs = multierr.Append(errs, berrs)
|
||||
|
||||
// Import standard files
|
||||
for _, fname := range files {
|
||||
var f interface{}
|
||||
var err error
|
||||
|
||||
if pf, exists := paramsFiles[fname]; exists && pf.URL != "" && !i.Exists(path.Join(exercice.Path, "files", fname)) {
|
||||
dest := GetDestinationFilePath(pf.URL, &pf.Filename)
|
||||
|
||||
if _, err := os.Stat(dest); !os.IsNotExist(err) {
|
||||
if d, err := actionAfterImport(fname, digests, dest, pf.URL); err == nil {
|
||||
f = d
|
||||
}
|
||||
}
|
||||
|
||||
if f == nil {
|
||||
errs = multierr.Append(errs, DownloadExerciceFile(paramsFiles[fname], dest, exercice, false))
|
||||
|
||||
f, err = actionAfterImport(fname, digests, dest, pf.URL)
|
||||
}
|
||||
} else {
|
||||
f, err = i.importFile(path.Join(exercice.Path, "files", fname), func(filePath, origin string) (interface{}, error) {
|
||||
return actionAfterImport(fname, digests, filePath, origin)
|
||||
})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
errs = multierr.Append(errs, NewFileError(exercice, fname, err))
|
||||
continue
|
||||
}
|
||||
|
||||
ret = append(ret, &importedFile{
|
||||
f,
|
||||
fname,
|
||||
})
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ImportExerciceFiles reads the content of files/ directory and import it as EFile for the given challenge.
|
||||
// SyncExerciceFiles reads the content of files/ directory and import it as EFile for the given challenge.
|
||||
// It takes care of DIGESTS.txt and ensure imported files match.
|
||||
func ImportExerciceFiles(i Importer, exercice *fic.Exercice, exceptions *CheckExceptions) (errs error) {
|
||||
func SyncExerciceFiles(i Importer, exercice *fic.Exercice, exceptions *CheckExceptions) (errs error) {
|
||||
if _, err := exercice.WipeFiles(); err != nil {
|
||||
errs = multierr.Append(errs, err)
|
||||
}
|
||||
|
@ -376,41 +328,63 @@ func ImportExerciceFiles(i Importer, exercice *fic.Exercice, exceptions *CheckEx
|
|||
return
|
||||
}
|
||||
|
||||
actionAfterImport := func(fname string, digests map[string][]byte, filePath, origin string) (interface{}, error) {
|
||||
var digest_shown []byte
|
||||
if strings.HasSuffix(fname, ".gz") {
|
||||
if d, exists := digests[strings.TrimSuffix(fname, ".gz")]; exists {
|
||||
digest_shown = d
|
||||
}
|
||||
}
|
||||
files, digests, berrs := BuildFilesListInto(i, exercice, "files")
|
||||
errs = multierr.Append(errs, berrs)
|
||||
|
||||
published := true
|
||||
disclaimer := ""
|
||||
if f, exists := paramsFiles[fname]; exists {
|
||||
published = !f.Hidden
|
||||
|
||||
// Call checks hooks
|
||||
for _, hk := range hooks.mdTextHooks {
|
||||
for _, err := range multierr.Errors(hk(f.Disclaimer, exercice.Language, exceptions)) {
|
||||
errs = multierr.Append(errs, NewFileError(exercice, fname, err))
|
||||
// Import standard files
|
||||
for _, fname := range files {
|
||||
actionAfterImport := func(filePath string, origin string) (interface{}, error) {
|
||||
var digest_shown []byte
|
||||
if strings.HasSuffix(fname, ".gz") {
|
||||
if d, exists := digests[strings.TrimSuffix(fname, ".gz")]; exists {
|
||||
digest_shown = d
|
||||
}
|
||||
}
|
||||
|
||||
if disclaimer, err = ProcessMarkdown(i, fixnbsp(f.Disclaimer), exercice.Path); err != nil {
|
||||
errs = multierr.Append(errs, NewFileError(exercice, fname, fmt.Errorf("error during markdown formating of disclaimer: %w", err)))
|
||||
published := true
|
||||
disclaimer := ""
|
||||
if f, exists := paramsFiles[fname]; exists {
|
||||
published = !f.Hidden
|
||||
|
||||
// Call checks hooks
|
||||
for _, hk := range hooks.mdTextHooks {
|
||||
for _, err := range multierr.Errors(hk(f.Disclaimer, exercice.Language, exceptions)) {
|
||||
errs = multierr.Append(errs, NewFileError(exercice, fname, err))
|
||||
}
|
||||
}
|
||||
|
||||
if disclaimer, err = ProcessMarkdown(i, fixnbsp(f.Disclaimer), exercice.Path); err != nil {
|
||||
errs = multierr.Append(errs, NewFileError(exercice, fname, fmt.Errorf("error during markdown formating of disclaimer: %w", err)))
|
||||
}
|
||||
}
|
||||
|
||||
return exercice.ImportFile(filePath, origin, digests[fname], digest_shown, disclaimer, published)
|
||||
}
|
||||
|
||||
return exercice.ImportFile(filePath, origin, digests[fname], digest_shown, disclaimer, published)
|
||||
}
|
||||
var f interface{}
|
||||
|
||||
files, berrs := SyncExerciceFiles(i, exercice, paramsFiles, actionAfterImport)
|
||||
errs = multierr.Append(errs, berrs)
|
||||
if pf, exists := paramsFiles[fname]; exists && pf.URL != "" {
|
||||
dest := GetDestinationFilePath(pf.URL, &pf.Filename)
|
||||
|
||||
// Import files in db
|
||||
for _, file := range files {
|
||||
fname := file.Name
|
||||
f := file.file
|
||||
if _, err := os.Stat(dest); !os.IsNotExist(err) {
|
||||
if d, err := actionAfterImport(dest, pf.URL); err == nil {
|
||||
f = d
|
||||
}
|
||||
}
|
||||
|
||||
if f == nil {
|
||||
errs = multierr.Append(errs, DownloadExerciceFile(paramsFiles[fname], dest, exercice, false))
|
||||
|
||||
f, err = actionAfterImport(dest, pf.URL)
|
||||
}
|
||||
} else {
|
||||
f, err = i.importFile(path.Join(exercice.Path, "files", fname), actionAfterImport)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
errs = multierr.Append(errs, NewFileError(exercice, fname, err))
|
||||
continue
|
||||
}
|
||||
|
||||
if f.(*fic.EFile).Size == 0 {
|
||||
errs = multierr.Append(errs, NewFileError(exercice, fname, fmt.Errorf("imported file is empty!")))
|
||||
|
@ -446,67 +420,36 @@ func ImportExerciceFiles(i Importer, exercice *fic.Exercice, exceptions *CheckEx
|
|||
return
|
||||
}
|
||||
|
||||
func GetRemoteExerciceFiles(thid, exid string) ([]*fic.EFile, error) {
|
||||
theme, exceptions, errs := BuildTheme(GlobalImporter, thid)
|
||||
if theme == nil {
|
||||
return nil, errs
|
||||
}
|
||||
|
||||
exercice, _, _, _, _, errs := BuildExercice(GlobalImporter, theme, path.Join(theme.Path, exid), nil, exceptions)
|
||||
if exercice == nil {
|
||||
return nil, errs
|
||||
}
|
||||
|
||||
files, digests, errs := BuildFilesListInto(GlobalImporter, exercice, "files")
|
||||
if files == nil {
|
||||
return nil, errs
|
||||
}
|
||||
|
||||
var ret []*fic.EFile
|
||||
for _, fname := range files {
|
||||
fPath := path.Join(exercice.Path, "files", fname)
|
||||
fSize, _ := GetFileSize(GlobalImporter, fPath)
|
||||
|
||||
file := fic.EFile{
|
||||
Path: fPath,
|
||||
Name: fname,
|
||||
Checksum: digests[fname],
|
||||
Size: fSize,
|
||||
Published: true,
|
||||
}
|
||||
|
||||
if d, exists := digests[strings.TrimSuffix(file.Name, ".gz")]; exists {
|
||||
file.Name = strings.TrimSuffix(file.Name, ".gz")
|
||||
file.Path = strings.TrimSuffix(file.Path, ".gz")
|
||||
file.ChecksumShown = d
|
||||
}
|
||||
|
||||
ret = append(ret, &file)
|
||||
}
|
||||
|
||||
// Complete with attributes
|
||||
if paramsFiles, err := GetExerciceFilesParams(GlobalImporter, exercice); err == nil {
|
||||
for _, file := range ret {
|
||||
if f, ok := paramsFiles[file.Name]; ok {
|
||||
file.Published = !f.Hidden
|
||||
|
||||
if disclaimer, err := ProcessMarkdown(GlobalImporter, fixnbsp(f.Disclaimer), exercice.Path); err == nil {
|
||||
file.Disclaimer = disclaimer
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// ApiGetRemoteExerciceFiles is an accessor to remote exercice files list.
|
||||
func ApiGetRemoteExerciceFiles(c *gin.Context) {
|
||||
files, err := GetRemoteExerciceFiles(c.Params.ByName("thid"), c.Params.ByName("exid"))
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
|
||||
theme, exceptions, errs := BuildTheme(GlobalImporter, c.Params.ByName("thid"))
|
||||
if theme != nil {
|
||||
exercice, _, _, _, _, errs := BuildExercice(GlobalImporter, theme, path.Join(theme.Path, c.Params.ByName("exid")), nil, exceptions)
|
||||
if exercice != nil {
|
||||
files, digests, errs := BuildFilesListInto(GlobalImporter, exercice, "files")
|
||||
if files != nil {
|
||||
var ret []*fic.EFile
|
||||
for _, fname := range files {
|
||||
fPath := path.Join(exercice.Path, "files", fname)
|
||||
fSize, _ := GetFileSize(GlobalImporter, fPath)
|
||||
ret = append(ret, &fic.EFile{
|
||||
Path: fPath,
|
||||
Name: fname,
|
||||
Checksum: digests[fname],
|
||||
Size: fSize,
|
||||
})
|
||||
}
|
||||
c.JSON(http.StatusOK, ret)
|
||||
} else {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Errorf("%q", errs)})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Errorf("%q", errs)})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Errorf("%q", errs)})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, files)
|
||||
}
|
||||
|
|
|
@ -121,7 +121,7 @@ func buildExerciceHints(i Importer, exercice *fic.Exercice, exceptions *CheckExc
|
|||
|
||||
// CheckExerciceHints checks if all hints are corrects..
|
||||
func CheckExerciceHints(i Importer, exercice *fic.Exercice, exceptions *CheckExceptions) ([]importHint, error) {
|
||||
exceptions = exceptions.GetFileExceptions("challenge.toml", "challenge.txt")
|
||||
exceptions = exceptions.GetFileExceptions("challenge.txt", "challenge.toml")
|
||||
|
||||
hints, errs := buildExerciceHints(i, exercice, exceptions)
|
||||
|
||||
|
@ -139,7 +139,7 @@ func SyncExerciceHints(i Importer, exercice *fic.Exercice, flagsBindings map[int
|
|||
if _, err := exercice.WipeHints(); err != nil {
|
||||
errs = multierr.Append(errs, err)
|
||||
} else {
|
||||
exceptions = exceptions.GetFileExceptions("challenge.toml", "challenge.txt")
|
||||
exceptions = exceptions.GetFileExceptions("challenge.txt", "challenge.toml")
|
||||
|
||||
hints, berrs := buildExerciceHints(i, exercice, exceptions)
|
||||
errs = multierr.Append(errs, berrs)
|
||||
|
@ -169,32 +169,25 @@ func SyncExerciceHints(i Importer, exercice *fic.Exercice, flagsBindings map[int
|
|||
return
|
||||
}
|
||||
|
||||
func GetRemoteExerciceHints(thid, exid string) ([]importHint, error) {
|
||||
theme, exceptions, errs := BuildTheme(GlobalImporter, thid)
|
||||
if theme == nil {
|
||||
return nil, errs
|
||||
}
|
||||
|
||||
exercice, _, _, eexceptions, _, errs := BuildExercice(GlobalImporter, theme, path.Join(theme.Path, exid), nil, exceptions)
|
||||
if exercice == nil {
|
||||
return nil, errs
|
||||
}
|
||||
|
||||
hints, errs := CheckExerciceHints(GlobalImporter, exercice, eexceptions)
|
||||
if hints == nil {
|
||||
return nil, errs
|
||||
}
|
||||
|
||||
return hints, nil
|
||||
}
|
||||
|
||||
// ApiListRemoteExerciceHints is an accessor letting foreign packages to access remote exercice hints.
|
||||
func ApiGetRemoteExerciceHints(c *gin.Context) {
|
||||
hints, errs := GetRemoteExerciceHints(c.Params.ByName("thid"), c.Params.ByName("exid"))
|
||||
if hints != nil {
|
||||
theme, exceptions, errs := BuildTheme(GlobalImporter, c.Params.ByName("thid"))
|
||||
if theme != nil {
|
||||
exercice, _, _, eexceptions, _, errs := BuildExercice(GlobalImporter, theme, path.Join(theme.Path, c.Params.ByName("exid")), nil, exceptions)
|
||||
if exercice != nil {
|
||||
hints, errs := CheckExerciceHints(GlobalImporter, exercice, eexceptions)
|
||||
if hints != nil {
|
||||
c.JSON(http.StatusOK, hints)
|
||||
return
|
||||
}
|
||||
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, fmt.Errorf("%q", errs))
|
||||
return
|
||||
}
|
||||
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, fmt.Errorf("%q", errs))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, hints)
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, fmt.Errorf("%q", errs))
|
||||
}
|
||||
|
|
|
@ -279,7 +279,6 @@ func buildKeyFlag(exercice *fic.Exercice, flag ExerciceFlag, flagline int, defau
|
|||
}
|
||||
|
||||
type importFlag struct {
|
||||
origin ExerciceFlag
|
||||
Line int
|
||||
Flag fic.Flag
|
||||
JustifyOf *fic.MCQ_entry
|
||||
|
@ -356,10 +355,8 @@ func buildExerciceFlag(i Importer, exercice *fic.Exercice, flag ExerciceFlag, nl
|
|||
flag.Type = "radio"
|
||||
case "mcq":
|
||||
flag.Type = "mcq"
|
||||
case "justified":
|
||||
flag.Type = "justified"
|
||||
default:
|
||||
errs = multierr.Append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("invalid type of flag: should be 'key', 'number', 'text', 'mcq', 'justified', 'ucq', 'radio' or 'vector'")))
|
||||
errs = multierr.Append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("invalid type of flag: should be 'key', 'number', 'text', 'mcq', 'ucq', 'radio' or 'vector'")))
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -393,9 +390,8 @@ func buildExerciceFlag(i Importer, exercice *fic.Exercice, flag ExerciceFlag, nl
|
|||
errs = multierr.Append(errs, berrs)
|
||||
if addedFlag != nil {
|
||||
ret = append(ret, importFlag{
|
||||
origin: flag,
|
||||
Line: nline + 1,
|
||||
Flag: addedFlag,
|
||||
Line: nline + 1,
|
||||
Flag: addedFlag,
|
||||
})
|
||||
}
|
||||
} else if flag.Type == "key" || strings.HasPrefix(flag.Type, "number") || flag.Type == "text" || flag.Type == "ucq" || flag.Type == "radio" || flag.Type == "vector" {
|
||||
|
@ -403,13 +399,12 @@ func buildExerciceFlag(i Importer, exercice *fic.Exercice, flag ExerciceFlag, nl
|
|||
errs = multierr.Append(errs, berrs)
|
||||
if addedFlag != nil {
|
||||
ret = append(ret, importFlag{
|
||||
origin: flag,
|
||||
Line: nline + 1,
|
||||
Flag: *addedFlag,
|
||||
Choices: choices,
|
||||
})
|
||||
}
|
||||
} else if flag.Type == "mcq" || flag.Type == "justified" {
|
||||
} else if flag.Type == "mcq" {
|
||||
addedFlag := fic.MCQ{
|
||||
IdExercice: exercice.Id,
|
||||
Order: int8(nline + 1),
|
||||
|
@ -418,7 +413,7 @@ func buildExerciceFlag(i Importer, exercice *fic.Exercice, flag ExerciceFlag, nl
|
|||
}
|
||||
|
||||
hasOne := false
|
||||
isJustified := flag.Type == "justified"
|
||||
isJustified := false
|
||||
|
||||
if len(flag.Variant) != 0 {
|
||||
errs = multierr.Append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("variant is not defined for this kind of flag")))
|
||||
|
@ -465,7 +460,6 @@ func buildExerciceFlag(i Importer, exercice *fic.Exercice, flag ExerciceFlag, nl
|
|||
errs = multierr.Append(errs, berrs)
|
||||
if addedFlag != nil {
|
||||
ret = append(ret, importFlag{
|
||||
origin: flag,
|
||||
Line: nline + 1,
|
||||
Flag: *addedFlag,
|
||||
JustifyOf: entry,
|
||||
|
@ -483,9 +477,8 @@ func buildExerciceFlag(i Importer, exercice *fic.Exercice, flag ExerciceFlag, nl
|
|||
}
|
||||
|
||||
ret = append([]importFlag{importFlag{
|
||||
origin: flag,
|
||||
Line: nline + 1,
|
||||
Flag: &addedFlag,
|
||||
Line: nline + 1,
|
||||
Flag: &addedFlag,
|
||||
}}, ret...)
|
||||
}
|
||||
return
|
||||
|
@ -548,7 +541,7 @@ func buildExerciceFlags(i Importer, exercice *fic.Exercice, exceptions *CheckExc
|
|||
|
||||
// CheckExerciceFlags checks if all flags for the given challenge are correct.
|
||||
func CheckExerciceFlags(i Importer, exercice *fic.Exercice, files []string, exceptions *CheckExceptions) (rf []fic.Flag, errs error) {
|
||||
exceptions = exceptions.GetFileExceptions("challenge.toml", "challenge.txt")
|
||||
exceptions = exceptions.GetFileExceptions("challenge.txt", "challenge.toml")
|
||||
|
||||
flags, flagsids, berrs := buildExerciceFlags(i, exercice, exceptions)
|
||||
errs = multierr.Append(errs, berrs)
|
||||
|
@ -575,10 +568,6 @@ func CheckExerciceFlags(i Importer, exercice *fic.Exercice, files []string, exce
|
|||
if int64(fk.ChoicesCost) >= exercice.Gain {
|
||||
errs = multierr.Append(errs, NewFlagError(exercice, nil, flag.Line, fmt.Errorf("flag's choice_cost is higher than exercice gain")))
|
||||
}
|
||||
|
||||
if raw, ok := flag.origin.Raw.(string); ok && raw == fk.Placeholder {
|
||||
errs = multierr.Append(errs, NewFlagError(exercice, nil, flag.Line, fmt.Errorf("flag's placeholder and raw are identical")))
|
||||
}
|
||||
}
|
||||
|
||||
// Check dependency loop
|
||||
|
@ -645,7 +634,7 @@ func SyncExerciceFlags(i Importer, exercice *fic.Exercice, exceptions *CheckExce
|
|||
} else if _, err := exercice.WipeMCQs(); err != nil {
|
||||
errs = multierr.Append(errs, err)
|
||||
} else {
|
||||
exceptions = exceptions.GetFileExceptions("challenge.toml", "challenge.txt")
|
||||
exceptions = exceptions.GetFileExceptions("challenge.txt", "challenge.toml")
|
||||
|
||||
flags, flagids, berrs := buildExerciceFlags(i, exercice, exceptions)
|
||||
errs = multierr.Append(errs, berrs)
|
||||
|
@ -699,32 +688,26 @@ func SyncExerciceFlags(i Importer, exercice *fic.Exercice, exceptions *CheckExce
|
|||
return
|
||||
}
|
||||
|
||||
func GetRemoteExerciceFlags(thid, exid string) ([]fic.Flag, error) {
|
||||
theme, exceptions, errs := BuildTheme(GlobalImporter, thid)
|
||||
if theme == nil {
|
||||
return nil, errs
|
||||
}
|
||||
|
||||
exercice, _, _, eexceptions, _, errs := BuildExercice(GlobalImporter, theme, path.Join(theme.Path, exid), nil, exceptions)
|
||||
if exercice == nil {
|
||||
return nil, errs
|
||||
}
|
||||
|
||||
flags, errs := CheckExerciceFlags(GlobalImporter, exercice, []string{}, eexceptions)
|
||||
if flags == nil {
|
||||
return nil, errs
|
||||
}
|
||||
|
||||
return flags, nil
|
||||
}
|
||||
|
||||
// ApiListRemoteExerciceFlags is an accessor letting foreign packages to access remote exercice flags.
|
||||
func ApiGetRemoteExerciceFlags(c *gin.Context) {
|
||||
flags, err := GetRemoteExerciceFlags(c.Params.ByName("thid"), c.Params.ByName("exid"))
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
|
||||
theme, exceptions, errs := BuildTheme(GlobalImporter, c.Params.ByName("thid"))
|
||||
if theme != nil {
|
||||
exercice, _, _, eexceptions, _, errs := BuildExercice(GlobalImporter, theme, path.Join(theme.Path, c.Params.ByName("exid")), nil, exceptions)
|
||||
if exercice != nil {
|
||||
flags, errs := CheckExerciceFlags(GlobalImporter, exercice, []string{}, eexceptions)
|
||||
if flags != nil {
|
||||
c.JSON(http.StatusOK, flags)
|
||||
return
|
||||
}
|
||||
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, fmt.Errorf("%q", errs))
|
||||
return
|
||||
}
|
||||
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, fmt.Errorf("%q", errs))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, flags)
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, fmt.Errorf("%q", errs))
|
||||
return
|
||||
}
|
||||
|
|
|
@ -63,17 +63,10 @@ func buildDependancyMap(i Importer, theme *fic.Theme) (dmap map[int64]*fic.Exerc
|
|||
continue
|
||||
}
|
||||
|
||||
// ename can be overrride by title.txt
|
||||
if i.Exists(path.Join(theme.Path, edir, "title.txt")) {
|
||||
if myTitle, err := GetFileContent(i, path.Join(theme.Path, edir, "title.txt")); err == nil {
|
||||
ename = strings.TrimSpace(myTitle)
|
||||
}
|
||||
}
|
||||
|
||||
var e *fic.Exercice
|
||||
e, err = theme.GetExerciceByTitle(ename)
|
||||
if err != nil {
|
||||
return dmap, fmt.Errorf("unable to GetExerciceByTitle(ename=%q, tid=%d): %w", ename, theme.Id, err)
|
||||
return
|
||||
}
|
||||
|
||||
dmap[int64(eid)] = e
|
||||
|
@ -287,7 +280,7 @@ func BuildExercice(i Importer, theme *fic.Theme, epath string, dmap *map[int64]*
|
|||
|
||||
e.WIP = p.WIP
|
||||
if p.WIP && !AllowWIPExercice {
|
||||
errs = multierr.Append(errs, NewExerciceError(e, fmt.Errorf("exercice declared Work In Progress in challenge.toml"), theme))
|
||||
errs = multierr.Append(errs, NewExerciceError(e, fmt.Errorf("exercice declared Work In Progress in challenge.txt"), theme))
|
||||
}
|
||||
|
||||
if p.Gain == 0 {
|
||||
|
@ -398,7 +391,7 @@ func SyncExercice(i Importer, theme *fic.Theme, epath string, dmap *map[int64]*f
|
|||
if len(e.Image) > 0 {
|
||||
if _, err := i.importFile(e.Image,
|
||||
func(filePath string, origin string) (interface{}, error) {
|
||||
if err := resizePicture(i, origin, filePath, image.Rect(0, 0, 500, 300)); err != nil {
|
||||
if err := resizePicture(filePath, image.Rect(0, 0, 500, 300)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
@ -475,57 +468,59 @@ func SyncExercices(i Importer, theme *fic.Theme, exceptions *CheckExceptions) (e
|
|||
return
|
||||
}
|
||||
|
||||
func ListRemoteExercices(thid string) ([]string, error) {
|
||||
if thid == "_" {
|
||||
return GetExercices(GlobalImporter, &fic.StandaloneExercicesTheme)
|
||||
}
|
||||
|
||||
theme, _, errs := BuildTheme(GlobalImporter, thid)
|
||||
if theme == nil {
|
||||
return nil, errs
|
||||
}
|
||||
|
||||
return GetExercices(GlobalImporter, theme)
|
||||
}
|
||||
|
||||
// ApiListRemoteExercices is an accessor letting foreign packages to access remote exercices list.
|
||||
func ApiListRemoteExercices(c *gin.Context) {
|
||||
exercices, err := ListRemoteExercices(c.Params.ByName("thid"))
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
|
||||
if c.Params.ByName("thid") == "_" {
|
||||
exercices, err := GetExercices(GlobalImporter, &fic.Theme{Path: StandaloneExercicesDirectory})
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, exercices)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, exercices)
|
||||
}
|
||||
theme, _, errs := BuildTheme(GlobalImporter, c.Params.ByName("thid"))
|
||||
if theme != nil {
|
||||
exercices, err := GetExercices(GlobalImporter, theme)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
func GetRemoteExercice(thid, exid string, inTheme *fic.Theme) (*fic.Exercice, error) {
|
||||
if thid == fic.StandaloneExercicesDirectory || thid == "_" {
|
||||
exercice, _, _, _, _, errs := BuildExercice(GlobalImporter, nil, path.Join(fic.StandaloneExercicesDirectory, exid), nil, nil)
|
||||
return exercice, errs
|
||||
}
|
||||
|
||||
theme, exceptions, errs := BuildTheme(GlobalImporter, thid)
|
||||
if theme == nil {
|
||||
return nil, fmt.Errorf("Theme not found")
|
||||
}
|
||||
|
||||
if inTheme == nil {
|
||||
inTheme = theme
|
||||
}
|
||||
|
||||
exercice, _, _, _, _, errs := BuildExercice(GlobalImporter, inTheme, path.Join(theme.Path, exid), nil, exceptions)
|
||||
return exercice, errs
|
||||
}
|
||||
|
||||
// ApiGetRemoteExercice is an accessor letting foreign packages to access remote exercice attributes.
|
||||
func ApiGetRemoteExercice(c *gin.Context) {
|
||||
exercice, err := GetRemoteExercice(c.Params.ByName("thid"), c.Params.ByName("exid"), nil)
|
||||
if exercice != nil {
|
||||
c.JSON(http.StatusOK, exercice)
|
||||
return
|
||||
c.JSON(http.StatusOK, exercices)
|
||||
} else {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, fmt.Errorf("%q", errs))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// ApiListRemoteExercice is an accessor letting foreign packages to access remote exercice attributes.
|
||||
func ApiGetRemoteExercice(c *gin.Context) {
|
||||
if c.Params.ByName("thid") == "_" {
|
||||
exercice, _, _, _, _, errs := BuildExercice(GlobalImporter, nil, path.Join(StandaloneExercicesDirectory, c.Params.ByName("exid")), nil, nil)
|
||||
if exercice != nil {
|
||||
c.JSON(http.StatusOK, exercice)
|
||||
return
|
||||
} else {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Errorf("%q", errs)})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
theme, exceptions, errs := BuildTheme(GlobalImporter, c.Params.ByName("thid"))
|
||||
if theme != nil {
|
||||
exercice, _, _, _, _, errs := BuildExercice(GlobalImporter, theme, path.Join(theme.Path, c.Params.ByName("exid")), nil, exceptions)
|
||||
if exercice != nil {
|
||||
c.JSON(http.StatusOK, exercice)
|
||||
return
|
||||
} else {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Errorf("%q", errs)})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Errorf("%q", errs)})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
|
@ -172,6 +172,10 @@ func GetFileContent(i Importer, URI string) (string, error) {
|
|||
buf = append(buf, b)
|
||||
}
|
||||
|
||||
if len(buf) == 0 {
|
||||
return "", fmt.Errorf("File is empty")
|
||||
}
|
||||
|
||||
return strings.TrimSpace(string(buf)), nil
|
||||
}
|
||||
}
|
||||
|
@ -188,35 +192,26 @@ func GetDestinationFilePath(URI string, filename *string) string {
|
|||
return path.Join(fic.FilesDir, strings.ToLower(base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(hash[:])), *filename)
|
||||
}
|
||||
|
||||
var fileWriter = fileWriterToFS
|
||||
|
||||
func SetWriteFileFunc(writerFunc func(dest string) (io.WriteCloser, error)) {
|
||||
fileWriter = writerFunc
|
||||
}
|
||||
|
||||
func fileWriterToFS(dest string) (io.WriteCloser, error) {
|
||||
func importFile(i Importer, URI string, dest string) error {
|
||||
if err := os.MkdirAll(path.Dir(dest), 0751); err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
return os.Create(dest)
|
||||
}
|
||||
|
||||
func importFile(i Importer, URI string, dest string) error {
|
||||
if fdfrom, closer, err := GetFile(i, URI); err != nil {
|
||||
os.Remove(dest)
|
||||
// Write file
|
||||
if fdto, err := os.Create(dest); err != nil {
|
||||
return err
|
||||
} else {
|
||||
defer closer()
|
||||
|
||||
fdto, err := fileWriter(dest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fdto.Close()
|
||||
|
||||
_, err = io.Copy(fdto, fdfrom)
|
||||
return err
|
||||
if fdfrom, closer, err := GetFile(i, URI); err != nil {
|
||||
os.Remove(dest)
|
||||
return err
|
||||
} else {
|
||||
defer closer()
|
||||
|
||||
_, err = io.Copy(fdto, fdfrom)
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -247,21 +242,3 @@ func WriteFileContent(i Importer, URI string, content []byte) error {
|
|||
return fmt.Errorf("%t is not capable of writing", i)
|
||||
}
|
||||
}
|
||||
|
||||
func OpenOrGetFile(i Importer, URI string) (fd io.Reader, closer func() error, err error) {
|
||||
if strings.HasPrefix(URI, "$FILES$") {
|
||||
var fdc io.ReadCloser
|
||||
fdc, err = os.Open(path.Join(fic.FilesDir, strings.TrimPrefix(URI, "$FILES$/")))
|
||||
fd = fdc
|
||||
closer = fdc.Close
|
||||
} else {
|
||||
fd, err = GlobalImporter.GetFile(URI)
|
||||
if fdcloser, ok := fd.(io.ReadCloser); ok {
|
||||
closer = fdcloser.Close
|
||||
} else {
|
||||
closer = func() error { return nil }
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
|
|
@ -24,22 +24,6 @@ var oneThemeDeepSync sync.Mutex
|
|||
// DeepSyncProgress expose the progression of the depp synchronization (0 = 0%, 255 = 100%).
|
||||
var DeepSyncProgress uint8
|
||||
|
||||
func OneDeepSyncStatus() bool {
|
||||
if oneDeepSync.TryLock() {
|
||||
oneDeepSync.Unlock()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func OneThemeDeepSyncStatus() bool {
|
||||
if oneThemeDeepSync.TryLock() {
|
||||
oneThemeDeepSync.Unlock()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type SyncReport struct {
|
||||
DateStart time.Time `json:"_started"`
|
||||
DateEnd time.Time `json:"_ended"`
|
||||
|
@ -72,9 +56,13 @@ func SpeedySyncDeep(i Importer) (errs SyncReport) {
|
|||
errs.ThemesSync = append(errs.ThemesSync, sterr.Error())
|
||||
}
|
||||
|
||||
if themes, err := fic.GetThemesExtended(); err == nil {
|
||||
if themes, err := fic.GetThemes(); err == nil {
|
||||
DeepSyncProgress = 2
|
||||
|
||||
if i.Exists(StandaloneExercicesDirectory) {
|
||||
themes = append(themes, &fic.Theme{Path: StandaloneExercicesDirectory})
|
||||
}
|
||||
|
||||
var themeStep uint8 = uint8(250) / uint8(len(themes))
|
||||
|
||||
for tid, theme := range themes {
|
||||
|
@ -139,9 +127,14 @@ func SyncDeep(i Importer) (errs SyncReport) {
|
|||
}
|
||||
|
||||
// Synchronize themes
|
||||
if themes, err := fic.GetThemesExtended(); err == nil {
|
||||
if themes, err := fic.GetThemes(); err == nil {
|
||||
DeepSyncProgress = 2
|
||||
|
||||
// Also synchronize standalone exercices
|
||||
if i.Exists(StandaloneExercicesDirectory) {
|
||||
themes = append(themes, &fic.Theme{Path: StandaloneExercicesDirectory})
|
||||
}
|
||||
|
||||
var themeStep uint8 = uint8(250) / uint8(len(themes))
|
||||
|
||||
for tid, theme := range themes {
|
||||
|
@ -237,7 +230,7 @@ func SyncThemeDeep(i Importer, theme *fic.Theme, tid int, themeStep uint8, excep
|
|||
log.Printf("Deep synchronization in progress: %d/255 - doing Theme %q, Exercice %q: %q\n", DeepSyncProgress, theme.Name, exercice.Title, exercice.Path)
|
||||
|
||||
DeepSyncProgress = 3 + uint8(tid)*themeStep + uint8(eid)*exerciceStep
|
||||
errs = multierr.Append(errs, ImportExerciceFiles(i, exercice, ex_exceptions[eid]))
|
||||
errs = multierr.Append(errs, SyncExerciceFiles(i, exercice, ex_exceptions[eid]))
|
||||
|
||||
DeepSyncProgress += exerciceStep / 3
|
||||
flagsBindings, ferrs := SyncExerciceFlags(i, exercice, ex_exceptions[eid])
|
||||
|
|
|
@ -12,14 +12,6 @@ var gitRemoteRe = regexp.MustCompile(`^(?:(?:git@|https://)([\w.@]+)(?:/|:))((?:
|
|||
|
||||
var oneGitPull sync.Mutex
|
||||
|
||||
func OneGitPullStatus() bool {
|
||||
if oneGitPull.TryLock() {
|
||||
oneGitPull.Unlock()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func countFileInDir(dirname string) (int, error) {
|
||||
files, err := os.ReadDir(dirname)
|
||||
if err != nil {
|
||||
|
@ -65,10 +57,6 @@ func (i GitImporter) Kind() string {
|
|||
return "git originated from " + i.Remote + " on " + i.li.Kind()
|
||||
}
|
||||
|
||||
func (i GitImporter) DeleteDir(filename string) error {
|
||||
return i.li.DeleteDir(filename)
|
||||
}
|
||||
|
||||
func getForgeBaseLink(remote string) (u *url.URL, err error) {
|
||||
res := gitRemoteRe.FindStringSubmatch(remote)
|
||||
u, err = url.Parse(res[2])
|
||||
|
|
|
@ -205,7 +205,7 @@ func (i GitImporter) GetThemeLink(th *fic.Theme) (u *url.URL, err error) {
|
|||
return
|
||||
}
|
||||
|
||||
u.Path = path.Join(u.Path, "-", "tree", i.Branch, strings.TrimPrefix("/"+th.Path, prefix))
|
||||
u.Path = path.Join(u.Path, "-", "tree", i.Branch, strings.TrimPrefix(th.Path, prefix))
|
||||
|
||||
return
|
||||
}
|
||||
|
@ -241,7 +241,7 @@ func (i GitImporter) GetExerciceLink(e *fic.Exercice) (u *url.URL, err error) {
|
|||
return
|
||||
}
|
||||
|
||||
u.Path = path.Join(u.Path, "-", "tree", i.Branch, strings.TrimPrefix("/"+e.Path, prefix))
|
||||
u.Path = path.Join(u.Path, "-", "tree", i.Branch, strings.TrimPrefix(e.Path, prefix))
|
||||
|
||||
return
|
||||
}
|
||||
|
|
|
@ -113,11 +113,3 @@ func (i LocalImporter) ListDir(filename string) ([]string, error) {
|
|||
func (i LocalImporter) Stat(filename string) (os.FileInfo, error) {
|
||||
return os.Stat(path.Join(i.Base, filename))
|
||||
}
|
||||
|
||||
type DeletableImporter interface {
|
||||
DeleteDir(filename string) error
|
||||
}
|
||||
|
||||
func (i LocalImporter) DeleteDir(filename string) error {
|
||||
return os.RemoveAll(path.Join(i.Base, filename))
|
||||
}
|
||||
|
|
|
@ -3,7 +3,9 @@ package sync
|
|||
import (
|
||||
"bytes"
|
||||
"encoding/base32"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
|
@ -87,10 +89,27 @@ func (t *imageImporterTransformer) Transform(doc *ast.Document, reader text.Read
|
|||
dPath := path.Join(fic.FilesDir, strings.ToLower(base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(t.hash[:])), iPath)
|
||||
child.Destination = []byte(path.Join(t.absPath, string(child.Destination)))
|
||||
|
||||
err := importFile(t.importer, path.Join(t.rootDir, iPath), dPath)
|
||||
if err != nil {
|
||||
if err := os.MkdirAll(path.Dir(dPath), 0755); err != nil {
|
||||
return ast.WalkStop, err
|
||||
}
|
||||
|
||||
if fdto, err := os.Create(dPath); err != nil {
|
||||
return ast.WalkStop, err
|
||||
} else {
|
||||
defer fdto.Close()
|
||||
|
||||
if fd, closer, err := GetFile(t.importer, path.Join(t.rootDir, iPath)); err != nil {
|
||||
os.Remove(dPath)
|
||||
return ast.WalkStop, err
|
||||
} else {
|
||||
defer closer()
|
||||
|
||||
_, err = io.Copy(fdto, fd)
|
||||
if err != nil {
|
||||
return ast.WalkStop, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ast.WalkContinue, nil
|
||||
|
|
|
@ -5,7 +5,6 @@ import (
|
|||
"fmt"
|
||||
"image"
|
||||
"image/jpeg"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"os"
|
||||
|
@ -23,13 +22,15 @@ import (
|
|||
"srs.epita.fr/fic-server/libfic"
|
||||
)
|
||||
|
||||
const StandaloneExercicesDirectory = "exercices"
|
||||
|
||||
// GetThemes returns all theme directories in the base directory.
|
||||
func GetThemes(i Importer) (themes []string, err error) {
|
||||
if dirs, err := i.ListDir("/"); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
for _, dir := range dirs {
|
||||
if !strings.HasPrefix(dir, ".") && !strings.HasPrefix(dir, "_") && dir != fic.StandaloneExercicesDirectory {
|
||||
if !strings.HasPrefix(dir, ".") && !strings.HasPrefix(dir, "_") && dir != StandaloneExercicesDirectory {
|
||||
if _, err := i.ListDir(dir); err == nil {
|
||||
themes = append(themes, dir)
|
||||
}
|
||||
|
@ -40,34 +41,16 @@ func GetThemes(i Importer) (themes []string, err error) {
|
|||
return themes, nil
|
||||
}
|
||||
|
||||
// GetThemesExtended returns all theme directories, including standalone exercices.
|
||||
func GetThemesExtended(i Importer) (themes []string, err error) {
|
||||
themes, err = GetThemes(i)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if i.Exists(fic.StandaloneExercicesDirectory) {
|
||||
themes = append(themes, fic.StandaloneExercicesDirectory)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// resizePicture makes the given image just fill the given rectangle.
|
||||
func resizePicture(i Importer, imgPath string, importedPath string, rect image.Rectangle) error {
|
||||
if fl, err := i.GetFile(imgPath); err != nil {
|
||||
func resizePicture(importedPath string, rect image.Rectangle) error {
|
||||
if fl, err := os.Open(importedPath); err != nil {
|
||||
return err
|
||||
} else {
|
||||
if src, _, err := image.Decode(fl); err != nil {
|
||||
if flc, ok := fl.(io.ReadCloser); ok {
|
||||
flc.Close()
|
||||
}
|
||||
fl.Close()
|
||||
return err
|
||||
} else if src.Bounds().Max.X > rect.Max.X && src.Bounds().Max.Y > rect.Max.Y {
|
||||
if flc, ok := fl.(io.ReadCloser); ok {
|
||||
flc.Close()
|
||||
}
|
||||
fl.Close()
|
||||
|
||||
mWidth := rect.Max.Y * src.Bounds().Max.X / src.Bounds().Max.Y
|
||||
mHeight := rect.Max.X * src.Bounds().Max.Y / src.Bounds().Max.X
|
||||
|
@ -80,7 +63,7 @@ func resizePicture(i Importer, imgPath string, importedPath string, rect image.R
|
|||
dst := image.NewRGBA(rect)
|
||||
draw.CatmullRom.Scale(dst, rect, src, src.Bounds(), draw.Over, nil)
|
||||
|
||||
dstFile, err := fileWriter(strings.TrimSuffix(importedPath, ".jpg") + ".thumb.jpg")
|
||||
dstFile, err := os.Create(strings.TrimSuffix(importedPath, ".jpg") + ".thumb.jpg")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -90,7 +73,7 @@ func resizePicture(i Importer, imgPath string, importedPath string, rect image.R
|
|||
return err
|
||||
}
|
||||
} else {
|
||||
dstFile, err := fileWriter(strings.TrimSuffix(importedPath, ".jpg") + ".thumb.jpg")
|
||||
dstFile, err := os.Create(strings.TrimSuffix(importedPath, ".jpg") + ".thumb.jpg")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -197,10 +180,8 @@ func BuildTheme(i Importer, tdir string) (th *fic.Theme, exceptions *CheckExcept
|
|||
th.URLId = fic.ToURLid(th.Name)
|
||||
|
||||
if authors, err := getAuthors(i, tdir); err != nil {
|
||||
if tdir != fic.StandaloneExercicesDirectory {
|
||||
errs = multierr.Append(errs, NewThemeError(th, fmt.Errorf("unable to get AUTHORS.txt: %w", err)))
|
||||
return nil, nil, errs
|
||||
}
|
||||
errs = multierr.Append(errs, NewThemeError(th, fmt.Errorf("unable to get AUTHORS.txt: %w", err)))
|
||||
return nil, nil, errs
|
||||
} else {
|
||||
// Format authors
|
||||
th.Authors = strings.Join(authors, ", ")
|
||||
|
@ -292,36 +273,6 @@ func BuildTheme(i Importer, tdir string) (th *fic.Theme, exceptions *CheckExcept
|
|||
return
|
||||
}
|
||||
|
||||
// SyncThemeFiles import all theme's related files
|
||||
func SyncThemeFiles(i Importer, btheme *fic.Theme) (errs error) {
|
||||
if len(btheme.Image) > 0 {
|
||||
if _, err := i.importFile(btheme.Image,
|
||||
func(filePath string, origin string) (interface{}, error) {
|
||||
if err := resizePicture(i, origin, filePath, image.Rect(0, 0, 500, 300)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
btheme.Image = strings.TrimPrefix(filePath, fic.FilesDir)
|
||||
btheme.BackgroundColor, _ = getBackgroundColor(filePath)
|
||||
return nil, nil
|
||||
}); err != nil {
|
||||
errs = multierr.Append(errs, NewThemeError(btheme, fmt.Errorf("unable to import heading image: %w", err)))
|
||||
}
|
||||
}
|
||||
|
||||
if len(btheme.PartnerImage) > 0 {
|
||||
if _, err := i.importFile(btheme.PartnerImage,
|
||||
func(filePath string, origin string) (interface{}, error) {
|
||||
btheme.PartnerImage = strings.TrimPrefix(filePath, fic.FilesDir)
|
||||
return nil, nil
|
||||
}); err != nil {
|
||||
errs = multierr.Append(errs, NewThemeError(btheme, fmt.Errorf("unable to import partner image: %w", err)))
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// SyncThemes imports new or updates existing themes.
|
||||
func SyncThemes(i Importer) (exceptions map[string]*CheckExceptions, errs error) {
|
||||
if themes, err := GetThemes(i); err != nil {
|
||||
|
@ -343,9 +294,29 @@ func SyncThemes(i Importer) (exceptions map[string]*CheckExceptions, errs error)
|
|||
|
||||
exceptions[tdir] = excepts
|
||||
|
||||
err = SyncThemeFiles(i, btheme)
|
||||
if err != nil {
|
||||
errs = multierr.Append(errs, NewThemeError(btheme, fmt.Errorf("unable to import heading image: %w", err)))
|
||||
if len(btheme.Image) > 0 {
|
||||
if _, err := i.importFile(btheme.Image,
|
||||
func(filePath string, origin string) (interface{}, error) {
|
||||
if err := resizePicture(filePath, image.Rect(0, 0, 500, 300)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
btheme.Image = strings.TrimPrefix(filePath, fic.FilesDir)
|
||||
btheme.BackgroundColor, _ = getBackgroundColor(filePath)
|
||||
return nil, nil
|
||||
}); err != nil {
|
||||
errs = multierr.Append(errs, NewThemeError(btheme, fmt.Errorf("unable to import heading image: %w", err)))
|
||||
}
|
||||
}
|
||||
|
||||
if len(btheme.PartnerImage) > 0 {
|
||||
if _, err := i.importFile(btheme.PartnerImage,
|
||||
func(filePath string, origin string) (interface{}, error) {
|
||||
btheme.PartnerImage = strings.TrimPrefix(filePath, fic.FilesDir)
|
||||
return nil, nil
|
||||
}); err != nil {
|
||||
errs = multierr.Append(errs, NewThemeError(btheme, fmt.Errorf("unable to import partner image: %w", err)))
|
||||
}
|
||||
}
|
||||
|
||||
var theme *fic.Theme
|
||||
|
@ -384,34 +355,18 @@ func ApiListRemoteThemes(c *gin.Context) {
|
|||
c.JSON(http.StatusOK, themes)
|
||||
}
|
||||
|
||||
func GetRemoteTheme(thid string) (*fic.Theme, error) {
|
||||
if thid == fic.StandaloneExercicesTheme.URLId || thid == fic.StandaloneExercicesDirectory {
|
||||
return &fic.StandaloneExercicesTheme, nil
|
||||
}
|
||||
|
||||
theme, _, errs := BuildTheme(GlobalImporter, thid)
|
||||
if theme == nil {
|
||||
return nil, errs
|
||||
}
|
||||
|
||||
return theme, nil
|
||||
}
|
||||
|
||||
// ApiListRemoteTheme is an accessor letting foreign packages to access remote main theme attributes.
|
||||
func ApiGetRemoteTheme(c *gin.Context) {
|
||||
var theme *fic.Theme
|
||||
var err error
|
||||
|
||||
if c.Params.ByName("thid") == fic.StandaloneExercicesTheme.URLId {
|
||||
theme, err = GetRemoteTheme(fic.StandaloneExercicesDirectory)
|
||||
} else {
|
||||
theme, err = GetRemoteTheme(c.Params.ByName("thid"))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
|
||||
if c.Params.ByName("thid") == "_" {
|
||||
c.Status(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, theme)
|
||||
r, _, errs := BuildTheme(GlobalImporter, c.Params.ByName("thid"))
|
||||
if r == nil {
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Errorf("%q", errs)})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, r)
|
||||
}
|
||||
|
|
|
@ -90,7 +90,6 @@ func reloadSettings(config *settings.Settings) {
|
|||
fic.GlobalScoreCoefficient = config.GlobalScoreCoefficient
|
||||
fic.CountOnlyNotGoodTries = config.CountOnlyNotGoodTries
|
||||
fic.DiscountedFactor = config.DiscountedFactor
|
||||
fic.QuestionGainRatio = config.QuestionGainRatio
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
|
|
@ -20,24 +20,3 @@ ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCUKevt/f1n2byv5oH43iQsZ7b4kAATHlHNUF6WMQjk
|
|||
# ?
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICo5yumHfQbMwhZAtEZByQR0xIVcoealS7g4MNTMEVaX roote@roote-VirtualBox
|
||||
|
||||
# cyril.blin
|
||||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCit9xqC+/EcC0D+x4Irg/AgnTx9rJDbE4FdKK2Z2vE0nSPzp1MbAtijVi5ndvr/JPlY3jUHeGEZBJHmADXeOvdJl1nvkqry/69phfr4nDYacvH0v66nDTRipqmCmebaYOkfXYG55oy40+6C6DwAGETTYq+PIaRcA/mSf6V9UxKBfnLVqdml7LFYEo1SbihAIFd0EZkwq1wevXdVmrDwF7VLiCin/5Axa6LUOe4l1SAYBpsV8pCY3PQ/KxpgCyJuYj2szhOl0shTPiV48f194xGtYrpx1uGhOHRDx6Rm/5LKY/5DUvKbHCa/ZAdUSoMTnd1TshAPJe1sYKSAAI1xPVmffOgF/Jh98QEuAuFmHfZXVgPdvApJ9r9Ea7gEyN6Xe37emkW1Dond4ARdNdaslVu0iwV6bQnDOGcEdAl3x3seRVRAiPAKp2tAEtVEqu7uFwX6v2mmpE5/uw8rfsl/wNgttjuYa/kJURNkto3bN02XNfzOnXXZ3bRtbNjHEyJQuM= cyril@nixos
|
||||
|
||||
# victor.chartraire
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJgDuQ4FrDuDjoo1Xv7pA0WEev1hhgJ8lXpT/17QXsds celian@DESKTOP-BQA641E
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPfUVWWSUrfuQYofWwvotQip4uqG34dF1Ybn4tTU1l0n celian@DESKTOP-BQA641E
|
||||
|
||||
# hugo.rubio
|
||||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC68cnL/66U9dKfGZe3Bfr+ggosVXZh5OymRuHEdl26lYr3uJAQ7tKtfgXXyfuIW87vxGuzZUOEAnDafJx0J24BVyTwNjJVRarB8efHVIvlL/S9hHPVKWaQyo1ZIgizCiRdljJLWweZKjRE6Yr8xPc6zlC7grQ0wG/WGtAKaedhxYUZcCkdxPMz9Jf2ufx23DB/ALUZ63KVwbWIof8LPtlBP3G74ooeuykR+BTH+6NinoOWlL94JsSTAhuVnKsSnG1eQ742Cv4XhhGleAStP3wFhyakdsvCOfw9bKN/z3dXA0nZGzwOqvHyI9OJx01GLHhyykfRL7PtpD8MMwzyq3r5 hugo0@LAPTOP-C4LN612T
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA1BINotXNgrTmHrep0BbxtUIAvCXOjSN8GrfTq03Sfd ryugo@LAPTOP-C4LN612T
|
||||
|
||||
# alexandra.delin
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOJGg902VI7w6nOSiGm6qyUnPn9w3sD3VKlMin3fbYK6 alex@AlexandraSRS
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIArw17NpbJRNf8m9eji5K+GcD8oiyJi/3ygceojEVtBL alexa@Alexandra_SRS
|
||||
|
||||
# maxence.michot
|
||||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCqiBbRBWhnG5+LU/pKevR5oJV3+2ZQRQ6Fu87F8873DkKP0okvAYqVXYEDIm/bq9+VocZHI8y9O+zbCXwIqdWRGESJFiM7r1L3HtimFlmWhzM+nzduS8dWGcGGa9XCw+OBHFMGUFEMyzNbi/YcF7QGybOGETdcyzXQng1JtW4IURdrFJm/0/EavRwO2O2xTexmHv8l7JAe0ChteeuDatADrWgNnoUXIqeQQIsPxKQJ9D4QZ7+QrD5K2iLCx8sqC8Eu10p7cRqB3pCcBzKc6MEOgA1FXJzYgNtWkkLR9s7cOVjKL6mL3DuYQla3VaSvkTh8SAKEkadgYXpYifF6ygL298EmhQIrqRdjDYAm+VDGfCvmtcEW4NM68tt74xLuhdDutLq9Riqx4n2pGPo8ws8ecCokcu5/ESGxpZwYmY+nMj8392yH4sgmW/nrs0WmAqawdwlDj09LIqbYXiGMd5zYYOu8V2nuMKG0zRtRESKbs04Uw2DUd/gTwfP2l5YqTn3K93xDNU2iZY+yLJai3vYaBLHzt6PEvESdhAceYBMTIN2iA6hGZfaMWJ4JHImoqCgwbnJw6qrIPHaj1SCWKWLPMjjYZbwndkH3Iy4oJ6gA6I/vT9YrzNvGYY7rVspa+O0RM+huVH1a0YoXSSzx6dop1k1ZWm7VwRCUJQ2ri+L2jw== thecr@maxence-portable-desktop
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMwyRf67AH/rUzSFsKpFfGDD/La4JqpYk9xCM04kJ6+P maxence@vokunaav
|
||||
|
||||
# leo.blanc-di-pasquale
|
||||
ssh-rsa AAAAB4NzaC1yc2EAAAADAQABAAABgQDBovLxpJA0+gJiAVWrpWmjy9fyEQwOXv3Ytou7C4Y1O3lAyvmmHfBWuW1ZqIY/8yKIpJeSLCoGeN5kyMD9ITvMVcfSAxnDHXj04Q4rJzBGjQ/LF/CInx+HHjJWzGVgJC76m0BS7J0J7g5gAxfprGKEb3Z1kJmCTRQ470Azv6WIPDCw7aLNd1o5JqAsrmWSh/LwYBSapyp3Tk0KjcbxRifvnBJfYLHwNm1ZoaLCGAT3u3TKIifawEomx5QEel/211FaEdbglkeDtRL0YuvKQSuZCf4KSg7xSRQoWHmSuPothE9eQGzEKpJONKViqkxBo12O04cBEhYzQBh7GHmH3U3/9weNUX4EaKJWkfqh43eAnWohN07IXDnANYPRxWL4ITv+MNBWxYL5Zp3Zr85rhJZYhkyKEfcGjvolHaESegnsndhC74QW3eOXazuscAeROQbiMShk5iBIceTsEFTqI02/+XqTs9gXkuq3H3StJ6+9cavDwRyObGx5nVHmhQYnMEE= leo21@Aled
|
||||
|
|
|
@ -20,8 +20,6 @@ escape_newline () {
|
|||
sed 's/$/\\n/g' | tr -d '\n'
|
||||
}
|
||||
|
||||
which mkisofs > /dev/null 2> /dev/null || { echo "Please install genisoimage (Debian/Ubuntu) or cdrkit (Alpine)" >&2; exit 1; }
|
||||
|
||||
if [ $# -gt 0 ]
|
||||
then
|
||||
which jq > /dev/null 2> /dev/null || { echo "Please install jq" >&2; exit 1; }
|
||||
|
|
|
@ -77,7 +77,7 @@ server {
|
|||
}
|
||||
}
|
||||
|
||||
location ~ ^/([A-Z]|_/) {
|
||||
location ~ ^/[A-Z_] {
|
||||
include fic-auth.conf;
|
||||
|
||||
rewrite ^/.*$ /index.html;
|
||||
|
|
|
@ -63,7 +63,7 @@ server {
|
|||
rewrite ^${FIC_BASEURL2}(.*)$ /$1;
|
||||
}
|
||||
|
||||
location ~ ^/([A-Z]|_/) {
|
||||
location ~ ^/[A-Z_] {
|
||||
include fic-get-team.conf;
|
||||
|
||||
rewrite ^/.*$ /index.html;
|
||||
|
|
|
@ -67,7 +67,7 @@ server {
|
|||
}
|
||||
}
|
||||
|
||||
location ~ ^/([A-Z]|_/) {
|
||||
location ~ ^/[A-Z_] {
|
||||
include fic-get-team.conf;
|
||||
|
||||
rewrite ^/.*$ /index.html;
|
||||
|
|
|
@ -21,8 +21,6 @@ OLD_KEY=$(cat /run/config/dm-crypt/key)
|
|||
[ "${NEW_KEY}" != "${OLD_KEY}" ] && {
|
||||
read -p "DM-CRYPT key changed in metadata, are you sure you want to erase it? (y/N) " V
|
||||
[ "$V" != "y" ] && [ "$V" != "Y" ] && while true; do
|
||||
mv /boot/imgs/fickit-metadata.iso /boot/imgs/fickit-metadata.iso.skipped
|
||||
cp /boot/imgs/fickit-metadata.iso.bak /boot/imgs/fickit-metadata.iso
|
||||
echo
|
||||
echo "Metadata drive not erased"
|
||||
echo
|
||||
|
|
|
@ -365,7 +365,7 @@
|
|||
<div class="carousel slide" data-interval="12000" style="padding-bottom: 0px" autocarousel>
|
||||
<div class="carousel-inner">
|
||||
<div class="carousel-item" ng-repeat="theme in themes" ng-class="{active: $first}">
|
||||
<div class="carousel-caption text-indent" ng-if="theme.urlid !== '_'">
|
||||
<div class="carousel-caption text-indent">
|
||||
<div class="card-img-top theme-card" style="background-image: url('{{ theme.image.substr(0, theme.image.length-3) }}thumb.jpg')"></div>
|
||||
<h3 class="text-left" ng-bind="theme.name"></h3>
|
||||
<p class="text-justify" style="font-size: 111%" ng-bind-html="theme.headline"></p>
|
||||
|
@ -377,8 +377,7 @@
|
|||
</div>
|
||||
|
||||
<div class="card niceborder bg-dark" ng-if="s.type == 'exercice' && !s.params.hide">
|
||||
<div class="card-img-top theme-card" style="background-image: url('{{exercices[s.params.exercice].image}}')" ng-if="exercices[s.params.exercice] && exercices[s.params.exercice].image"></div>
|
||||
<div class="card-img-top theme-card" style="background-image: url('{{themes[my.exercices[s.params.exercice].theme_id].image}}')" ng-if="!exercices[s.params.exercice] || !exercices[s.params.exercice].image"></div>
|
||||
<div class="card-img-top theme-card" style="background-image: url('{{themes[my.exercices[s.params.exercice].theme_id].image}}')"></div>
|
||||
<div class="card-body text-light">
|
||||
<h3 style="font-size: 1.0rem; text-weight: bold; overflow: hidden; text-overflow: ellipsis; white-space: nowrap">Défi <em>{{ exercices[s.params.exercice].title }}</em> du thème <em>{{ themes[my.exercices[s.params.exercice].theme_id].name }}</em></h3>
|
||||
<p ng-bind-html="my.exercices[s.params.exercice].overview"></p>
|
||||
|
@ -398,8 +397,7 @@
|
|||
Challenges à la une
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-img-top theme-card" style="background-image: url('{{exercices[lastExercice].image}}')" ng-if="exercices[lastExercice] && exercices[lastExercice].image"></div>
|
||||
<div class="card-img-top theme-card" style="background-image: url('{{themes[my.exercices[lastExercice].theme_id].image}}')" ng-if="!exercices[s.params.exercice] || !exercices[s.params.exercice].image"></div>
|
||||
<div class="card-img-top theme-card" style="background-image: url('{{themes[my.exercices[lastExercice].theme_id].image}}')"></div>
|
||||
<div class="card-body text-light">
|
||||
<h3 style="font-size: 1.0rem; text-weight: bold; overflow: hidden; text-overflow: ellipsis; white-space: nowrap"><em>{{ exercices[lastExercice].title }}</em> du thème <em>{{ themes[my.exercices[lastExercice].theme_id].name }}</em></h3>
|
||||
<p ng-bind-html="my.exercices[lastExercice].overview"></p>
|
||||
|
|
|
@ -50,7 +50,6 @@ services:
|
|||
networks:
|
||||
- fic-net
|
||||
volumes:
|
||||
- dashboard:/srv/DASHBOARD
|
||||
- settings:/srv/SETTINGS
|
||||
- settingsdist:/srv/SETTINGSDIST
|
||||
|
||||
|
|
|
@ -1,54 +1,54 @@
|
|||
kernel:
|
||||
#image: nemunaire/kernel:5.10.62-0b705d955f5e283f62583c4e227d64a7924c138f-amd64
|
||||
image: linuxkit/kernel:6.6.71
|
||||
image: linuxkit/kernel:6.6.13
|
||||
cmdline: "console=ttyS0 console=tty0"
|
||||
|
||||
init:
|
||||
- linuxkit/init:8eea386739975a43af558eec757a7dcb3a3d2e7b
|
||||
- linuxkit/runc:667e7ea2c426a2460ca21e3da065a57dbb3369c9
|
||||
- linuxkit/containerd:a988a1a8bcbacc2c0390ca0c08f949e2b4b5915d
|
||||
- linuxkit/ca-certificates:7b32a26ca9c275d3ef32b11fe2a83dbd2aee2fdb
|
||||
- linuxkit/getty:05eca453695984a69617f1f1f0bcdae7f7032967
|
||||
- linuxkit/init:v1.0.0
|
||||
- linuxkit/runc:6062483d748609d505f2bcde4e52ee64a3329f5f
|
||||
- linuxkit/containerd:v1.0.0
|
||||
- linuxkit/ca-certificates:v1.0.0
|
||||
- linuxkit/getty:v1.0.0
|
||||
- nemunaire/mdadm:04814350d71ba9417e1f861be1685de26adf7a67
|
||||
- nemunaire/kexec:839b4eedfce02a56c581dec2383dc6faff120855
|
||||
|
||||
onboot:
|
||||
- name: mod
|
||||
image: linuxkit/modprobe:773ee174006ecbb412830e48889795bae40b62f9
|
||||
image: linuxkit/modprobe:v1.0.0
|
||||
command: ["/bin/sh", "-c", "modprobe xhci_pci ahci intel_lpss_pci i2c_i801 megaraid_sas tg3 bnxt_en"]
|
||||
|
||||
- name: sysctl
|
||||
image: linuxkit/sysctl:5f56434b81004b50b47ed629b222619168c2bcdf
|
||||
image: linuxkit/sysctl:v1.0.0
|
||||
binds:
|
||||
- /etc/sysctl.d/01-fic.conf:/etc/sysctl.d/01-fic.conf:ro
|
||||
|
||||
# Metadata
|
||||
- name: metadata
|
||||
image: linuxkit/metadata:4f81c0c3a2b245567fd7d32d799018c9614a9907
|
||||
image: linuxkit/metadata:v1.0.0
|
||||
command: ["/usr/bin/metadata", "-v", "cdrom"]
|
||||
|
||||
# Filesystem
|
||||
- name: swap
|
||||
image: linuxkit/swap:f4b8ffef87c8c72165bd8a92b790ac252ccf1821
|
||||
image: linuxkit/swap:v1.0.0
|
||||
command: ["/sbin/swapon", "/dev/sda3"]
|
||||
- name: dm-crypt
|
||||
image: linuxkit/dm-crypt:981fde241bb84616a5ba94c04cdefa1489431a25
|
||||
image: linuxkit/dm-crypt:d49723bc9d10c5ada9e03b0670f4e57416d5d084
|
||||
command: ["/usr/bin/crypto", "-l", "crypt_fic", "/dev/sda4"]
|
||||
binds:
|
||||
- /dev:/dev
|
||||
- /run/config/dm-crypt:/etc/dm-crypt
|
||||
- name: mount
|
||||
image: linuxkit/mount:cb8caa72248f7082fc2074ce843d53cdc15df04a
|
||||
image: linuxkit/mount:v1.0.0
|
||||
command: ["/usr/bin/mountie", "-device", "/dev/mapper/crypt_fic", "/var/lib/fic" ]
|
||||
|
||||
# Network
|
||||
# - name: dhcpcd
|
||||
# image: linuxkit/dhcpcd:157df9ef45a035f1542ec2270e374f18efef98a5
|
||||
# image: linuxkit/dhcpcd:v1.0.0
|
||||
# command: ["/sbin/dhcpcd", "--nobackground", "-f", "/dhcpcd.conf", "-1"]
|
||||
# - name: ntp
|
||||
# image: linuxkit/openntpd:f99c4117763480815553b72022b426639a13ce86
|
||||
# image: linuxkit/openntpd:v1.0.0
|
||||
- name: synchro-ip-setup
|
||||
image: linuxkit/ip:9696394a7d57b384ae919662ae162c9152029156
|
||||
image: linuxkit/ip:v1.0.0
|
||||
command: ["/bin/sh", "-c", "ip a add 10.10.10.1/29 dev eth2; ip link set eth2 up;" ]
|
||||
net: new
|
||||
runtime:
|
||||
|
@ -57,7 +57,7 @@ onboot:
|
|||
bindNS:
|
||||
net: /run/netns/synchro
|
||||
- name: qa-ip-setup
|
||||
image: linuxkit/ip:9696394a7d57b384ae919662ae162c9152029156
|
||||
image: linuxkit/ip:v1.0.0
|
||||
command: ["/bin/sh", "-c", "ip link show eth1 2> /dev/null && { ip a add 10.10.10.1/29 dev eth1; ip link set eth1 up; }; ip a add 172.17.0.6/24 dev vethin-qa; ip link set vethin-qa up" ]
|
||||
net: new
|
||||
runtime:
|
||||
|
@ -69,7 +69,7 @@ onboot:
|
|||
bindNS:
|
||||
net: /run/netns/fic-qa
|
||||
- name: admin-ip-setup
|
||||
image: linuxkit/ip:9696394a7d57b384ae919662ae162c9152029156
|
||||
image: linuxkit/ip:v1.0.0
|
||||
#command: ["/bin/sh", "-c", "ip link add link eth3 name adminiface type vlan id 99; ip a add 172.16.99.219/24 dev adminiface; ip link set eth3 up; ip link set adminiface up; ip r add default via 172.16.99.1; ip a add 172.17.0.2/24 dev vethin-admin; ip link set vethin-admin up; ping -W 10 -c 1 172.16.99.1;" ]
|
||||
command: ["/bin/sh", "-c", "ip link set eth3 up; while read IP; do ip a add ${IP} dev eth3; done < /run/config/ip_config/backend-admin; ip r add default via $(cat /run/config/ip_config/backend-router); ip a add 172.17.0.2/24 dev vethin-admin; ip link set vethin-admin up; echo 'Waiting for' $(cat /run/config/ip_config/backend-router); ping -W 10 -c 1 $(cat /run/config/ip_config/backend-router); ip link show eth1 2> /dev/null && { ip a add 10.0.0.1/24 dev eth1; ip link set eth1 up; };" ]
|
||||
net: new
|
||||
|
@ -85,7 +85,7 @@ onboot:
|
|||
bindNS:
|
||||
net: /run/netns/fic-admin
|
||||
- name: checker-ip-setup
|
||||
image: linuxkit/ip:9696394a7d57b384ae919662ae162c9152029156
|
||||
image: linuxkit/ip:v1.0.0
|
||||
command: ["/bin/sh", "-c", "ip a add 172.17.0.3/24 dev vethin-checker; ip link set vethin-checker up;" ]
|
||||
net: new
|
||||
runtime:
|
||||
|
@ -96,7 +96,7 @@ onboot:
|
|||
bindNS:
|
||||
net: /run/netns/fic-checker
|
||||
- name: generator-ip-setup
|
||||
image: linuxkit/ip:9696394a7d57b384ae919662ae162c9152029156
|
||||
image: linuxkit/ip:v1.0.0
|
||||
command: ["/bin/sh", "-c", "ip a add 172.17.0.5/24 dev vethin-generat; ip link set vethin-generat up;" ]
|
||||
net: new
|
||||
runtime:
|
||||
|
@ -107,7 +107,7 @@ onboot:
|
|||
bindNS:
|
||||
net: /run/netns/fic-generator
|
||||
- name: mysql-ip-setup
|
||||
image: linuxkit/ip:9696394a7d57b384ae919662ae162c9152029156
|
||||
image: linuxkit/ip:v1.0.0
|
||||
command: ["/bin/sh", "-c", "ip a add 172.17.0.4/24 dev vethin-db; ip link set vethin-db up;" ]
|
||||
net: new
|
||||
runtime:
|
||||
|
@ -118,7 +118,7 @@ onboot:
|
|||
bindNS:
|
||||
net: /run/netns/db
|
||||
- name: bridge-setup
|
||||
image: linuxkit/ip:9696394a7d57b384ae919662ae162c9152029156
|
||||
image: linuxkit/ip:v1.0.0
|
||||
command: ["/bin/sh", "-c", "ip a add 172.17.0.1/24 dev br0; ip link set veth-admin master br0; ip link set veth-checker master br0; ip link set veth-generator master br0; ip link set veth-db master br0; ip link set veth-qa master br0; ip link set br0 up; ip link set veth-admin up; ip link set veth-checker up; ip link set veth-generator up; ip link set veth-db up; ip link set veth-qa up;" ]
|
||||
runtime:
|
||||
interfaces:
|
||||
|
@ -126,7 +126,7 @@ onboot:
|
|||
add: bridge
|
||||
|
||||
- name: firewall-synchro
|
||||
image: linuxkit/ip:9696394a7d57b384ae919662ae162c9152029156
|
||||
image: linuxkit/ip:v1.0.0
|
||||
command: ["/bin/bash", "-c", "/sbin/iptables-restore < /etc/iptables/rules-synchro.v4; /sbin/ip6tables-restore < /etc/iptables/rules.v6" ]
|
||||
binds:
|
||||
- /etc/iptables/rules-synchro.v4:/etc/iptables/rules-synchro.v4:ro
|
||||
|
@ -136,7 +136,7 @@ onboot:
|
|||
mkdir:
|
||||
- /var/lib/fic/teams
|
||||
- name: firewall-admin
|
||||
image: linuxkit/ip:9696394a7d57b384ae919662ae162c9152029156
|
||||
image: linuxkit/ip:v1.0.0
|
||||
command: ["/bin/bash", "-c", "/sbin/iptables-restore < /etc/iptables/rules-admin.v4; /sbin/ip6tables-restore < /etc/iptables/rules.v6" ]
|
||||
binds:
|
||||
- /etc/iptables/rules-admin.v4:/etc/iptables/rules-admin.v4:ro
|
||||
|
@ -144,7 +144,7 @@ onboot:
|
|||
net: /run/netns/fic-admin
|
||||
|
||||
- name: create-secrets
|
||||
image: alpine:3.21
|
||||
image: alpine:3.19
|
||||
command: ["/bin/init_secrets.sh"]
|
||||
binds:
|
||||
- /bin/init_secrets.sh:/bin/init_secrets.sh:ro
|
||||
|
@ -153,26 +153,17 @@ onboot:
|
|||
mkdir:
|
||||
- /var/lib/fic/secrets
|
||||
|
||||
- name: create-ssh-keys
|
||||
image: nemunaire/rsync:a3d76b2dd0a9ad73be44dc77ad765b20d96a3285
|
||||
command: ["/bin/sh", "-c", "touch /etc/ssh/sshd_config && ssh-keygen -A"]
|
||||
binds:
|
||||
- /var/lib/fic/ssh:/etc/ssh
|
||||
runtime:
|
||||
mkdir:
|
||||
- /var/lib/fic/ssh
|
||||
|
||||
services:
|
||||
# - name: getty
|
||||
# image: linuxkit/getty:05eca453695984a69617f1f1f0bcdae7f7032967
|
||||
# image: linuxkit/getty:v1.0.0
|
||||
# env:
|
||||
# - INSECURE=true
|
||||
|
||||
# Enable acpi to shutdown on power events
|
||||
- name: acpid
|
||||
image: linuxkit/acpid:6cb5575e487a8fcbd4c3eb6721c23299e6ea452f
|
||||
image: linuxkit/acpid:v1.0.0
|
||||
- name: rngd
|
||||
image: linuxkit/rngd:1a18f2149e42a0a1cb9e7d37608a494342c26032
|
||||
image: linuxkit/rngd:v1.0.0
|
||||
- name: db
|
||||
image: mariadb:11
|
||||
command: ["/bin/bash", "/usr/local/bin/docker-entrypoint.sh", "mariadbd"]
|
||||
|
@ -237,7 +228,7 @@ services:
|
|||
- /var/lib/fic/generator:/srv/GENERATOR:ro
|
||||
- /var/lib/fic/pki:/srv/PKI
|
||||
- /var/lib/fic/settings:/srv/SETTINGS
|
||||
- /var/lib/fic/submissions:/srv/submissions
|
||||
- /var/lib/fic/submissions:/srv/submissions:ro
|
||||
- /var/lib/fic/sync:/srv/SYNC
|
||||
- /var/lib/fic/teams:/srv/TEAMS
|
||||
net: /run/netns/fic-admin
|
||||
|
@ -278,10 +269,7 @@ services:
|
|||
binds:
|
||||
- /etc/hosts:/etc/hosts:ro
|
||||
- /var/lib/fic/generator:/srv/GENERATOR:ro
|
||||
# Uncomment this to disallow registrations
|
||||
- /var/lib/fic/teams:/srv/TEAMS:ro
|
||||
# Uncomment this to allow registrations
|
||||
#- /var/lib/fic/teams:/srv/TEAMS
|
||||
- /var/lib/fic/secrets/mysql_password:/run/secrets/mysql_password:ro
|
||||
- /var/lib/fic/settingsdist:/srv/SETTINGSDIST:ro
|
||||
- /var/lib/fic/submissions:/srv/submissions
|
||||
|
@ -373,6 +361,7 @@ services:
|
|||
- /var/lib/fic/files
|
||||
- /var/lib/fic/pki/shared
|
||||
- /var/lib/fic/settingsdist
|
||||
- /var/lib/fic/ssh
|
||||
- /var/lib/fic/submissions
|
||||
- /var/lib/fic/teams
|
||||
- /var/log/frontend
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
kernel:
|
||||
#image: nemunaire/kernel:5.10.62-0b705d955f5e283f62583c4e227d64a7924c138f-amd64
|
||||
image: linuxkit/kernel:6.6.71
|
||||
image: linuxkit/kernel:6.6.13
|
||||
cmdline: "console=ttyS0 console=tty0"
|
||||
|
||||
init:
|
||||
|
|
|
@ -1,50 +1,50 @@
|
|||
kernel:
|
||||
#image: nemunaire/kernel:5.10.62-0b705d955f5e283f62583c4e227d64a7924c138f-amd64
|
||||
image: linuxkit/kernel:6.6.71
|
||||
image: linuxkit/kernel:6.6.13
|
||||
cmdline: "console=ttyS0 console=tty0"
|
||||
|
||||
init:
|
||||
- linuxkit/init:8eea386739975a43af558eec757a7dcb3a3d2e7b
|
||||
- linuxkit/runc:667e7ea2c426a2460ca21e3da065a57dbb3369c9
|
||||
- linuxkit/containerd:a988a1a8bcbacc2c0390ca0c08f949e2b4b5915d
|
||||
- linuxkit/ca-certificates:7b32a26ca9c275d3ef32b11fe2a83dbd2aee2fdb
|
||||
- linuxkit/getty:05eca453695984a69617f1f1f0bcdae7f7032967
|
||||
- linuxkit/init:v1.0.0
|
||||
- linuxkit/runc:6062483d748609d505f2bcde4e52ee64a3329f5f
|
||||
- linuxkit/containerd:v1.0.0
|
||||
- linuxkit/ca-certificates:v1.0.0
|
||||
- linuxkit/getty:v1.0.0
|
||||
- nemunaire/mdadm:04814350d71ba9417e1f861be1685de26adf7a67
|
||||
- nemunaire/kexec:839b4eedfce02a56c581dec2383dc6faff120855
|
||||
- nemunaire/fic-frontend-ui:latest
|
||||
|
||||
onboot:
|
||||
- name: mod
|
||||
image: linuxkit/modprobe:773ee174006ecbb412830e48889795bae40b62f9
|
||||
image: linuxkit/modprobe:v1.0.0
|
||||
command: ["/bin/sh", "-c", "modprobe xhci_pci ahci intel_lpss_pci i2c_i801 megaraid_sas tg3 bnxt_en"]
|
||||
|
||||
- name: sysctl
|
||||
image: linuxkit/sysctl:5f56434b81004b50b47ed629b222619168c2bcdf
|
||||
image: linuxkit/sysctl:v1.0.0
|
||||
|
||||
# Metadata
|
||||
- name: metadata
|
||||
image: linuxkit/metadata:4f81c0c3a2b245567fd7d32d799018c9614a9907
|
||||
image: linuxkit/metadata:v1.0.0
|
||||
command: ["/usr/bin/metadata", "-v", "cdrom"]
|
||||
|
||||
# Filesystem
|
||||
- name: swap
|
||||
image: linuxkit/swap:f4b8ffef87c8c72165bd8a92b790ac252ccf1821
|
||||
image: linuxkit/swap:v1.0.0
|
||||
command: ["/sbin/swapon", "/dev/sda3"]
|
||||
- name: dm-crypt
|
||||
image: linuxkit/dm-crypt:981fde241bb84616a5ba94c04cdefa1489431a25
|
||||
image: linuxkit/dm-crypt:d49723bc9d10c5ada9e03b0670f4e57416d5d084
|
||||
command: ["/usr/bin/crypto", "-l", "crypt_fic", "/dev/sda4"]
|
||||
binds:
|
||||
- /dev:/dev
|
||||
- /run/config/dm-crypt:/etc/dm-crypt
|
||||
- name: mount
|
||||
image: linuxkit/mount:cb8caa72248f7082fc2074ce843d53cdc15df04a
|
||||
image: linuxkit/mount:v1.0.0
|
||||
command: ["/usr/bin/mountie", "-device", "/dev/mapper/crypt_fic", "/var/lib/fic" ]
|
||||
|
||||
# Network
|
||||
# - name: ntp
|
||||
# image: linuxkit/openntpd:f99c4117763480815553b72022b426639a13ce86
|
||||
# image: linuxkit/openntpd:v1.0.0
|
||||
- name: nginx-ip-setup
|
||||
image: linuxkit/ip:9696394a7d57b384ae919662ae162c9152029156
|
||||
image: linuxkit/ip:v1.0.0
|
||||
command: ["/bin/sh", "-c", "ip a add 172.17.1.2/24 dev vethin-nginx; ip link set vethin-nginx up;" ]
|
||||
net: new
|
||||
runtime:
|
||||
|
@ -55,7 +55,7 @@ onboot:
|
|||
bindNS:
|
||||
net: /run/netns/nginx
|
||||
- name: frontal-ip-setup # without bonding
|
||||
image: linuxkit/ip:9696394a7d57b384ae919662ae162c9152029156
|
||||
image: linuxkit/ip:v1.0.0
|
||||
command: ["/bin/sh", "-c", "ip link set name bond-frontal eth3; ip link set bond-frontal up; while read IP; do ip a add ${IP} dev bond-frontal; done < /run/config/ip_config/frontend-players; ip r add default via $(cat /run/config/ip_config/frontend-router); ip link add link bond-frontal name internet type vlan id 4; ip a add 10.10.10.2/29 dev internet; ip link set internet up;" ]
|
||||
net: /run/netns/nginx
|
||||
binds:
|
||||
|
@ -67,7 +67,7 @@ onboot:
|
|||
- name: eth3
|
||||
# - name: eth4
|
||||
# - name: frontal-ip-setup # with bonding
|
||||
# image: linuxkit/ip:9696394a7d57b384ae919662ae162c9152029156
|
||||
# image: linuxkit/ip:v1.0.0
|
||||
# command: ["/bin/sh", "-c", "ip link set dev bond-frontal type bond mode balance-alb; ip link set bond-frontal up; ifenslave bond-frontal eth1 eth2 eth3 eth4; while read IP; do ip a add ${IP} dev bond-frontal; done < /run/config/ip_config/frontend-players; ip r add default via $(cat /run/config/ip_config/frontend-router); ip link add link bond-frontal name internet type vlan id 4; ip link set internet up; sysctl -w net.ipv4.ip_forward=1;" ]
|
||||
# net: /run/netns/nginx
|
||||
# binds:
|
||||
|
@ -81,7 +81,7 @@ onboot:
|
|||
# - name: bond-frontal
|
||||
# add: bond
|
||||
- name: receiver-ip-setup
|
||||
image: linuxkit/ip:9696394a7d57b384ae919662ae162c9152029156
|
||||
image: linuxkit/ip:v1.0.0
|
||||
command: ["/bin/sh", "-c", "ip a add 172.17.1.3/24 dev vethin-receiver; ip link set vethin-receiver up;" ]
|
||||
net: new
|
||||
runtime:
|
||||
|
@ -92,7 +92,7 @@ onboot:
|
|||
bindNS:
|
||||
net: /run/netns/fic-receiver
|
||||
- name: sshd-ip-setup
|
||||
image: linuxkit/ip:9696394a7d57b384ae919662ae162c9152029156
|
||||
image: linuxkit/ip:v1.0.0
|
||||
command: ["/bin/sh", "-c", "ip a add 10.10.10.2/29 dev eth2; ip link set eth2 up;" ]
|
||||
net: new
|
||||
runtime:
|
||||
|
@ -101,7 +101,7 @@ onboot:
|
|||
bindNS:
|
||||
net: /run/netns/sshd
|
||||
- name: auth-ip-setup
|
||||
image: linuxkit/ip:9696394a7d57b384ae919662ae162c9152029156
|
||||
image: linuxkit/ip:v1.0.0
|
||||
command: ["/bin/sh", "-c", "ip a add 172.17.1.4/24 dev vethin-auth; ip link set vethin-auth up;" ]
|
||||
net: new
|
||||
runtime:
|
||||
|
@ -112,7 +112,7 @@ onboot:
|
|||
bindNS:
|
||||
net: /run/netns/auth
|
||||
- name: bridge-setup
|
||||
image: linuxkit/ip:9696394a7d57b384ae919662ae162c9152029156
|
||||
image: linuxkit/ip:v1.0.0
|
||||
command: ["/bin/sh", "-c", "ip a add 172.17.1.1/24 dev br0; ip link set veth-nginx master br0; ip link set veth-receiver master br0; ip link set veth-auth master br0; ip link set br0 up; ip link set veth-nginx up; ip link set veth-receiver up; ip link set veth-auth up;" ]
|
||||
runtime:
|
||||
interfaces:
|
||||
|
@ -120,7 +120,7 @@ onboot:
|
|||
add: bridge
|
||||
|
||||
- name: firewall-frontal
|
||||
image: linuxkit/ip:9696394a7d57b384ae919662ae162c9152029156
|
||||
image: linuxkit/ip:v1.0.0
|
||||
command: ["/bin/bash", "-c", "/sbin/iptables-restore < /etc/iptables/rules-frontal.v4; /sbin/ip6tables-restore < /etc/iptables/rules.v6; [ -f /run/config/remote_sync/destination ] && /sbin/iptables -I OUTPUT 7 -o bond-frontal -d $(cat /run/config/remote_sync/destination | tr -d '\n') -p tcp -m tcp --dport https -j ACCEPT;" ]
|
||||
binds:
|
||||
- /etc/iptables/rules-frontal.v4:/etc/iptables/rules-frontal.v4:ro
|
||||
|
@ -129,35 +129,26 @@ onboot:
|
|||
- /run/config/remote_sync/:/run/config/remote_sync/:ro
|
||||
net: /run/netns/nginx
|
||||
- name: firewall-sshd
|
||||
image: linuxkit/ip:9696394a7d57b384ae919662ae162c9152029156
|
||||
image: linuxkit/ip:v1.0.0
|
||||
command: ["/bin/bash", "-c", "/sbin/iptables-restore < /etc/iptables/rules-sshd.v4; /sbin/ip6tables-restore < /etc/iptables/rules.v6" ]
|
||||
binds:
|
||||
- /etc/iptables/rules-sshd.v4:/etc/iptables/rules-sshd.v4:ro
|
||||
- /etc/iptables/rules.v6:/etc/iptables/rules.v6:ro
|
||||
net: /run/netns/sshd
|
||||
|
||||
- name: create-ssh-keys
|
||||
image: nemunaire/rsync:a3d76b2dd0a9ad73be44dc77ad765b20d96a3285
|
||||
command: ["/bin/sh", "-c", "touch /etc/ssh/sshd_config && ssh-keygen -A"]
|
||||
binds:
|
||||
- /var/lib/fic/ssh:/etc/ssh
|
||||
runtime:
|
||||
mkdir:
|
||||
- /var/lib/fic/ssh
|
||||
|
||||
services:
|
||||
# - name: getty
|
||||
# image: linuxkit/getty:05eca453695984a69617f1f1f0bcdae7f7032967
|
||||
# image: linuxkit/getty:v1.0.0
|
||||
# env:
|
||||
# - INSECURE=true
|
||||
|
||||
# Enable acpi to shutdown on power events
|
||||
- name: acpid
|
||||
image: linuxkit/acpid:6cb5575e487a8fcbd4c3eb6721c23299e6ea452f
|
||||
image: linuxkit/acpid:v1.0.0
|
||||
- name: rngd
|
||||
image: linuxkit/rngd:1a18f2149e42a0a1cb9e7d37608a494342c26032
|
||||
image: linuxkit/rngd:v1.0.0
|
||||
- name: dhcpcd
|
||||
image: linuxkit/dhcpcd:157df9ef45a035f1542ec2270e374f18efef98a5
|
||||
image: linuxkit/dhcpcd:v1.0.0
|
||||
net: /run/netns/nginx
|
||||
binds:
|
||||
- /etc/dhcpcd.conf:/dhcpcd.conf:ro
|
||||
|
@ -266,6 +257,7 @@ services:
|
|||
- /var/lib/fic/files
|
||||
- /var/lib/fic/pki
|
||||
- /var/lib/fic/settingsdist
|
||||
- /var/lib/fic/ssh
|
||||
- /var/lib/fic/submissions
|
||||
- /var/lib/fic/teams
|
||||
|
||||
|
@ -288,7 +280,7 @@ services:
|
|||
# net: /run/netns/nginx
|
||||
|
||||
- name: dexidp
|
||||
image: ghcr.io/dexidp/dex:v2.42.0
|
||||
image: ghcr.io/dexidp/dex:v2.39.0
|
||||
net: /run/netns/auth
|
||||
binds:
|
||||
- /etc/hosts:/etc/hosts:ro
|
||||
|
@ -302,7 +294,7 @@ services:
|
|||
mkdir:
|
||||
- /var/lib/fic/dex
|
||||
- name: vouch-proxy
|
||||
image: quay.io/vouch/vouch-proxy:alpine-0.41
|
||||
image: quay.io/vouch/vouch-proxy:alpine-0.39
|
||||
env:
|
||||
- VOUCH_CONFIG=/etc/vouch/config.yml
|
||||
net: /run/netns/auth
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
kernel:
|
||||
#image: nemunaire/kernel:5.10.62-0b705d955f5e283f62583c4e227d64a7924c138f-amd64
|
||||
image: linuxkit/kernel:6.6.71
|
||||
image: linuxkit/kernel:6.6.13
|
||||
cmdline: "console=ttyS0 console=tty0"
|
||||
|
||||
|
||||
init:
|
||||
- nemunaire/mdadm:04814350d71ba9417e1f861be1685de26adf7a67
|
||||
- nemunaire/syslinux:086f221f281d577d300949aa1094fb20c5cd90dc
|
||||
- linuxkit/format:3fb088f60ed73ba4a15be41e44654b74112fd3f9
|
||||
- linuxkit/dm-crypt:981fde241bb84616a5ba94c04cdefa1489431a25
|
||||
- linuxkit/metadata:4f81c0c3a2b245567fd7d32d799018c9614a9907
|
||||
- linuxkit/format:v1.0.0
|
||||
- linuxkit/dm-crypt:d49723bc9d10c5ada9e03b0670f4e57416d5d084
|
||||
- linuxkit/metadata:v1.0.0
|
||||
- alpine:latest
|
||||
|
||||
files:
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
kernel:
|
||||
#image: nemunaire/kernel:5.10.62-0b705d955f5e283f62583c4e227d64a7924c138f-amd64
|
||||
image: linuxkit/kernel:6.6.71
|
||||
image: linuxkit/kernel:6.6.13
|
||||
cmdline: "console=ttyS0 console=tty0"
|
||||
|
||||
|
||||
init:
|
||||
- nemunaire/mdadm:04814350d71ba9417e1f861be1685de26adf7a67
|
||||
- linuxkit/metadata:4f81c0c3a2b245567fd7d32d799018c9614a9907
|
||||
- linuxkit/metadata:v1.0.0
|
||||
- alpine:latest
|
||||
|
||||
|
||||
|
|
1
fileexporter/.gitignore
vendored
1
fileexporter/.gitignore
vendored
|
@ -1 +0,0 @@
|
|||
fileexporter
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
2097
frontend/fic/package-lock.json
generated
2097
frontend/fic/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -12,14 +12,14 @@
|
|||
"@sveltejs/adapter-static": "^3.0.0",
|
||||
"@sveltejs/kit": "^2.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||
"@sveltestrap/sveltestrap": "^7.0.0",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-config-prettier": "^10.0.0",
|
||||
"eslint-plugin-svelte": "^3.0.0",
|
||||
"@sveltestrap/sveltestrap": "^6.2.1",
|
||||
"eslint": "^8.4.2",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-svelte": "^2.35.1",
|
||||
"prettier": "^3.0.0",
|
||||
"prettier-plugin-svelte": "^3.1.2",
|
||||
"sass": "^1.51.0",
|
||||
"sass-loader": "^16.0.0",
|
||||
"sass-loader": "^14.0.0",
|
||||
"svelte": "^4.0.0",
|
||||
"vite": "^5.0.0"
|
||||
},
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
style:filter={theme.locked ? "grayscale(60%)":null}
|
||||
></div>
|
||||
{/if}
|
||||
<CardBody>
|
||||
<CardBody class="text-indent">
|
||||
{#if exercice}
|
||||
{#if $exercices_idx[exercice.id].tags.includes("Reverse") || $exercices_idx[exercice.id].tags.includes("Reverse Engineering")}
|
||||
<Badge class="float-end">#Reverse</Badge>
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
ListGroupItem,
|
||||
} from '@sveltestrap/sveltestrap';
|
||||
|
||||
import { hasDownloaded } from '$lib/stores/downloaded.js';
|
||||
import FileSize from './FileSize.svelte';
|
||||
|
||||
export let files = [];
|
||||
|
@ -28,15 +27,15 @@
|
|||
</CardBody>
|
||||
<ListGroup flush class="border-secondary">
|
||||
{#each files as file, index}
|
||||
<ListGroupItem tag="a" href={file.path} target={(file.name.endsWith(".txt") || file.name.endsWith(".xml") || file.name.endsWith(".jpg") || file.name.endsWith(".png") || file.name.endsWith(".pdf"))?"_blank":"_self"} class="d-flex" action on:click={() => hasDownloaded.update((u) => {u[file.path] = true; return u;})}>
|
||||
<h1 class="me-3" class:text-info={!$hasDownloaded[file.path]}>
|
||||
<ListGroupItem tag="a" href={file.path} target={(file.name.endsWith(".txt") || file.name.endsWith(".jpg") || file.name.endsWith(".png") || file.name.endsWith(".pdf"))?"_blank":"_self"} class="d-flex">
|
||||
<h1 class="me-3">
|
||||
<Icon name="arrow-down-circle" />
|
||||
</h1>
|
||||
<div style="min-width: 0">
|
||||
<h4 class="fw-bold"><samp>{file.name}</samp></h4>
|
||||
{#if file.disclaimer}
|
||||
<div class="file-disclaimer text-warning">
|
||||
{file.disclaimer}
|
||||
{#if file.disclamer}
|
||||
<div class="file-disclamer text-warning">
|
||||
{file.disclamer}
|
||||
</div>
|
||||
{/if}
|
||||
<nobr>
|
||||
|
@ -62,10 +61,10 @@
|
|||
{/if}
|
||||
|
||||
<style>
|
||||
.file-disclaimer {
|
||||
.file-disclamer {
|
||||
display: none;
|
||||
}
|
||||
:global(.list-group-item:hover .file-disclaimer) {
|
||||
:global(.list-group-item:hover .file-disclamer) {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
|
||||
import { my } from '$lib/stores/my.js';
|
||||
import { settings } from '$lib/stores/settings.js';
|
||||
import { submissions } from '$lib/stores/submissions.js';
|
||||
import { timeouted, waitDiff, waitInProgress } from '$lib/wait.js';
|
||||
|
||||
import DateFormat from './DateFormat.svelte';
|
||||
|
@ -29,32 +28,11 @@
|
|||
export let readonly = false;
|
||||
|
||||
export let forcesolved = false;
|
||||
let last_submission = { };
|
||||
let responses = { };
|
||||
async function submitFlags() {
|
||||
waitInProgress.set(true);
|
||||
sberr = "";
|
||||
message = "";
|
||||
last_submission = JSON.parse(JSON.stringify(responses));
|
||||
|
||||
submissions.update((u) => {
|
||||
for (const k in last_submission.flags) {
|
||||
if (last_submission.flags[k])
|
||||
u.flags[k] = last_submission.flags[k];
|
||||
}
|
||||
|
||||
for (const k in last_submission.mcqs) {
|
||||
if (last_submission.mcqs[k])
|
||||
u.mcqs[k] = last_submission.mcqs[k];
|
||||
}
|
||||
|
||||
for (const k in last_submission.justifications) {
|
||||
if (last_submission.justifications[k])
|
||||
u.justifications[k] = last_submission.justifications[k];
|
||||
}
|
||||
|
||||
return u;
|
||||
})
|
||||
|
||||
if ($my && $my.team_id === 0) {
|
||||
let allGoodResponse = true;
|
||||
|
@ -141,11 +119,6 @@
|
|||
mcqs: { },
|
||||
justifications: { },
|
||||
};
|
||||
last_submission = {
|
||||
flags: { },
|
||||
mcqs: { },
|
||||
justifications: { },
|
||||
};
|
||||
}
|
||||
|
||||
let last_exercice = null;
|
||||
|
@ -179,12 +152,10 @@
|
|||
{#if exercice.tries || exercice.solved_time || exercice.submitted || sberr || $timeouted}
|
||||
<ListGroup class="border-dark">
|
||||
{#if exercice.solved_time || exercice.tries}
|
||||
<div class="d-flex align-items-center">
|
||||
<ListGroupItem class="rounded-0 {$waitInProgress?'text-secondary':'text-warning'}">
|
||||
{#if exercice.tries > 0}{exercice.tries} {exercice.tries==1?"tentative effectuée":"tentatives effectuées"}.{/if}
|
||||
Dernière solution envoyée le <span class:placeholder={$waitInProgress} class:placeholder-glow={$waitInProgress}><DateFormat date={exercice.solved_time} /></span>.
|
||||
</ListGroupItem>
|
||||
</div>
|
||||
<ListGroupItem class="text-warning rounded-0">
|
||||
{#if exercice.tries > 0}{exercice.tries} {exercice.tries==1?"tentative effectuée":"tentatives effectuées"}.{/if}
|
||||
Dernière solution envoyée à <DateFormat date={exercice.solved_time} />.
|
||||
</ListGroupItem>
|
||||
{/if}
|
||||
{#if exercice.solve_dist}
|
||||
<ListGroupItem class="rounded-0">
|
||||
|
@ -220,7 +191,6 @@
|
|||
<FlagMCQ
|
||||
exercice_id={exercice.id}
|
||||
{flag}
|
||||
previous_values={$waitInProgress ? { justifications: { }, mcqs: { } } : last_submission}
|
||||
bind:values={responses.mcqs}
|
||||
bind:justifications={responses.justifications}
|
||||
/>
|
||||
|
@ -229,7 +199,6 @@
|
|||
class="mb-3"
|
||||
exercice_id={exercice.id}
|
||||
{flag}
|
||||
previous_value={$waitInProgress ? "" : last_submission.flags[flag.id]}
|
||||
bind:value={responses.flags[flag.id]}
|
||||
/>
|
||||
{/if}
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
|
||||
import { my } from '$lib/stores/my.js';
|
||||
import { settings } from '$lib/stores/settings.js';
|
||||
import { submissions } from '$lib/stores/submissions.js';
|
||||
|
||||
export { className as class };
|
||||
let className = '';
|
||||
|
@ -17,7 +16,6 @@
|
|||
export let exercice_id = 0;
|
||||
export let flag = { };
|
||||
export let no_label = false;
|
||||
export let previous_value = "";
|
||||
export let value = "";
|
||||
let values = [""];
|
||||
|
||||
|
@ -143,7 +141,6 @@
|
|||
<input
|
||||
type="number"
|
||||
class="form-control flag"
|
||||
class:is-invalid={previous_value && previous_value == value}
|
||||
id="sol_{flag.type}{flag.id}_{index}"
|
||||
autocomplete="off"
|
||||
bind:value={values[index]}
|
||||
|
@ -157,7 +154,6 @@
|
|||
<input
|
||||
type="text"
|
||||
class="form-control flag"
|
||||
class:is-invalid={previous_value && previous_value == value}
|
||||
id="sol_{flag.type}{flag.id}_{index}"
|
||||
autocomplete="off"
|
||||
bind:value={values[index]}
|
||||
|
@ -168,7 +164,6 @@
|
|||
{:else}
|
||||
<textarea
|
||||
class="form-control flag"
|
||||
class:is-invalid={previous_value && previous_value == value}
|
||||
id="sol_{flag.type}{flag.id}_{index}"
|
||||
autocomplete="off"
|
||||
bind:value={values[index]}
|
||||
|
@ -213,7 +208,6 @@
|
|||
value={l}
|
||||
bind:group={values[index]}
|
||||
class="form-check-input"
|
||||
class:is-invalid={previous_value && previous_value == value}
|
||||
>
|
||||
<label
|
||||
class="form-check-label"
|
||||
|
@ -226,7 +220,6 @@
|
|||
{:else}
|
||||
<select
|
||||
class="form-select"
|
||||
class:is-invalid={previous_value && previous_value == value}
|
||||
id="sol_{flag.type}{flag.id}_{index}"
|
||||
bind:value={values[index]}
|
||||
>
|
||||
|
@ -248,15 +241,6 @@
|
|||
title="Flag trouvé à {flag.found}"
|
||||
value={value}
|
||||
>
|
||||
{:else if $submissions && $submissions.flags && $submissions.flags[flag.id]}
|
||||
<input
|
||||
class="form-control is-valid"
|
||||
disabled
|
||||
id="sol_{flag.type}{flag.id}_0"
|
||||
type="text"
|
||||
title="Flag trouvé à {flag.found}"
|
||||
value={$submissions.flags[flag.id]}
|
||||
>
|
||||
{:else}
|
||||
<Icon
|
||||
name="check"
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
|
||||
export let exercice_id = 0;
|
||||
export let flag = { };
|
||||
export let previous_values = { justifications: { }, mcqs: { } };
|
||||
export let values = { };
|
||||
export let justifications = { };
|
||||
</script>
|
||||
|
@ -25,7 +24,7 @@
|
|||
{#each Object.keys(flag.choices) as cid, index}
|
||||
<div class="form-check ms-3">
|
||||
{#if typeof flag.choices[cid] != "object"}
|
||||
<input class="form-check-input" class:is-invalid={previous_values.mcqs && Object.keys(flag.choices).reduce((acc, cur) => acc + (previous_values.mcqs[Number(cur)] !== undefined ? 1 : 0), 0) > 0 && Object.keys(flag.choices).reduce((acc, cur) => acc && previous_values.mcqs[Number(cur)] == values[Number(cur)], true)} type="checkbox" id="mcq_{flag.id}_{cid}" bind:checked={values[Number(cid)]} disabled={flag.found || flag.part_solved}>
|
||||
<input class="form-check-input" type="checkbox" id="mcq_{flag.id}_{cid}" bind:checked={values[Number(cid)]} disabled={flag.found || flag.part_solved}>
|
||||
<label class="form-check-label" for="mcq_{flag.id}_{cid}">
|
||||
{flag.choices[cid]}{#if values[Number(cid)] && flag.justify} :{/if}
|
||||
</label>
|
||||
|
@ -35,7 +34,6 @@
|
|||
{exercice_id}
|
||||
flag={{id: cid, placeholder: "Flag correspondant"}}
|
||||
no_label={true}
|
||||
previous_value={previous_values.justifications && previous_values.justifications[cid]}
|
||||
bind:value={justifications[cid]}
|
||||
/>
|
||||
{/if}
|
||||
|
@ -45,7 +43,6 @@
|
|||
class={flag.choices[cid].justification.found?"":"mb-3"}
|
||||
{exercice_id}
|
||||
flag={flag.choices[cid].justification}
|
||||
previous_value={previous_values.justifications[cid]}
|
||||
bind:value={justifications[cid]}
|
||||
/>
|
||||
{/if}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<script>
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
import {
|
||||
Badge,
|
||||
|
@ -50,7 +49,7 @@
|
|||
<a href="." style="max-width: 50%">
|
||||
{#if $challengeInfo && $challengeInfo.main_logo}
|
||||
{#each $challengeInfo.main_logo as logo, i}
|
||||
<img src={logo.replace('$FILES$/', base + '/files/')} alt={'Logo principal #' + i} class={'h-100' + (i > 0?' d-none d-md-inline ms-2':'')}>
|
||||
<img src={logo.replace('$FILES$', base + '/files/')} alt={'Logo principal #' + i} class={'h-100' + (i > 0?' d-none d-md-inline ms-2':'')}>
|
||||
{/each}
|
||||
{/if}
|
||||
</a>
|
||||
|
@ -63,7 +62,7 @@
|
|||
<NavbarToggler on:click={() => (isOpen = !isOpen)} />
|
||||
<Collapse {isOpen} navbar expand="md" on:update={handleUpdate}>
|
||||
<Nav navbar>
|
||||
<NavItem active={$page.route && $page.route.id === "/"}>
|
||||
<NavItem>
|
||||
<NavLink href=".">
|
||||
<Icon name="house" />
|
||||
Accueil
|
||||
|
@ -72,7 +71,7 @@
|
|||
<NavThemes />
|
||||
<NavTags />
|
||||
{#if $settings && $settings.end - $settings.start > 0 && $teams && Object.keys($teams).length}
|
||||
<NavItem active={$page.route && $page.route.id === "/rank"}>
|
||||
<NavItem>
|
||||
<NavLink href="rank">
|
||||
<Icon name="sort-down" />
|
||||
Classement
|
||||
|
@ -80,7 +79,7 @@
|
|||
</NavItem>
|
||||
{/if}
|
||||
<HeaderIssues />
|
||||
<NavItem active={$page.route && $page.route.id === "/rules"}>
|
||||
<NavItem>
|
||||
<NavLink href="rules">
|
||||
<Icon name="signpost-split" />
|
||||
Aide
|
||||
|
@ -95,7 +94,7 @@
|
|||
<Nav class="ms-auto text-light" navbar>
|
||||
{#if $my && $my.team_id}
|
||||
<NavItem>
|
||||
{$my.score100/100} {Math.abs($my.score) < 2 ? 'point' : 'points'}
|
||||
{Math.round($my.score*100)/100} {$my.score === 1 ? 'point' : 'points'}
|
||||
{#if $teams && $teams[$my.team_id] && $teams[$my.team_id].rank}
|
||||
– {$teams[$my.team_id].rank}<sup>e</sup> sur {Object.keys($teams).length}
|
||||
{/if}
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
<script>
|
||||
import { page } from '$app/stores';
|
||||
|
||||
import {
|
||||
Badge,
|
||||
Icon,
|
||||
|
@ -26,7 +24,7 @@
|
|||
</script>
|
||||
|
||||
{#if $issues.length}
|
||||
<NavItem active={$page.route && $page.route.id === "/issues"}>
|
||||
<NavItem>
|
||||
<NavLink href="issues">
|
||||
<Icon name="bug" />
|
||||
Problèmes
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
<script>
|
||||
import { page } from '$app/stores';
|
||||
|
||||
import {
|
||||
Badge,
|
||||
Dropdown,
|
||||
|
@ -16,7 +14,7 @@
|
|||
let filter = "";
|
||||
</script>
|
||||
|
||||
<Dropdown nav inNavbar active={$page.params.tag}>
|
||||
<Dropdown nav inNavbar>
|
||||
<DropdownToggle nav caret>
|
||||
<Icon name="tags" />
|
||||
Tags
|
||||
|
@ -29,8 +27,8 @@
|
|||
bind:value={filter}
|
||||
>
|
||||
<div>
|
||||
{#each Object.keys($tags).sort(function (a, b) { return a.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().localeCompare(b.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase()); }) as itag, index}
|
||||
{#if (filter === "" && $tags[itag].count > 1) || (filter !== "" && itag.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().indexOf(filter.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase()) >= 0)}
|
||||
{#each Object.keys($tags).sort(function (a, b) { return a.toLowerCase().localeCompare(b.toLowerCase()); }) as itag, index}
|
||||
{#if (filter === "" && $tags[itag].count > 1) || (filter !== "" && itag.toLowerCase().indexOf(filter.toLowerCase()) >= 0)}
|
||||
<DropdownItem href="tags/{itag}">
|
||||
#{itag}
|
||||
<Badge>
|
||||
|
|
|
@ -14,7 +14,6 @@
|
|||
import { myThemes, themes } from '$lib/stores/mythemes.js';
|
||||
</script>
|
||||
|
||||
{#if $themes.length > 0 && ($themes[0].id != 0 || $themes.length > 1)}
|
||||
<Dropdown nav inNavbar active={$current_theme && $current_theme.id != 0}>
|
||||
<DropdownToggle nav caret>
|
||||
<Icon name="tv" />
|
||||
|
@ -55,7 +54,6 @@
|
|||
</div>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
{/if}
|
||||
{#if $themesStore && $themesStore["0"] && $themesStore["0"].exercices}
|
||||
<Dropdown nav inNavbar active={$current_theme && $current_theme && $current_theme.id == 0}>
|
||||
<DropdownToggle nav caret>
|
||||
|
@ -67,7 +65,7 @@
|
|||
{#each $themesStore["0"].exercices as exercice, index}
|
||||
<DropdownItem href="{$themesStore["0"].urlid}/{exercice.urlid}" active={$current_theme && $current_theme.id == 0 && $current_exercice && $current_exercice.id == exercice.id}>
|
||||
{exercice.title}
|
||||
{#if $my && $my.id_team && exercice.solved}
|
||||
{#if exercice.solved}
|
||||
<Badge color="success" pill>
|
||||
<Icon name="check" />
|
||||
</Badge>
|
||||
|
|
|
@ -30,12 +30,6 @@
|
|||
partJ = true;
|
||||
}
|
||||
|
||||
function JchangeTeam() {
|
||||
value = { };
|
||||
|
||||
partJ = false;
|
||||
}
|
||||
|
||||
function submit(event) {
|
||||
if (!partJ) {
|
||||
JvalidateTeam();
|
||||
|
@ -68,11 +62,7 @@
|
|||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if partJ}
|
||||
<Button color="info" type="button" on:click={JchangeTeam}>Changer</Button>
|
||||
{:else}
|
||||
<Button color="info" type="button" on:click={JvalidateTeam} disabled={partJ}>Valider</Button>
|
||||
{/if}
|
||||
<Button color="info" type="button" on:click={JvalidateTeam} disabled={partJ}>Valider</Button>
|
||||
<div class="invalid-feedback">
|
||||
Veuillez indiquer une équipe valide.
|
||||
</div>
|
||||
|
|
|
@ -10,7 +10,6 @@
|
|||
import DateFormat from '$lib/components/DateFormat.svelte';
|
||||
|
||||
import { my } from '$lib/stores/my.js';
|
||||
import { settings } from '$lib/stores/settings.js';
|
||||
import { themes, exercices_idx } from '$lib/stores/themes.js';
|
||||
|
||||
let req = null;
|
||||
|
@ -56,10 +55,6 @@
|
|||
{:else if row.reason == "Display choices"}
|
||||
<Badge color="secondary"><Icon name="info-square" /></Badge>
|
||||
Échange champ de texte contre liste de choix
|
||||
{:else if row.reason.startsWith("Response ")}
|
||||
{@const fields = row.reason.split(" ")}
|
||||
<Badge class="bg-success-subtle text-dark"><Icon name="clipboard2-check" /></Badge>
|
||||
Validation {fields[1]} n<sup>o</sup> {fields[3]}
|
||||
{:else}
|
||||
<Badge color="primary"><Icon name="question" /></Badge>
|
||||
{row.reason}
|
||||
|
@ -71,16 +66,14 @@
|
|||
{/if}
|
||||
</Column>
|
||||
<Column header="Détail">
|
||||
<span title="Valeur initiale (cette valeur est fixe)">{Math.trunc(10*row.points)/10}</span> × {#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} %</span> ÷ <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> − <span title="Pourcentage des points déjà obtenu au travers des réponses aux questions">{Math.trunc($settings.questionGainRatio * 1000)/10} %</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> × <span title="Coefficient multiplicateur (il varie selon les événements en cours sur la plateforme)">{row.coeff}</span>
|
||||
</Column>
|
||||
<Column header="Points">
|
||||
{Math.trunc(10*row.points * row.coeff)/10}
|
||||
</Column>
|
||||
</Table>
|
||||
{:else}
|
||||
<CardBody>
|
||||
Vous n'avez fait aucune action vous faisant gagner ou perdre des points.
|
||||
</CardBody>
|
||||
Vous n'avez fait aucune action vous faisant gagner ou perdre des points.
|
||||
{/if}
|
||||
<button class="btn btn-primary" on:click={refresh_scores}>
|
||||
<Icon name="arrow-clockwise" />
|
||||
|
|
|
@ -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();
|
|
@ -37,9 +37,6 @@ function createMyStore() {
|
|||
});
|
||||
} else if (res_my.status === 404) {
|
||||
update((m) => (null));
|
||||
if (cb) {
|
||||
cb(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -59,7 +59,6 @@ export const tags = derived([my, themesStore], ([$my, $themesStore]) => {
|
|||
for (const key in $themesStore) {
|
||||
for (const exercice of $themesStore[key].exercices) {
|
||||
exercice.tags.forEach((tag) => {
|
||||
tag = tag.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
|
||||
if (!tags[tag])
|
||||
tags[tag] = {count: 1, solved: 0};
|
||||
else
|
||||
|
|
|
@ -153,7 +153,6 @@ export const time = readable({}, function start(set) {
|
|||
_settings = settings;
|
||||
});
|
||||
|
||||
set(updateTime(_settings));
|
||||
const interval = setInterval(() => {
|
||||
set(updateTime(_settings));
|
||||
}, 1000);
|
||||
|
|
|
@ -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();
|
|
@ -3,9 +3,7 @@
|
|||
import "bootstrap-icons/font/bootstrap-icons.css";
|
||||
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import {
|
||||
Alert,
|
||||
Container,
|
||||
//Styles,
|
||||
} from '@sveltestrap/sveltestrap';
|
||||
|
@ -13,7 +11,6 @@
|
|||
import Header from '$lib/components/Header.svelte';
|
||||
|
||||
import { challengeInfo } from '$lib/stores/challengeinfo';
|
||||
import { my } from '$lib/stores/my.js';
|
||||
import { settings } from '$lib/stores/settings';
|
||||
</script>
|
||||
|
||||
|
@ -35,21 +32,6 @@
|
|||
</div>
|
||||
{/if}
|
||||
<Header />
|
||||
|
||||
{#if !$my && $page.route.id != "/register"}
|
||||
<Container class="mt-3 mb-3">
|
||||
{#if $settings.allowRegistration}
|
||||
<Alert color="warning" class="text-justify" fade={false}>
|
||||
<strong>Votre équipe n'est pas encore enregistrée.</strong> Rendez-vous sur <a href="register">cette page</a> pour procéder à votre inscription.
|
||||
</Alert>
|
||||
{:else}
|
||||
<Alert color="danger" class="text-justify" fade={false}>
|
||||
<strong>Il semblerait qu'il y ait eu un problème lors de l'attribution de votre certificat.</strong> Veuillez vous signaler auprès de notre équipe afin de corriger ce problème.
|
||||
</Alert>
|
||||
{/if}
|
||||
</Container>
|
||||
{/if}
|
||||
|
||||
<slot></slot>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
document.body.style.backgroundColor = "";
|
||||
|
||||
let items = [];
|
||||
function refresh_items() {
|
||||
$: {
|
||||
const tmpitems = [];
|
||||
for (const th of $themes) {
|
||||
if (th.id == 0) continue;
|
||||
|
@ -35,54 +35,54 @@
|
|||
}
|
||||
|
||||
if ($themesStore["0"] && !$themesStore["0"].locked && $themesStore["0"].exercices) {
|
||||
if (tmpitems.length) {
|
||||
let nb_ex_max = tmpitems.length;
|
||||
let i = 1;
|
||||
let j = 0;
|
||||
for (j = $themesStore["0"].exercices.length - 1; j >= 0 && i < tmpitems.length; j--) {
|
||||
if ($my && $my.team_id && !$my.exercices[$themesStore["0"].exercices[j].id]) {
|
||||
// Only apply after start
|
||||
if (!($time.startIn && j < nb_ex_max && j < $settings.unlockedStandaloneExercices))
|
||||
continue;
|
||||
} else if ($my && !$my.team_id && j >= nb_ex_max) {
|
||||
let nb_ex_max = tmpitems.length;
|
||||
let i = 1;
|
||||
let j = 0;
|
||||
for (j = $themesStore["0"].exercices.length - 1; j >= 0 && i < tmpitems.length; j--) {
|
||||
if ($my && $my.team_id && !$my.exercices[$themesStore["0"].exercices[j].id]) {
|
||||
// Only apply after start
|
||||
if (!($time.startIn && j < nb_ex_max && j < $settings.unlockedStandaloneExercices))
|
||||
continue;
|
||||
}
|
||||
|
||||
tmpitems.splice(i, 0, {id: tmpitems.length, theme: $themesStore["0"], exercice: $themesStore["0"].exercices[j]});
|
||||
i += 2;
|
||||
}
|
||||
|
||||
if (j >= 0 || i == 1) {
|
||||
tmpitems.push({
|
||||
id: tmpitems.length,
|
||||
theme: {
|
||||
...$themesStore["0"],
|
||||
name: "Voir les autres défis",
|
||||
headline: "Il y a " + ($themesStore["0"].exercices.length) + " défis à découvrir ! Cliquez ici pour les afficher.",
|
||||
locked: $themesStore["0"].locked || i == 1,
|
||||
},
|
||||
color: "light",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
for (const j in $themesStore["0"].exercices) {
|
||||
tmpitems.push({id: tmpitems.length, theme: $themesStore["0"], exercice: $themesStore["0"].exercices[j]});
|
||||
}
|
||||
tmpitems.splice(i, 0, {id: tmpitems.length, theme: $themesStore["0"], exercice: $themesStore["0"].exercices[j]});
|
||||
i += 2;
|
||||
}
|
||||
|
||||
if (j >= 0 || i == 1) {
|
||||
tmpitems.push({
|
||||
id: tmpitems.length,
|
||||
theme: {
|
||||
...$themesStore["0"],
|
||||
name: "Voir les autres défis",
|
||||
headline: "Il y a " + ($themesStore["0"].exercices.length) + " défis à découvrir ! Cliquez ici pour les afficher.",
|
||||
locked: $themesStore["0"].locked || i == 1,
|
||||
},
|
||||
color: "light",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
items = tmpitems;
|
||||
}
|
||||
$: refresh_items($themes);
|
||||
</script>
|
||||
|
||||
<Container class="mt-3 mb-5">
|
||||
{#if $my}
|
||||
{#if !($my.team_id)}
|
||||
{#if !$my}
|
||||
{#if $settings.allowRegistration}
|
||||
<Alert color="warning" class="text-justify" fade={false}>
|
||||
<strong>Votre équipe n'est pas encore enregistrée.</strong> Rendez-vous sur <a href="register">cette page</a> pour procéder à votre inscription.
|
||||
</Alert>
|
||||
{:else}
|
||||
<Alert color="danger" class="text-justify" fade={false}>
|
||||
<strong>Il semblerait qu'il y ait eu un problème lors de l'attribution de votre certificat.</strong> Veuillez vous signaler auprès de notre équipe afin de corriger ce problème.
|
||||
</Alert>
|
||||
{/if}
|
||||
{:else if !($my.team_id)}
|
||||
<Alert color="danger" fade={false}>
|
||||
<strong>Attention :</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 !
|
||||
</Alert>
|
||||
{:else if $teams[$my.team_id]}
|
||||
{:else if $teams[$my.team_id]}
|
||||
<Alert color="info" class="text-justify" fade={false}>
|
||||
<strong>Félicitations {#if $my.members}{#each $my.members as member, index (member.id)}{#if member.id !== $my.members[0].id}{#if member.id === $my.members[$my.members.length - 1].id} et {:else}, {/if}{/if}{member.firstname} {member.lastname}{/each} {/if}!</strong> vous êtes maintenant connecté à l'espace de votre équipe <em>{$teams[$my.team_id].name}</em>.
|
||||
{#if !$settings.denyNameChange}Vous pouvez changer ce nom dès maintenant en vous rendant sur la page de <a href="edit">votre équipe</a>.{/if}
|
||||
|
@ -93,9 +93,9 @@
|
|||
<strong>Les membres de votre équipe ne sont pas encore enregistrés.</strong> Passez voir l'équipe serveur pour corriger cela.
|
||||
</Alert>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
|
||||
<Masonry
|
||||
{items}
|
||||
let:item
|
||||
|
|
|
@ -84,27 +84,24 @@
|
|||
</Col>
|
||||
<Col lg={6} xl={5}>
|
||||
{#if $current_theme.exercices && $current_theme.exercices.length}
|
||||
<ul class="list-group h-100 d-flex flex-column">
|
||||
<ul class="list-group">
|
||||
{#each $current_theme.exercices as exercice, index}
|
||||
<li
|
||||
class="list-group-item flex-fill border-0 rounded-0"
|
||||
class="list-group-item"
|
||||
class:list-group-item-action={$my && $my.exercices[exercice.id]}
|
||||
on:click={goto(`${$current_theme.urlid}/${exercice.urlid}`)}
|
||||
on:keypress={goto(`${$current_theme.urlid}/${exercice.urlid}`)}
|
||||
>
|
||||
<div class="row h-100">
|
||||
{#if index + 1 == $current_theme.exercices.length}
|
||||
<div class="col-1"></div>
|
||||
{:else}
|
||||
<div class="col-1" style="margin-top: -0.5rem; margin-bottom: -0.5rem; text-align: right; border-right: 5px solid #{$my && $my.exercices[exercice.id] && $my.exercices[exercice.id].solved_rank ? '62c462' : 'bbb'}"></div>
|
||||
{/if}
|
||||
<div class="row">
|
||||
<div class="col-1" style="margin-top: -0.5rem; margin-bottom: -0.5rem; text-align: right; border-right: 5px solid #{$my && $my.exercices[exercice.id] && $my.exercices[exercice.id].solved_rank ? '62c462' : 'bbb'}">
|
||||
</div>
|
||||
<div class="col-10">
|
||||
<div style="position: absolute; margin-left: calc(var(--bs-gutter-x) * -.5 - 15px); margin-top: -0.5rem;">
|
||||
<svg style="height: 50px; width: 23px;">
|
||||
<rect
|
||||
style="fill:#{$my && $my.exercices[exercice.id] && (index < 1 || ($my.exercices[$current_theme.exercices[index-1].id] && $my.exercices[$current_theme.exercices[index-1].id].solved_rank)) ? '62c462' : 'bbb'}"
|
||||
width="5"
|
||||
height="27"
|
||||
height="30"
|
||||
x="10"
|
||||
y="0" />
|
||||
<path
|
||||
|
@ -114,17 +111,24 @@
|
|||
</div>
|
||||
<div class="d-flex justify-content-between flex-wrap">
|
||||
<h5 class="fw-bold text-truncate">
|
||||
<span style="white-space: nowrap">
|
||||
{#if $my && $my.exercices[exercice.id] && $my.exercices[exercice.id].wip}
|
||||
<Icon name="cone-striped" aria-hidden="true" title="Cette étape est encore en construction." />
|
||||
{/if}
|
||||
{exercice.title}
|
||||
</span>
|
||||
{#if $my && $my.exercices[exercice.id]}
|
||||
<span style="white-space: nowrap">
|
||||
{#if $my.exercices[exercice.id].wip}
|
||||
<Icon name="cone-striped" aria-hidden="true" title="Cette étape est encore en construction." />
|
||||
{/if}
|
||||
{exercice.title}
|
||||
</span>
|
||||
{:else}
|
||||
<span style="white-space: nowrap">
|
||||
<Icon name="lock-fill" aria-hidden="true" title="Vous n'avez pas encore accès à ce défi" />
|
||||
{exercice.title}
|
||||
</span>
|
||||
{/if}
|
||||
{#if exercice.curcoeff > 1.0}
|
||||
<Icon name="gift" aria-hidden="true" title="Un bonus est actuellement appliqué lors de la résolution de ce défi" />
|
||||
{/if}
|
||||
</h5>
|
||||
<div class="mb-2">
|
||||
<div>
|
||||
{#each exercice.tags as tag, idx}
|
||||
<Badge href="tags/{tag}" pill color="secondary" class="mx-1 float-end">#{tag}</Badge>
|
||||
{/each}
|
||||
|
@ -132,14 +136,14 @@
|
|||
</div>
|
||||
<p>{@html exercice.headline}</p>
|
||||
</div>
|
||||
<div class="d-none d-md-flex flex-column h-100 justify-content-center align-items-end col-1 pe-0">
|
||||
<div class="d-none d-md-block col-1 pe-0">
|
||||
{#if $my && $my.exercices[exercice.id]}
|
||||
<a href="{$current_theme.urlid}/{exercice.urlid}" style="font-size: 3rem">
|
||||
<a class="float-end" href="{$current_theme.urlid}/{exercice.urlid}" style="font-size: 3rem">
|
||||
<Icon name="chevron-right" aria-hidden="true" />
|
||||
</a>
|
||||
{:else}
|
||||
<span style="font-size: 3rem">
|
||||
<Icon name="lock-fill" aria-hidden="true" title="Vous n'avez pas encore accès à ce défi" />
|
||||
<span class="float-end" style="font-size: 3rem">
|
||||
<Icon name="chevron-right" aria-hidden="true" />
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
Container,
|
||||
Icon,
|
||||
Row,
|
||||
Spinner,
|
||||
} from '@sveltestrap/sveltestrap';
|
||||
|
||||
import { goto } from '$app/navigation';
|
||||
|
@ -24,10 +23,8 @@
|
|||
let partJ = false;
|
||||
let messageClass;
|
||||
let message;
|
||||
let registrationInProgress = false;
|
||||
|
||||
function gotoHomeOnDiff(i) {
|
||||
registrationInProgress = true;
|
||||
my.refresh((my) => {
|
||||
if (my && my.team_id) {
|
||||
themesStore.refresh(() => {
|
||||
|
@ -35,10 +32,6 @@
|
|||
});
|
||||
} else if (i > 0) {
|
||||
setTimeout(gotoHomeOnDiff, 650, i-1);
|
||||
} else {
|
||||
registrationInProgress = false;
|
||||
messageClass = 'danger';
|
||||
message = "Temps d'attente dépassé.";
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -113,13 +106,9 @@
|
|||
<strong>Oups, il semblerait qu'il y ait eu un problème lors de l'attribution de votre certificat.</strong>
|
||||
Veuillez vous signaler auprès de notre équipe afin de corriger ce problème.
|
||||
</Alert>
|
||||
{:else if registrationInProgress}
|
||||
<div class="d-flex justify-content-center align-items-center gap-4 mt-5 fw-bold text-primary">
|
||||
<Spinner size="lg" color="primary" /> Inscription en cours…
|
||||
</div>
|
||||
{:else}
|
||||
{#if !$settings.denyTeamCreation && !partJ}
|
||||
<Card body class="niceborder my-3 text-white">
|
||||
<Card body class="niceborder my-3">
|
||||
<p>
|
||||
Votre équipe n'est pas encore enregistrée sur notre serveur. Afin de
|
||||
pouvoir participer au challenge, nous vous remercions de bien vouloir
|
||||
|
@ -129,7 +118,7 @@
|
|||
</Card>
|
||||
{/if}
|
||||
{#if $settings.canJoinTeam && !partR}
|
||||
<Card body class="niceborder my-3 text-white">
|
||||
<Card body class="niceborder my-3">
|
||||
<p>
|
||||
{#if !$settings.denyTeamCreation}
|
||||
Si votre équipe est déjà créée, rejoignez-là !
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
|
||||
<div class="card-group text-justify mb-5">
|
||||
<div class="card niceborder">
|
||||
<div class="card-body text-indent text-white">
|
||||
<div class="card-body text-indent">
|
||||
<h2>Débloquage des challenges</h2>
|
||||
<p>
|
||||
Au début, seul le premier défi de chaque scénario est
|
||||
|
@ -31,7 +31,7 @@
|
|||
{#if $settings.unlockedStandaloneExercicesByThemeStepValidation > 0 || $settings.unlockedStandaloneExercicesByStandaloneExerciceValidation > 0}
|
||||
<p>
|
||||
Vous avez également accès à {$settings.unlockedStandaloneExercices} défis indépendants.
|
||||
D'autres défis sont débloqués
|
||||
Ces défis sont débloqués
|
||||
{#if $settings.unlockedStandaloneExercicesByThemeStepValidation > 0}{#if $settings.unlockedStandaloneExercicesByThemeStepValidation < 1} toutes les {1/$settings.unlockedStandaloneExercicesByThemeStepValidation} étape{#if 1/$settings.unlockedStandaloneExercicesByThemeStepValidation > 1}s{/if} de scénario que vous validez{:else}par {$settings.unlockedStandaloneExercicesByThemeStepValidation} défis pour chaque étape de scénario validée{/if}{/if}
|
||||
{#if $settings.unlockedStandaloneExercicesByStandaloneExerciceValidation > 0}{#if $settings.unlockedStandaloneExercicesByStandaloneExerciceValidation < 1} tous les {1/$settings.unlockedStandaloneExercicesByStandaloneExerciceValidation} défi{#if 1/$settings.unlockedStandaloneExercicesByStandaloneExerciceValidation > 1}s{/if} indépendant que vous validez{:else}par {$settings.unlockedStandaloneExercicesByStandaloneExerciceValidation} exercice indépendant validé{/if}{/if}
|
||||
</p>
|
||||
|
@ -55,21 +55,6 @@
|
|||
proposés. Plus le challenge est compliqué, plus il rapporte de points.
|
||||
</p>
|
||||
|
||||
{#if $settings.questionGainRatio != 0}
|
||||
<p>
|
||||
Même si vous n'arrivez pas à valider un défi, toutes les questions validées augmentent votre score.
|
||||
{Math.trunc($settings.questionGainRatio * 1000)/10} % 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 :<br>
|
||||
20 × {Math.trunc($settings.questionGainRatio * 1000)/10} % ÷ 5 × 3 = {Math.trunc(20 * $settings.questionGainRatio / 5 * 3 * 100)/100} points.
|
||||
</p>
|
||||
<p>
|
||||
Les {Math.trunc(1000 - $settings.questionGainRatio * 1000)/10} % restants sont obtenus à la validation complète du défi.
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
{#if $settings.submissionCostBase != 0}
|
||||
<h3>Coût des tentatives</h3>
|
||||
<p>
|
||||
Vous disposez de 10 tentatives pour trouver la/les solutions d'un
|
||||
|
@ -129,21 +114,20 @@
|
|||
parmi ce nombre de tentatives.
|
||||
</p>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card niceborder">
|
||||
<div class="card-body text-indent text-white">
|
||||
<div class="card-body text-indent">
|
||||
{#if $settings.discountedFactor > 0}
|
||||
<h3>Décote des gains</h3>
|
||||
<p>
|
||||
Une validation d'étape ne vous garantit pas un solde de points fixe.
|
||||
Une validation d'étape ne vous garanti pas un solde de points fixe.
|
||||
</p>
|
||||
<p>
|
||||
Selon le nombre d'équipes qui valident un challenge donné, sa cote diminue et vous rapporte alors moins de points. Le gain final est donc indépendant du fait que vous ayez validé l'étape avant une autre équipe : le gain affiché est un gain maximum que vous obtiendriez si aucune autre équipe ne valide cette étape.
|
||||
Selon le nombre d'équipe qui valident un challenge donné, sa cote diminue et vous rapporte alors moins de points. Le gain est donc indépendemment du fait que vous ayez validé l'étape avant une autre équipe : le gain affiché est un gain maximum, entendu si aucune autre équipe ne le valide.
|
||||
</p>
|
||||
<p>
|
||||
Chaque validation réduit de {$settings.discountedFactor*100} % la cote de l'exercice.
|
||||
Chaque validation réduit de {$settings.discountedFactor*100} % la cote de l'exercice.
|
||||
</p>
|
||||
<p>
|
||||
Ainsi, pour un exercice d'une valeur initiale de {10*$settings.globalScoreCoefficient} points :
|
||||
|
@ -162,19 +146,19 @@
|
|||
</tr>
|
||||
<tr>
|
||||
<td>2</td>
|
||||
<td>{Math.round(100*$settings.globalScoreCoefficient*(1-$settings.discountedFactor))/10} points</td>
|
||||
<td>{10*$settings.globalScoreCoefficient*(1-$settings.discountedFactor)} points</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>5</td>
|
||||
<td>{Math.round(100*$settings.globalScoreCoefficient*(1-$settings.discountedFactor*5))/10} points</td>
|
||||
<td>{10*$settings.globalScoreCoefficient*(1-$settings.discountedFactor*5)} points</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>10</td>
|
||||
<td>{Math.round(100*$settings.globalScoreCoefficient*(1-$settings.discountedFactor*10))/10} points</td>
|
||||
<td>{10*$settings.globalScoreCoefficient*(1-$settings.discountedFactor*10)} points</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>20</td>
|
||||
<td>{Math.round(100*$settings.globalScoreCoefficient*(1-$settings.discountedFactor*20))/10} points</td>
|
||||
<td>{10*$settings.globalScoreCoefficient*(1-$settings.discountedFactor*20)} points</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>...</td>
|
||||
|
@ -208,12 +192,10 @@
|
|||
défi.
|
||||
</p>
|
||||
|
||||
{#if $settings.firstBlood}
|
||||
<h4>Prem's</h4>
|
||||
<p>
|
||||
Un bonus de +{$settings.firstBlood * 100} % est attribué à la première équipe qui résout un défi.
|
||||
</p>
|
||||
{/if}
|
||||
<h4>Prem's</h4>
|
||||
<p>
|
||||
Un bonus de +{$settings.firstBlood * 100} % est attribué à la première équipe qui résout un défi.
|
||||
</p>
|
||||
|
||||
<h4>Bonus temporaires <small><Icon name="gift" aria-hidden="true" title="Des
|
||||
bonus existent pour au moins un challenge de ce thème" /></small></h4>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Reference in a new issue