├── .dockerignore
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── cookies
├── Dockerfile
├── README.md
├── api
│ ├── .gitignore
│ └── Java_api.java
├── cookies.sh
└── hamcrest-all-1.3.jar
├── docker-compose.yml
├── fastapi
├── .gitignore
├── Dockerfile
├── __init__.py
├── alembic
│ ├── README
│ ├── env.py
│ ├── script.py.mako
│ └── versions
│ │ ├── 075dad69ab65_added_must_change_password_to_user.py
│ │ ├── 48e0ab5941fc_init.py
│ │ ├── f031d57aa4e6_added_main_class_to_project.py
│ │ └── f42eca714ce0_added_enable_tests_to_homework.py
├── alembic_dev.ini
├── alembic_prod.ini
├── api.py
├── auth.py
├── code.py
├── config.py
├── cookies_api.py
├── crud.py
├── database_config.py
├── git.py
├── gunicorn_conf.py
├── java_helloWorld
│ ├── de
│ │ ├── Schoco.java
│ │ └── Tests.java
│ └── en
│ │ ├── Schoco.java
│ │ └── Tests.java
├── main.py
├── models_and_schemas.py
├── prestart.sh
├── requirements.txt
└── users.py
├── frontend
├── .env.development
├── .env.production
├── .gitignore
├── README.md
├── index.html
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ ├── schoco-icon.svg
│ ├── schoco-text-1.svg
│ ├── schoco-text-2.svg
│ └── schoco-text-3.svg
├── src
│ ├── App.vue
│ ├── components
│ │ ├── ColorModeSwitch.vue
│ │ ├── CourseBadge.vue
│ │ ├── IDEFileTree.vue
│ │ ├── NavBar.vue
│ │ ├── PasswordInfo.vue
│ │ ├── PasswordInput.vue
│ │ ├── ProjectCard.vue
│ │ ├── Submission.vue
│ │ └── TreeNode.vue
│ ├── fonts
│ │ └── NotoSans-Regular.ttf
│ ├── locales
│ │ ├── de
│ │ │ ├── color_mode_switch.js
│ │ │ ├── general.js
│ │ │ ├── home.js
│ │ │ ├── homework.js
│ │ │ ├── ide.js
│ │ │ ├── login.js
│ │ │ ├── navbar.js
│ │ │ ├── new_project.js
│ │ │ ├── password_info.js
│ │ │ ├── project_card.js
│ │ │ └── users.js
│ │ ├── en
│ │ │ ├── color_mode_switch.js
│ │ │ ├── general.js
│ │ │ ├── home.js
│ │ │ ├── homework.js
│ │ │ ├── ide.js
│ │ │ ├── login.js
│ │ │ ├── navbar.js
│ │ │ ├── new_project.js
│ │ │ ├── password_info.js
│ │ │ ├── project_card.js
│ │ │ └── users.js
│ │ └── index.js
│ ├── main.js
│ ├── router
│ │ └── index.js
│ ├── sass
│ │ └── main.scss
│ ├── services
│ │ ├── auth-headers.js
│ │ ├── code.service.js
│ │ ├── user.service.js
│ │ └── websocket-worker.js
│ ├── stores
│ │ └── auth.store.js
│ └── views
│ │ ├── ChangePassword.vue
│ │ ├── Home.vue
│ │ ├── Homework.vue
│ │ ├── IDE.vue
│ │ ├── Login.vue
│ │ ├── NewProject.vue
│ │ └── Users.vue
└── vite.config.js
├── gitea
├── .gitignore
└── docker-compose.yml
├── logos
├── favicon.ico
├── favicon.png
├── favicon.svg
├── schoco-full.svg
├── schoco-logo.svg
├── schoco-text-1.svg
├── schoco-text-2.svg
├── schoco-text-3.svg
└── schoco-text.svg
├── nginx
├── nginx.conf
└── nginx.dev.conf
└── readme
├── IDE.png
├── assignment_teacher.png
├── home_assignment_teacher.png
├── schoco-full.png
├── schoco-logo.svg
├── schoco-promo.jpg
├── schoco_architecture.svg
├── usermanagement_course.jpg
├── usermanagement_end.jpg
└── usermanagement_students.jpg
/.dockerignore:
--------------------------------------------------------------------------------
1 | gitea
2 | cookies
3 | fastapi
4 | data
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | data
2 | *.envrc
3 | .DS_Store
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # Dockerfile for the FRONTEND
2 |
3 | # build vue
4 | FROM node:18.20.3-alpine3.20 as build-vue
5 | WORKDIR /app
6 | COPY ./frontend/package*.json .
7 | RUN npm install
8 | COPY ./frontend .
9 | RUN npm run build
10 |
11 |
12 | FROM nginxinc/nginx-unprivileged:1.25.5-alpine3.19
13 | COPY nginx/nginx.conf /etc/nginx/conf.d/default.conf
14 | COPY --from=build-vue /app/dist /usr/share/nginx/html
15 |
16 | # Only contains nginx and the frontend. Is the only entrypoint to the schoco-network!
17 | # Used to filter incoming traffic to ONLY (!!!) allow websocket-container-attachment to /var/run/docker.sock.
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Marco Kümmel
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/cookies/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM alpine:3.17
2 |
3 | COPY cookies.sh /app/cookies.sh
4 | COPY hamcrest-all-1.3.jar /app/hamcrest.jar
5 | COPY api/*.class /
6 |
7 | RUN apk add --no-cache openjdk8 junit libc6-compat bash \
8 | && chmod +x /app/cookies.sh
9 |
10 | # The Entrypoint starts the minimum-API written in Java.
11 | # The container just waits for commands via the API.
12 | # When a /compile /run /test command is coming in,
13 | # then the cookies.sh-Script gets executed
14 | ENTRYPOINT ["java", "Java_api"]
15 |
--------------------------------------------------------------------------------
/cookies/README.md:
--------------------------------------------------------------------------------
1 | COOKIES stands for **Co**mpile **o**nline, **k**eep **i**ts **e**xecution **s**upervised
2 |
3 | Cookies is the docker-image, which will run in multiple instances and do the actual Java compilation and execution work of schoco. For each compilation and execution there's a new container-instance used (TODO: perhaps we can keep the same containers running at least for the compilation!!). Cookies itself is hardly useful and is tighlty coupled to schoco.
4 |
5 | It is based on [codeboard-mantra](https://github.com/codeboardio/mantra), but works slightly different concerning the creation, starting and stopping of the containers:
6 |
7 | - Create and start at least one container to keep it fully ready to start working. Creating and starting takes roughly around 0.3 seconds. We can save this time for each single compilation/execution when we do not wait with starting a container until a new command is coming in. Instead the container is already running, gets prepared for the specific command, and then the command gets executed (using the API inside the container - written in Java, see /api).
--------------------------------------------------------------------------------
/cookies/api/.gitignore:
--------------------------------------------------------------------------------
1 | *.class
--------------------------------------------------------------------------------
/cookies/api/Java_api.java:
--------------------------------------------------------------------------------
1 | import com.sun.net.httpserver.HttpServer;
2 | import com.sun.net.httpserver.HttpExchange;
3 | import com.sun.net.httpserver.HttpHandler;
4 | import java.io.IOException;
5 | import java.io.InputStream;
6 | import java.io.OutputStream;
7 | import java.io.BufferedReader;
8 | import java.io.InputStreamReader;
9 | import java.net.InetSocketAddress;
10 | import java.util.HashMap;
11 | import java.util.stream.Collectors;
12 |
13 | public class Java_api {
14 | public static void main(String[] args) throws IOException, InterruptedException {
15 | Java_api server = new Java_api();
16 | }
17 |
18 | public Java_api() throws IOException {
19 | HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
20 | server.createContext("/compile", new HttpHandler() {
21 |
22 | @Override
23 | public void handle(HttpExchange exchange) throws IOException {
24 | if ("POST".equals(exchange.getRequestMethod())) {
25 |
26 | HashMap postData = getRequestData(exchange.getRequestBody());
27 |
28 | int exitCode = 0;
29 | String stdout = "";
30 |
31 | try {
32 | String[] command = { "sh", "-c", "bash /app/cookies.sh 'javac -cp /app/tmp/:/usr/share/java/junit.jar /app/tmp/*.java' "
33 | + postData.get("timeout_cpu")
34 | + " "
35 | + postData.get("timeout_session")
36 | + (postData.get("save_output").trim().equals("true") ? " ; echo '\nschoco compilation finished SBsodjpFo43E5Y7d'" : "")
37 | + "; exit" }; // random string to indicate end of execution
38 |
39 | Process p;
40 |
41 | if (postData.get("save_output").trim().equals("true")) {
42 | p = new ProcessBuilder().redirectErrorStream(true).command(command).start();
43 |
44 | BufferedReader reader =
45 | new BufferedReader(new InputStreamReader(p.getInputStream()));
46 | StringBuilder builder = new StringBuilder();
47 | String line = null;
48 |
49 | while ((line = reader.readLine()) != null) {
50 | if (line.equals("schoco compilation finished SBsodjpFo43E5Y7d")) break;
51 | builder.append(line);
52 | builder.append(System.getProperty("line.separator"));
53 | }
54 |
55 | stdout = builder.toString();
56 | } else {
57 | p = new ProcessBuilder().inheritIO().command(command).start();
58 | }
59 |
60 | exitCode = p.waitFor();
61 | System.out.flush();
62 |
63 | } catch (InterruptedException e) {
64 | e.printStackTrace();
65 | exchange.sendResponseHeaders(500, -1);// Internal Server Error
66 | OutputStream os = exchange.getResponseBody();
67 | os.write("InterruptedException occured".getBytes());
68 | os.close();
69 | return;
70 | } catch (Exception e) {
71 | e.printStackTrace();
72 | exchange.sendResponseHeaders(501, -1);
73 | OutputStream os = exchange.getResponseBody();
74 | os.write("Exception occured".getBytes());
75 | os.close();
76 | return;
77 | }
78 |
79 | String responseText = "{\"exitCode\":\"" + exitCode + (postData.get("save_output").trim().equals("true") ? ("\", \"stdout\":\"" + stdout.replaceAll("\"","\\\\\"")) : "") + "\"}";
80 | exchange.sendResponseHeaders(200, responseText.getBytes().length);
81 | OutputStream os = exchange.getResponseBody();
82 | os.write(responseText.getBytes());
83 | os.close();
84 | } else {
85 | exchange.sendResponseHeaders(405, -1);// 405 Method Not Allowed
86 | }
87 | exchange.close();
88 | }
89 | });
90 |
91 | server.createContext("/execute", new HttpHandler() {
92 |
93 | @Override
94 | public void handle(HttpExchange exchange) throws IOException {
95 | if ("POST".equals(exchange.getRequestMethod())) {
96 |
97 | HashMap postData = getRequestData(exchange.getRequestBody());
98 |
99 | int exitCode = 0;
100 | String stdout = "";
101 |
102 | try {
103 | String[] command = { "sh", "-c", "bash /app/cookies.sh 'java -Djava.security.manager=default -cp /app/tmp "
104 | + postData.get("entry_point")
105 | + "' "
106 | + postData.get("timeout_cpu")
107 | + " "
108 | + postData.get("timeout_session")
109 | + (postData.get("save_output").trim().equals("true") ? " ; echo '\nschoco execution finished JVXjUq5wpdxDTki5'" : "")
110 | + "; exit" };
111 |
112 | Process p;
113 |
114 | if (postData.get("save_output").trim().equals("true")) {
115 | p = new ProcessBuilder().redirectErrorStream(true).command(command).start();
116 |
117 | BufferedReader reader =
118 | new BufferedReader(new InputStreamReader(p.getInputStream()));
119 | StringBuilder builder = new StringBuilder();
120 | String line = null;
121 |
122 | while ((line = reader.readLine()) != null) {
123 | if (line.equals("schoco execution finished JVXjUq5wpdxDTki5")) break;
124 | builder.append(line);
125 | builder.append(System.getProperty("line.separator"));
126 | }
127 |
128 | stdout = builder.toString();
129 | } else {
130 | p = new ProcessBuilder().inheritIO().command(command).start();
131 | }
132 |
133 | exitCode = p.waitFor();
134 | System.out.flush();
135 |
136 | } catch (InterruptedException e) {
137 | e.printStackTrace();
138 | exchange.sendResponseHeaders(500, -1);// Internal Server Error
139 | OutputStream os = exchange.getResponseBody();
140 | os.write("InterruptedException occured".getBytes());
141 | os.close();
142 | return;
143 | } catch (Exception e) {
144 | e.printStackTrace();
145 | exchange.sendResponseHeaders(501, -1);
146 | OutputStream os = exchange.getResponseBody();
147 | os.write("Exception occured".getBytes());
148 | os.close();
149 | return;
150 | }
151 |
152 | String responseText = "{\"exitCode\":\"" + exitCode + (postData.get("save_output").trim().equals("true") ? ("\", \"stdout\":\"" + stdout.replaceAll("\"","\\\\\"")) : "") + "\"}";
153 | exchange.sendResponseHeaders(200, responseText.getBytes().length);
154 | OutputStream os = exchange.getResponseBody();
155 | os.write(responseText.getBytes());
156 | os.close();
157 | } else {
158 | exchange.sendResponseHeaders(405, -1);// 405 Method Not Allowed
159 | }
160 | exchange.close();
161 | }
162 | });
163 |
164 | server.createContext("/test", new HttpHandler() {
165 |
166 | @Override
167 | public void handle(HttpExchange exchange) throws IOException {
168 | if ("POST".equals(exchange.getRequestMethod())) {
169 |
170 | HashMap postData = getRequestData(exchange.getRequestBody());
171 |
172 | int exitCode = 0;
173 | String stdout;
174 |
175 | try {
176 | String[] command = { "sh", "-c", "bash /app/cookies.sh 'java -cp /app/tmp:/usr/share/java/junit.jar:/app/hamcrest.jar org.junit.runner.JUnitCore Tests' " + postData.get("timeout_cpu") + " " + postData.get("timeout_session") + " ; echo '\nschoco JUnit finished'; exit" };
177 | Process p = new ProcessBuilder().redirectErrorStream(true).command(command).start();
178 |
179 | BufferedReader reader =
180 | new BufferedReader(new InputStreamReader(p.getInputStream()));
181 | StringBuilder builder = new StringBuilder();
182 | String line = null;
183 |
184 | while ((line = reader.readLine()) != null) {
185 | if (line.equals("schoco JUnit finished")) break;
186 | //System.out.println(line);
187 | //System.out.flush();
188 | builder.append(line);
189 | builder.append(System.getProperty("line.separator"));
190 | }
191 |
192 | stdout = builder.toString();
193 |
194 | exitCode = p.waitFor();
195 | System.out.flush();
196 |
197 | } catch (InterruptedException e) {
198 | e.printStackTrace();
199 | exchange.sendResponseHeaders(500, -1);// Internal Server Error
200 | OutputStream os = exchange.getResponseBody();
201 | os.write("InterruptedException occured".getBytes());
202 | os.close();
203 | return;
204 | } catch (Exception e) {
205 | e.printStackTrace();
206 | exchange.sendResponseHeaders(501, -1);
207 | OutputStream os = exchange.getResponseBody();
208 | os.write("Exception occured".getBytes());
209 | os.close();
210 | return;
211 | }
212 |
213 | String responseText = "{\"exitCode\":\"" + exitCode + "\", \"stdout\":\"" + stdout + "\"}";
214 | exchange.sendResponseHeaders(200, responseText.getBytes().length);
215 | OutputStream os = exchange.getResponseBody();
216 | os.write(responseText.getBytes());
217 | os.close();
218 | } else {
219 | exchange.sendResponseHeaders(405, -1);// 405 Method Not Allowed
220 | }
221 | exchange.close();
222 | }
223 | });
224 |
225 | server.setExecutor(null); // creates a default executor
226 | server.start();
227 | }
228 |
229 |
230 |
231 | private HashMap getRequestData(InputStream is) throws IOException {
232 | HashMap request = new HashMap<>();
233 |
234 | StringBuilder sb = new StringBuilder();
235 | int i;
236 | while ((i = is.read()) != -1) {
237 | sb.append((char) i);
238 | }
239 | String rs = sb.toString();
240 | if (rs.startsWith("'{") || rs.startsWith("\"{"))
241 | rs = rs.substring(2, rs.length()-2);
242 | else if (rs.startsWith("{"))
243 | rs = rs.substring(1, rs.length()-1);
244 | for (String kv : rs.split(",")) {
245 | kv = kv.trim();
246 | String key = kv.split(":")[0];
247 | if (key.startsWith("\"") || key.startsWith("\'"))
248 | key = key.substring(1, key.length()-1);
249 | String value = kv.split(":")[1];
250 | if (value.startsWith("\"") || value.startsWith("\'"))
251 | value = value.substring(1, value.length()-1);
252 | request.put(key, value);
253 | }
254 | return request;
255 | }
256 | }
--------------------------------------------------------------------------------
/cookies/cookies.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | ###
4 | # Schoco-cookies script
5 | # (based on Codeboard-Mantra, see below)
6 | #-----------------------------
7 | #
8 | # Codeboard - Mantra script
9 | #
10 | # Executes "compile" or "run" actions
11 | # Times out when either a CPU usage limit is reached
12 | # or the process (session) has been alive for a certain time
13 | #
14 | # IMPORTANT: this script assumes that you have 'bc' installed (apt-get install bc)
15 | # Note: we run this in a bash because bash has the "time" command
16 | #
17 | # Copyright: H.-Christian Estler
18 | #
19 | ###
20 |
21 | CMD_ARG=$1
22 | TIMEOUT_CPU=$2
23 | TIMEOUT_SESSION=$3
24 |
25 | ( sleep $TIMEOUT_SESSION && echo -e "\n\nSchoco terminated your program because it exceeded the session time limit.\n" && kill $$ ) &
26 |
27 | forkedPID=$!
28 | disown
29 |
30 | ulimit -t $TIMEOUT_CPU
31 | exec 3>&1 4>&2;
32 | CPU_TIME_STRING=$(TIMEFORMAT="%U;%S"; { time sh -c "$CMD_ARG" 1>&3 2>&4; } 2>&1);
33 | CPU_TIME_USER=$(echo $CPU_TIME_STRING | cut -d ';' -f 1)
34 | CPU_TIME_SYSTEM=$(echo $CPU_TIME_STRING | cut -d ';' -f 2)
35 | exec 3>&- 4>&-;
36 |
37 | if [ $(echo "$CPU_TIME_USER + $CPU_TIME_SYSTEM + 0.1 >= $TIMEOUT_CPU" | bc) -eq 1 ]; then
38 | echo -e "\n\nSchoco terminated your program because it exceeded the CPU usage limit.\n"
39 | fi;
40 |
41 | kill $forkedPID
42 |
--------------------------------------------------------------------------------
/cookies/hamcrest-all-1.3.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PhiTux/schoco/c302238b5c42b8125eff5d3bfd2d4a2485f14f69/cookies/hamcrest-all-1.3.jar
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | networks:
2 | schoco:
3 | name: schoco
4 |
5 | services:
6 | schoco-backend:
7 | image: phitux/schoco-backend: # use the newest tag, see https://hub.docker.com/r/phitux/schoco-backend/tags
8 | container_name: schoco-backend
9 | restart: always
10 | user: "1000:1000" # find out the user-id (uid) and group-id (gid) of the new user schoco by running 'id schoco' in your bash
11 | group_add:
12 | # find your docker group ID. Either run in your bash: export DOCKER_GROUP_ID=$(getent group docker | cut -d: -f3)
13 | # and import it as variable, or just run the command from within the brackets and replace ${DOCKER_GROUP_ID} with the output.
14 | # Important: the group ID must be used as String (in quotes)!
15 | - ${DOCKER_GROUP_ID}
16 | environment:
17 | - FULL_DATA_PATH=/path/to/my/data
18 | # same as (left part of) first volume - but here as FULL PATH!!!
19 |
20 | - MAX_CONTAINERS=4
21 | # sets the amount of java-workers (you want to set this higher!) I recommend as rule of thumb 1.5x the amout of cores of your CPU
22 |
23 | - SECRET_KEY=secret
24 | # used for session token
25 |
26 | - TEACHER_KEY=teacherkey
27 | # this is the 'password' that is used to create new teacher-accounts. It must only be known to the teachers.
28 |
29 | - GITEA_USERNAME=schoco
30 | # this is the username of the gitea-user (see last image in this yaml-file)
31 |
32 | - GITEA_PASSWORD=schoco1234
33 | # and that is the password of the gitea-user.
34 | # Actually both username and password can stay like this, if you use the gitea-image from this yaml-file and if gitea is not made public (default)!
35 |
36 | - GITEA_HOST=http://schoco-gitea:3000
37 | # stays like this, if you use the gitea-image from this yaml-file and if gitea is not made public (default)!
38 | # change it to your domain, if you use a public gitea-instance
39 | networks:
40 | - schoco
41 | volumes:
42 | - ./data:/app/data
43 | - /var/run/docker.sock:/var/run/docker.sock
44 |
45 | schoco-frontend:
46 | image: phitux/schoco-frontend: # always use the same tag as schoco-backend (see https://hub.docker.com/r/phitux/schoco-frontend/tags)
47 | container_name: schoco-frontend
48 | restart: always
49 | group_add:
50 | - ${DOCKER_GROUP_ID} # see above
51 | networks:
52 | - schoco
53 | volumes:
54 | - /var/run/docker.sock:/var/run/docker.sock
55 | ports:
56 | - "80:8080" # adapt the left host-port to your needs
57 |
58 | schoco-gitea:
59 | image: gitea/gitea:1.17.3 # you could probably use a newer version, but API-changes might break something...
60 | container_name: schoco-gitea
61 | restart: always
62 | environment:
63 | - USER_UID=1000
64 | - USER_GID=1000
65 | - GITEA__security__INSTALL_LOCK=true
66 | networks:
67 | - schoco
68 | volumes:
69 | - ./gitea-data:/data
70 | - /etc/timezone:/etc/timezone:ro
71 | - /etc/localtime:/etc/localtime:ro
72 |
--------------------------------------------------------------------------------
/fastapi/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__/
2 | sql_app.db
3 | data/*
4 | *.class
5 | .envrc*
6 | ..envrc.un~
7 | .venv
8 | venv
9 |
--------------------------------------------------------------------------------
/fastapi/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM tiangolo/uvicorn-gunicorn-fastapi:python3.10-slim
2 | COPY ./requirements.txt /app/requirements.txt
3 | RUN apt-get update && \
4 | apt-get install -y libcurl4-openssl-dev libssl-dev gcc && \
5 | pip install --no-cache-dir --upgrade -r /app/requirements.txt && \
6 | apt-get purge -y gcc && \
7 | apt-get autoremove -y && \
8 | rm -rf /var/lib/apt/lists/*
9 | COPY . /app
10 |
--------------------------------------------------------------------------------
/fastapi/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PhiTux/schoco/c302238b5c42b8125eff5d3bfd2d4a2485f14f69/fastapi/__init__.py
--------------------------------------------------------------------------------
/fastapi/alembic/README:
--------------------------------------------------------------------------------
1 | Generic single-database configuration.
--------------------------------------------------------------------------------
/fastapi/alembic/env.py:
--------------------------------------------------------------------------------
1 | from logging.config import fileConfig
2 |
3 | from sqlalchemy import engine_from_config
4 | from sqlalchemy import pool
5 | from sqlmodel import SQLModel
6 |
7 | # this line has to stay, otherwise the models won't be found (although the import seems not to be used!)
8 | from models_and_schemas import UserCourseLink, User, Course, Project, Homework, EditingHomework
9 |
10 | from alembic import context
11 |
12 | # this is the Alembic Config object, which provides
13 | # access to the values within the .ini file in use.
14 | config = context.config
15 |
16 | # Interpret the config file for Python logging.
17 | # This line sets up loggers basically.
18 | if config.config_file_name is not None:
19 | fileConfig(config.config_file_name)
20 |
21 | # add your model's MetaData object here
22 | # for 'autogenerate' support
23 | # from myapp import mymodel
24 | # target_metadata = mymodel.Base.metadata
25 | target_metadata = SQLModel.metadata
26 |
27 | # other values from the config, defined by the needs of env.py,
28 | # can be acquired:
29 | # my_important_option = config.get_main_option("my_important_option")
30 | # ... etc.
31 |
32 |
33 | def run_migrations_offline() -> None:
34 | """Run migrations in 'offline' mode.
35 |
36 | This configures the context with just a URL
37 | and not an Engine, though an Engine is acceptable
38 | here as well. By skipping the Engine creation
39 | we don't even need a DBAPI to be available.
40 |
41 | Calls to context.execute() here emit the given string to the
42 | script output.
43 |
44 | """
45 | url = config.get_main_option("sqlalchemy.url")
46 | context.configure(
47 | url=url,
48 | target_metadata=target_metadata,
49 | literal_binds=True,
50 | dialect_opts={"paramstyle": "named"},
51 | )
52 |
53 | with context.begin_transaction():
54 | context.run_migrations()
55 |
56 |
57 | def run_migrations_online() -> None:
58 | """Run migrations in 'online' mode.
59 |
60 | In this scenario we need to create an Engine
61 | and associate a connection with the context.
62 |
63 | """
64 | connectable = engine_from_config(
65 | config.get_section(config.config_ini_section, {}),
66 | prefix="sqlalchemy.",
67 | poolclass=pool.NullPool,
68 | )
69 |
70 | with connectable.connect() as connection:
71 | context.configure(
72 | connection=connection, target_metadata=target_metadata
73 | )
74 |
75 | with context.begin_transaction():
76 | context.run_migrations()
77 |
78 |
79 | if context.is_offline_mode():
80 | run_migrations_offline()
81 | else:
82 | run_migrations_online()
83 |
--------------------------------------------------------------------------------
/fastapi/alembic/script.py.mako:
--------------------------------------------------------------------------------
1 | """${message}
2 |
3 | Revision ID: ${up_revision}
4 | Revises: ${down_revision | comma,n}
5 | Create Date: ${create_date}
6 |
7 | """
8 | from typing import Sequence, Union
9 | import sqlmodel
10 |
11 | from alembic import op
12 | import sqlalchemy as sa
13 | ${imports if imports else ""}
14 |
15 | # revision identifiers, used by Alembic.
16 | revision: str = ${repr(up_revision)}
17 | down_revision: Union[str, None] = ${repr(down_revision)}
18 | branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
19 | depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
20 |
21 |
22 | def upgrade() -> None:
23 | ${upgrades if upgrades else "pass"}
24 |
25 |
26 | def downgrade() -> None:
27 | ${downgrades if downgrades else "pass"}
28 |
--------------------------------------------------------------------------------
/fastapi/alembic/versions/075dad69ab65_added_must_change_password_to_user.py:
--------------------------------------------------------------------------------
1 | """added must_change_password to user
2 |
3 | Revision ID: 075dad69ab65
4 | Revises: f42eca714ce0
5 | Create Date: 2024-01-03 15:24:45.889915
6 |
7 | """
8 | from typing import Sequence, Union
9 | import sqlmodel
10 |
11 | from alembic import op
12 | from sqlalchemy.sql import false
13 | import sqlalchemy as sa
14 |
15 |
16 | # revision identifiers, used by Alembic.
17 | revision: str = '075dad69ab65'
18 | down_revision: Union[str, None] = 'f42eca714ce0'
19 | branch_labels: Union[str, Sequence[str], None] = None
20 | depends_on: Union[str, Sequence[str], None] = None
21 |
22 |
23 | def upgrade() -> None:
24 | # ### commands auto generated by Alembic - please adjust! ###
25 | op.add_column('user', sa.Column('must_change_password', sa.Boolean(), nullable=True, server_default=false()))
26 | # ### end Alembic commands ###
27 |
28 |
29 | def downgrade() -> None:
30 | # ### commands auto generated by Alembic - please adjust! ###
31 | op.drop_column('user', 'must_change_password')
32 | # ### end Alembic commands ###
33 |
--------------------------------------------------------------------------------
/fastapi/alembic/versions/48e0ab5941fc_init.py:
--------------------------------------------------------------------------------
1 | """init
2 |
3 | Revision ID: 48e0ab5941fc
4 | Revises:
5 | Create Date: 2023-10-16 18:22:37.026689
6 |
7 | """
8 | from typing import Sequence, Union
9 | import sqlmodel
10 |
11 | from alembic import op
12 | import sqlalchemy as sa
13 |
14 |
15 | # revision identifiers, used by Alembic.
16 | revision: str = '48e0ab5941fc'
17 | down_revision: Union[str, None] = None
18 | branch_labels: Union[str, Sequence[str], None] = None
19 | depends_on: Union[str, Sequence[str], None] = None
20 |
21 |
22 | def upgrade() -> None:
23 | # ### commands auto generated by Alembic - please adjust! ###
24 | op.create_table('course',
25 | sa.Column('id', sa.Integer(), nullable=False),
26 | sa.Column(
27 | 'name', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
28 | sa.Column(
29 | 'color', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
30 | sa.Column('fontDark', sa.Boolean(), nullable=False),
31 | sa.PrimaryKeyConstraint('id'),
32 | sa.UniqueConstraint('name')
33 | )
34 | op.create_table('user',
35 | sa.Column(
36 | 'username', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
37 | sa.Column(
38 | 'full_name', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
39 | sa.Column(
40 | 'role', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
41 | sa.Column('id', sa.Integer(), nullable=False),
42 | sa.Column('hashed_password',
43 | sqlmodel.sql.sqltypes.AutoString(), nullable=False),
44 | sa.PrimaryKeyConstraint('id'),
45 | sa.UniqueConstraint('username')
46 | )
47 | op.create_table('project',
48 | sa.Column('id', sa.Integer(), nullable=False),
49 | sa.Column(
50 | 'uuid', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
51 | sa.Column(
52 | 'name', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
53 | sa.Column('description', sqlmodel.sql.sqltypes.AutoString(),
54 | nullable=False),
55 | sa.Column('owner_id', sa.Integer(), nullable=False),
56 | sa.Column('computation_time', sa.Integer(), nullable=True),
57 | sa.ForeignKeyConstraint(['owner_id'], ['user.id'], ),
58 | sa.PrimaryKeyConstraint('id'),
59 | sa.UniqueConstraint('uuid')
60 | )
61 | op.create_table('usercourselink',
62 | sa.Column('user_id', sa.Integer(), nullable=False),
63 | sa.Column('course_id', sa.Integer(), nullable=False),
64 | sa.ForeignKeyConstraint(['course_id'], ['course.id'], ),
65 | sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
66 | sa.PrimaryKeyConstraint('user_id', 'course_id')
67 | )
68 | op.create_table('homework',
69 | sa.Column('id', sa.Integer(), nullable=False),
70 | sa.Column('course_id', sa.Integer(), nullable=True),
71 | sa.Column('template_project_id',
72 | sa.Integer(), nullable=True),
73 | sa.Column('original_project_id',
74 | sa.Integer(), nullable=True),
75 | sa.Column('deadline', sqlmodel.sql.sqltypes.AutoString(),
76 | nullable=False),
77 | sa.Column('solution_project_id',
78 | sa.Integer(), nullable=True),
79 | sa.Column('solution_start_showing',
80 | sqlmodel.sql.sqltypes.AutoString(), nullable=True),
81 | sa.ForeignKeyConstraint(['course_id'], ['course.id'], ),
82 | sa.ForeignKeyConstraint(
83 | ['original_project_id'], ['project.id'], ),
84 | sa.ForeignKeyConstraint(
85 | ['solution_project_id'], ['project.id'], ),
86 | sa.ForeignKeyConstraint(
87 | ['template_project_id'], ['project.id'], ),
88 | sa.PrimaryKeyConstraint('id'),
89 | sa.UniqueConstraint('template_project_id')
90 | )
91 | op.create_table('editinghomework',
92 | sa.Column('id', sa.Integer(), nullable=False),
93 | sa.Column('homework_id', sa.Integer(), nullable=True),
94 | sa.Column('owner_id', sa.Integer(), nullable=True),
95 | sa.Column('submission', sqlmodel.sql.sqltypes.AutoString(),
96 | nullable=True),
97 | sa.Column('number_of_compilations',
98 | sa.Integer(), nullable=True),
99 | sa.Column('number_of_runs', sa.Integer(), nullable=True),
100 | sa.Column('number_of_tests', sa.Integer(), nullable=True),
101 | sa.ForeignKeyConstraint(
102 | ['homework_id'], ['homework.id'], ),
103 | sa.ForeignKeyConstraint(['owner_id'], ['user.id'], ),
104 | sa.PrimaryKeyConstraint('id')
105 | )
106 | # ### end Alembic commands ###
107 |
108 |
109 | def downgrade() -> None:
110 | # ### commands auto generated by Alembic - please adjust! ###
111 | op.drop_table('editinghomework')
112 | op.drop_table('homework')
113 | op.drop_table('usercourselink')
114 | op.drop_table('project')
115 | op.drop_table('user')
116 | op.drop_table('course')
117 | # ### end Alembic commands ###
118 |
--------------------------------------------------------------------------------
/fastapi/alembic/versions/f031d57aa4e6_added_main_class_to_project.py:
--------------------------------------------------------------------------------
1 | """added main_class to project
2 |
3 | Revision ID: f031d57aa4e6
4 | Revises: 48e0ab5941fc
5 | Create Date: 2023-09-30 17:30:06.131495
6 |
7 | """
8 | from typing import Sequence, Union
9 | import sqlmodel
10 |
11 | from alembic import op
12 | import sqlalchemy as sa
13 |
14 |
15 | # revision identifiers, used by Alembic.
16 | revision: str = 'f031d57aa4e6'
17 | down_revision: Union[str, None] = '48e0ab5941fc'
18 | branch_labels: Union[str, Sequence[str], None] = None
19 | depends_on: Union[str, Sequence[str], None] = None
20 |
21 |
22 | def upgrade() -> None:
23 | # ### commands auto generated by Alembic - please adjust! ###
24 | op.add_column('project', sa.Column('main_class', sqlmodel.sql.sqltypes.AutoString(
25 | ), nullable=False, server_default='Schoco.java/'))
26 | # ### end Alembic commands ###
27 |
28 |
29 | def downgrade() -> None:
30 | # ### commands auto generated by Alembic - please adjust! ###
31 | op.drop_column('project', 'main_class')
32 | # ### end Alembic commands ###
33 |
--------------------------------------------------------------------------------
/fastapi/alembic/versions/f42eca714ce0_added_enable_tests_to_homework.py:
--------------------------------------------------------------------------------
1 | """added enable_tests to homework
2 |
3 | Revision ID: f42eca714ce0
4 | Revises: f031d57aa4e6
5 | Create Date: 2023-10-21 16:20:39.538990
6 |
7 | """
8 | from typing import Sequence, Union
9 | import sqlmodel
10 |
11 | from alembic import op
12 | import sqlalchemy as sa
13 |
14 |
15 | # revision identifiers, used by Alembic.
16 | revision: str = 'f42eca714ce0'
17 | down_revision: Union[str, None] = 'f031d57aa4e6'
18 | branch_labels: Union[str, Sequence[str], None] = None
19 | depends_on: Union[str, Sequence[str], None] = None
20 |
21 |
22 | def upgrade() -> None:
23 | # ### commands auto generated by Alembic - please adjust! ###
24 | op.add_column('homework', sa.Column('enable_tests', sa.Boolean(), nullable=True, default=True))
25 | # ### end Alembic commands ###
26 |
27 |
28 | def downgrade() -> None:
29 | # ### commands auto generated by Alembic - please adjust! ###
30 | op.drop_column('homework', 'enable_tests')
31 | # ### end Alembic commands ###
32 |
--------------------------------------------------------------------------------
/fastapi/alembic_dev.ini:
--------------------------------------------------------------------------------
1 | # A generic, single database configuration.
2 |
3 | [alembic]
4 | # path to migration scripts
5 | script_location = alembic
6 |
7 | # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
8 | # Uncomment the line below if you want the files to be prepended with date and time
9 | # see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
10 | # for all available tokens
11 | # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
12 |
13 | # sys.path path, will be prepended to sys.path if present.
14 | # defaults to the current working directory.
15 | prepend_sys_path = .
16 |
17 | # timezone to use when rendering the date within the migration file
18 | # as well as the filename.
19 | # If specified, requires the python-dateutil library that can be
20 | # installed by adding `alembic[tz]` to the pip requirements
21 | # string value is passed to dateutil.tz.gettz()
22 | # leave blank for localtime
23 | # timezone =
24 |
25 | # max length of characters to apply to the
26 | # "slug" field
27 | # truncate_slug_length = 40
28 |
29 | # set to 'true' to run the environment during
30 | # the 'revision' command, regardless of autogenerate
31 | # revision_environment = false
32 |
33 | # set to 'true' to allow .pyc and .pyo files without
34 | # a source .py file to be detected as revisions in the
35 | # versions/ directory
36 | # sourceless = false
37 |
38 | # version location specification; This defaults
39 | # to alembic/versions. When using multiple version
40 | # directories, initial revisions must be specified with --version-path.
41 | # The path separator used here should be the separator specified by "version_path_separator" below.
42 | # version_locations = %(here)s/bar:%(here)s/bat:alembic/versions
43 |
44 | # version path separator; As mentioned above, this is the character used to split
45 | # version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
46 | # If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
47 | # Valid values for version_path_separator are:
48 | #
49 | # version_path_separator = :
50 | # version_path_separator = ;
51 | # version_path_separator = space
52 | version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
53 |
54 | # set to 'true' to search source files recursively
55 | # in each "version_locations" directory
56 | # new in Alembic version 1.10
57 | # recursive_version_locations = false
58 |
59 | # the output encoding used when revision files
60 | # are written from script.py.mako
61 | # output_encoding = utf-8
62 |
63 | sqlalchemy.url = sqlite:///./data/sql_app.db
64 |
65 |
66 | [post_write_hooks]
67 | # post_write_hooks defines scripts or Python functions that are run
68 | # on newly generated revision scripts. See the documentation for further
69 | # detail and examples
70 |
71 | # format using "black" - use the console_scripts runner, against the "black" entrypoint
72 | # hooks = black
73 | # black.type = console_scripts
74 | # black.entrypoint = black
75 | # black.options = -l 79 REVISION_SCRIPT_FILENAME
76 |
77 | # Logging configuration
78 | [loggers]
79 | keys = root,sqlalchemy,alembic
80 |
81 | [handlers]
82 | keys = console
83 |
84 | [formatters]
85 | keys = generic
86 |
87 | [logger_root]
88 | level = WARN
89 | handlers = console
90 | qualname =
91 |
92 | [logger_sqlalchemy]
93 | level = WARN
94 | handlers =
95 | qualname = sqlalchemy.engine
96 |
97 | [logger_alembic]
98 | level = INFO
99 | handlers =
100 | qualname = alembic
101 |
102 | [handler_console]
103 | class = StreamHandler
104 | args = (sys.stderr,)
105 | level = NOTSET
106 | formatter = generic
107 |
108 | [formatter_generic]
109 | format = %(levelname)-5.5s [%(name)s] %(message)s
110 | datefmt = %H:%M:%S
111 |
--------------------------------------------------------------------------------
/fastapi/alembic_prod.ini:
--------------------------------------------------------------------------------
1 | # A generic, single database configuration.
2 |
3 | [alembic]
4 | # path to migration scripts
5 | script_location = alembic
6 |
7 | # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
8 | # Uncomment the line below if you want the files to be prepended with date and time
9 | # see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
10 | # for all available tokens
11 | # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
12 |
13 | # sys.path path, will be prepended to sys.path if present.
14 | # defaults to the current working directory.
15 | prepend_sys_path = .
16 |
17 | # timezone to use when rendering the date within the migration file
18 | # as well as the filename.
19 | # If specified, requires the python-dateutil library that can be
20 | # installed by adding `alembic[tz]` to the pip requirements
21 | # string value is passed to dateutil.tz.gettz()
22 | # leave blank for localtime
23 | # timezone =
24 |
25 | # max length of characters to apply to the
26 | # "slug" field
27 | # truncate_slug_length = 40
28 |
29 | # set to 'true' to run the environment during
30 | # the 'revision' command, regardless of autogenerate
31 | # revision_environment = false
32 |
33 | # set to 'true' to allow .pyc and .pyo files without
34 | # a source .py file to be detected as revisions in the
35 | # versions/ directory
36 | # sourceless = false
37 |
38 | # version location specification; This defaults
39 | # to alembic/versions. When using multiple version
40 | # directories, initial revisions must be specified with --version-path.
41 | # The path separator used here should be the separator specified by "version_path_separator" below.
42 | # version_locations = %(here)s/bar:%(here)s/bat:alembic/versions
43 |
44 | # version path separator; As mentioned above, this is the character used to split
45 | # version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
46 | # If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
47 | # Valid values for version_path_separator are:
48 | #
49 | # version_path_separator = :
50 | # version_path_separator = ;
51 | # version_path_separator = space
52 | version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
53 |
54 | # set to 'true' to search source files recursively
55 | # in each "version_locations" directory
56 | # new in Alembic version 1.10
57 | # recursive_version_locations = false
58 |
59 | # the output encoding used when revision files
60 | # are written from script.py.mako
61 | # output_encoding = utf-8
62 |
63 | sqlalchemy.url = sqlite:////app/data/sql_app.db
64 |
65 |
66 | [post_write_hooks]
67 | # post_write_hooks defines scripts or Python functions that are run
68 | # on newly generated revision scripts. See the documentation for further
69 | # detail and examples
70 |
71 | # format using "black" - use the console_scripts runner, against the "black" entrypoint
72 | # hooks = black
73 | # black.type = console_scripts
74 | # black.entrypoint = black
75 | # black.options = -l 79 REVISION_SCRIPT_FILENAME
76 |
77 | # Logging configuration
78 | [loggers]
79 | keys = root,sqlalchemy,alembic
80 |
81 | [handlers]
82 | keys = console
83 |
84 | [formatters]
85 | keys = generic
86 |
87 | [logger_root]
88 | level = WARN
89 | handlers = console
90 | qualname =
91 |
92 | [logger_sqlalchemy]
93 | level = WARN
94 | handlers =
95 | qualname = sqlalchemy.engine
96 |
97 | [logger_alembic]
98 | level = INFO
99 | handlers =
100 | qualname = alembic
101 |
102 | [handler_console]
103 | class = StreamHandler
104 | args = (sys.stderr,)
105 | level = NOTSET
106 | formatter = generic
107 |
108 | [formatter_generic]
109 | format = %(levelname)-5.5s [%(name)s] %(message)s
110 | datefmt = %H:%M:%S
111 |
--------------------------------------------------------------------------------
/fastapi/api.py:
--------------------------------------------------------------------------------
1 | """ from fastapi import APIRouter
2 | import users
3 |
4 | router = APIRouter()
5 | router.include_router(users.users)
6 | """
7 |
--------------------------------------------------------------------------------
/fastapi/auth.py:
--------------------------------------------------------------------------------
1 | from passlib.context import CryptContext
2 | import models_and_schemas
3 | from jose import jwt
4 | from datetime import datetime, timedelta
5 | from fastapi.security import OAuth2PasswordBearer
6 | from fastapi import Depends, HTTPException
7 | from config import settings
8 |
9 | pwd_context = CryptContext(schemes=["bcrypt"])
10 | oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login")
11 |
12 |
13 | def create_password_hash(password):
14 | return pwd_context.hash(password)
15 |
16 |
17 | def verify_password(plain_password, hashed_password):
18 | return pwd_context.verify(plain_password, hashed_password)
19 |
20 |
21 | def create_access_token(user: models_and_schemas.User):
22 | claims = {
23 | "sub": user.username,
24 | "name": user.full_name,
25 | "role": user.role,
26 | "exp": datetime.utcnow() + timedelta(days=settings.JWT_EXP_DAYS)
27 | }
28 | return jwt.encode(claims=claims, key=settings.SECRET_KEY, algorithm="HS256")
29 |
30 |
31 | def decode_token(token):
32 | claims = jwt.decode(token, key=settings.SECRET_KEY)
33 | return claims
34 |
35 |
36 | def get_username_by_token(token: str = Depends(oauth2_scheme)):
37 | claims = jwt.decode(token, key=settings.SECRET_KEY)
38 | return claims.get('sub')
39 |
40 |
41 | def check_teacher(token: str = Depends(oauth2_scheme)):
42 | claims = decode_token(token)
43 | role = claims.get('role')
44 | if role != "teacher":
45 | raise HTTPException(
46 | status_code=403,
47 | detail="Only teachers!",
48 | headers={"WWW-Authenticate": "Bearer"}
49 | )
50 | return claims
51 |
--------------------------------------------------------------------------------
/fastapi/config.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseSettings
2 | import os
3 |
4 |
5 | class Settings(BaseSettings):
6 | SECRET_KEY: str = os.urandom(24)
7 | TEACHER_KEY: str = os.urandom(10)
8 | GITEA_USERNAME: str = "schoco"
9 | GITEA_PASSWORD: str = "schoco1234"
10 | GITEA_LOCALHOST_PORT: int = 3000
11 | GITEA_HOST: str = ""
12 | MAX_CONTAINERS: int = 8
13 | PRODUCTION: bool = True
14 | JWT_EXP_DAYS: int = 15
15 | FULL_DATA_PATH: str = "/app/data"
16 | BACKEND_VER: str = "1.3.1"
17 |
18 |
19 | settings = Settings()
20 |
--------------------------------------------------------------------------------
/fastapi/database_config.py:
--------------------------------------------------------------------------------
1 | from sqlmodel import Session, SQLModel, create_engine
2 | from config import settings
3 |
4 | if settings.PRODUCTION:
5 | SQLITE_DATABASE_URL = "sqlite:////app/data/sql_app.db"
6 | else:
7 | SQLITE_DATABASE_URL = "sqlite:///./data/sql_app.db"
8 |
9 | engine = create_engine(SQLITE_DATABASE_URL, connect_args={
10 | "check_same_thread": False})
11 |
12 |
13 | def get_db():
14 | with Session(engine) as session:
15 | yield session
16 |
17 |
18 | def create_db_and_tables():
19 | SQLModel.metadata.create_all(engine)
20 |
--------------------------------------------------------------------------------
/fastapi/gunicorn_conf.py:
--------------------------------------------------------------------------------
1 | import json
2 | import multiprocessing
3 | import os
4 |
5 | workers_per_core_str = os.getenv("WORKERS_PER_CORE", "1")
6 | max_workers_str = os.getenv("MAX_WORKERS")
7 | use_max_workers = None
8 | if max_workers_str:
9 | use_max_workers = int(max_workers_str)
10 | web_concurrency_str = os.getenv("WEB_CONCURRENCY", None)
11 |
12 | host = os.getenv("HOST", "0.0.0.0")
13 | port = os.getenv("PORT", "80")
14 | bind_env = os.getenv("BIND", None)
15 | use_loglevel = os.getenv("LOG_LEVEL", "debug")
16 | if bind_env:
17 | use_bind = bind_env
18 | else:
19 | use_bind = f"{host}:{port}"
20 |
21 | cores = multiprocessing.cpu_count()
22 | workers_per_core = float(workers_per_core_str)
23 | default_web_concurrency = workers_per_core * cores
24 | if web_concurrency_str:
25 | web_concurrency = int(web_concurrency_str)
26 | assert web_concurrency > 0
27 | else:
28 | web_concurrency = max(int(default_web_concurrency), 2)
29 | if use_max_workers:
30 | web_concurrency = min(web_concurrency, use_max_workers)
31 | accesslog_var = os.getenv("ACCESS_LOG", "-")
32 | use_accesslog = accesslog_var or None
33 | errorlog_var = os.getenv("ERROR_LOG", "-")
34 | use_errorlog = errorlog_var or None
35 | graceful_timeout_str = os.getenv("GRACEFUL_TIMEOUT", "120")
36 | timeout_str = os.getenv("TIMEOUT", "120")
37 | keepalive_str = os.getenv("KEEP_ALIVE", "5")
38 |
39 | # Gunicorn config variables
40 | loglevel = use_loglevel
41 | workers = web_concurrency
42 | bind = use_bind
43 | errorlog = use_errorlog
44 | worker_tmp_dir = "/dev/shm"
45 | accesslog = use_accesslog
46 | graceful_timeout = int(graceful_timeout_str)
47 | timeout = int(timeout_str)
48 | keepalive = int(keepalive_str)
49 |
50 |
51 | # For debugging and testing
52 | log_data = {
53 | "loglevel": loglevel,
54 | "workers": workers,
55 | "bind": bind,
56 | "graceful_timeout": graceful_timeout,
57 | "timeout": timeout,
58 | "keepalive": keepalive,
59 | "errorlog": errorlog,
60 | "accesslog": accesslog,
61 | # Additional, non-gunicorn variables
62 | "workers_per_core": workers_per_core,
63 | "use_max_workers": use_max_workers,
64 | "host": host,
65 | "port": port,
66 | }
67 | print(json.dumps(log_data))
68 |
69 | preload_app = True
70 |
--------------------------------------------------------------------------------
/fastapi/java_helloWorld/de/Schoco.java:
--------------------------------------------------------------------------------
1 | public class {
2 | // Hier kannst du Instanzvariablen deklarieren, z.B.:
3 | // private String name;
4 |
5 | /**
6 | * Konstruktor für die -Klasse
7 | */
8 | public () {
9 |
10 | }
11 |
12 | #main#public static void main(String[] args) {
13 | object1 = new ();
14 | }!#main#
15 | }
16 |
--------------------------------------------------------------------------------
/fastapi/java_helloWorld/de/Tests.java:
--------------------------------------------------------------------------------
1 | import static org.junit.Assert.*;
2 | import org.junit.BeforeClass;
3 | import org.junit.Test;
4 |
5 | // 🚀 Im Wiki gibts eine Anleitung: https://github.com/PhiTux/schoco/wiki/JUnit-Tests-anlegen-%28de%29
6 |
7 | /**
8 | * ⭐⭐⭐
9 | * Diese Klasse / Datei ist für die Schüler/innen NICHT sichtbar.
10 | * Sie ist die einzige Datei, die JUnit-Tests enthalten darf.
11 | * Sie kann NICHT umbenannt werden.
12 | * ⭐⭐⭐
13 | */
14 | public class Tests {
15 | /** 🛑 Ändere nichts an diesem ersten Codeblock!!
16 | * Er aktiviert den Security-Manager, der die Rechte des Schülercodes während der Tests einschränkt.
17 | * Dadurch können Sicherheitslücken verhindert werden. */
18 | @BeforeClass
19 | public static void setUp() {
20 | System.setSecurityManager(new SecurityManager());
21 | }
22 | /* 🛑 Ende des Security-Codes
23 | *================================================*/
24 |
25 |
26 | // Zwei Beispiele für JUnit-Tests
27 | @Test
28 | public void isGreaterTrue() {
29 | /*Schoco schoco = new Schoco();
30 | assertTrue("Num 1 is greater than Num 2", schoco.isGreater(4, 3));
31 | assertTrue(schoco.isGreater(4, 3));*/
32 | }
33 |
34 | @Test
35 | public void isGreaterFalse() {
36 | /*Schoco schoco = new Schoco();
37 | assertEquals(4, schoco.addition(1, 3));*/
38 | }
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/fastapi/java_helloWorld/en/Schoco.java:
--------------------------------------------------------------------------------
1 | public class {
2 | // Here you can declare instance variables like:
3 | // private String name;
4 |
5 | /**
6 | * Constructor for the class
7 | */
8 | public () {
9 |
10 | }
11 |
12 | #main#public static void main(String[] args) {
13 | object1 = new ();
14 | }!#main#
15 | }
16 |
--------------------------------------------------------------------------------
/fastapi/java_helloWorld/en/Tests.java:
--------------------------------------------------------------------------------
1 | import static org.junit.Assert.*;
2 | import org.junit.BeforeClass;
3 | import org.junit.Test;
4 |
5 | // 🚀 See the wiki for explanation: https://github.com/PhiTux/schoco/wiki/Create-JUnit-Tests-%28en%29
6 |
7 | /**
8 | * ⭐⭐⭐
9 | * This class / file is NOT visible to students.
10 | * It is the only file which may contain the JUnit-Tests.
11 | * You can NOT rename this file.
12 | * ⭐⭐⭐
13 | */
14 | public class Tests {
15 | /** 🛑 don't touch the following code-block!!
16 | * It activates the security-manager which ensures limited rights of student's code during Testing. */
17 | @BeforeClass
18 | public static void setUp() {
19 | System.setSecurityManager(new SecurityManager());
20 | }
21 | /* 🛑 end of security code
22 | *================================================*/
23 |
24 |
25 | // Two examples for JUnit-Tests
26 | @Test
27 | public void isGreaterTrue() {
28 | /*Schoco schoco = new Schoco();
29 | assertTrue("Num 1 is greater than Num 2", schoco.isGreater(4, 3));
30 | assertTrue(schoco.isGreater(4, 3));*/
31 | }
32 |
33 | @Test
34 | public void isGreaterFalse() {
35 | /*Schoco schoco = new Schoco();
36 | assertEquals(4, schoco.addition(1, 3));*/
37 | }
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/fastapi/main.py:
--------------------------------------------------------------------------------
1 | from fastapi import FastAPI, Request
2 | from fastapi.middleware.cors import CORSMiddleware
3 | from fastapi_jwt_auth.exceptions import AuthJWTException
4 | from fastapi.responses import JSONResponse
5 | from multiprocessing import Lock, Manager
6 | import users
7 | import code
8 | import cookies_api
9 | from fastapi_utils.tasks import repeat_every
10 | from fastapi.logger import logger
11 | import logging
12 |
13 | gunicorn_logger = logging.getLogger('gunicorn.error')
14 | logger.handlers = gunicorn_logger.handlers
15 | if __name__ != "main":
16 | logger.setLevel(gunicorn_logger.level)
17 | else:
18 | logger.setLevel(logging.DEBUG)
19 |
20 |
21 | app = FastAPI()
22 |
23 | app.include_router(users.users)
24 | app.include_router(code.code)
25 |
26 |
27 | @app.exception_handler(AuthJWTException)
28 | def authjwt_exception_handler(request: Request, exc: AuthJWTException):
29 | return JSONResponse(
30 | status_code=exc.status_code,
31 | content={"detail": exc.message}
32 | )
33 |
34 |
35 | origins = [
36 | "http://lab:5173",
37 | "http://127.0.0.1:5173",
38 | "http://localhost:5173",
39 | ]
40 |
41 | app.add_middleware(CORSMiddleware, allow_origins=origins,
42 | allow_credentials=True, allow_methods=["*"], allow_headers=["*"], expose_headers=["content-disposition"])
43 |
44 |
45 | lock = Lock()
46 |
47 | m = Manager()
48 | firstRun = m.Value(bool, True)
49 |
50 |
51 | # we repeat it every 60 seconds to refill the queue (in case there's some hickup...)
52 | @app.on_event("startup")
53 | @repeat_every(seconds=60)
54 | def startup_event():
55 | # logger.info("startup_event")
56 | if firstRun.value:
57 | firstRun.value = False
58 | # delete all container_dirs from previous runs
59 | cookies_api.remove_all_container_dirs()
60 |
61 | with lock:
62 | cookies_api.fillNewContainersQueue()
63 | else:
64 | with lock:
65 | cookies_api.refillNewContainersQueue()
66 |
67 |
68 | @app.on_event("shutdown")
69 | def shutdown_event():
70 | # delete all containers and container_dirs
71 | cookies_api.kill_all_containers()
72 | cookies_api.remove_all_container_dirs()
73 | logger.info("Goodbye!")
74 |
--------------------------------------------------------------------------------
/fastapi/models_and_schemas.py:
--------------------------------------------------------------------------------
1 | from sqlmodel import SQLModel, Field, Relationship
2 | from enum import Enum
3 | from typing import Optional, List
4 | from pydantic import BaseModel
5 |
6 | # alembic readme:
7 | """
8 | When updating the database models, you need to create a new revision with Alembic.
9 | BEFORE creating the model, make sure, that your old DB has included the metadata of the old alembic-revision.
10 | -> call: alembic -c alembic_dev.ini upgrade head
11 |
12 | Then edit the models and create a new revision:
13 | -> call: alembic -c alembic_dev.ini revision --autogenerate -m "EXCPLAIN WHAT YOU CHANGED"
14 |
15 | Afterwards actually update the dev-database:
16 | -> call: alembic -c alembic_dev.ini upgrade head
17 | """
18 |
19 | # database models
20 |
21 |
22 | class UserCourseLink(SQLModel, table=True):
23 | user_id: Optional[int] = Field(
24 | default=None, foreign_key="user.id", primary_key=True)
25 | course_id: Optional[int] = Field(
26 | default=None, foreign_key="course.id", primary_key=True)
27 |
28 |
29 | class Roles(str, Enum):
30 | pupil = "pupil"
31 | teacher = "teacher"
32 |
33 |
34 | class BaseUser(SQLModel):
35 | username: str = Field(unique=True)
36 | full_name: str
37 | role: Roles
38 |
39 |
40 | class User(BaseUser, table=True):
41 | id: Optional[int] = Field(default=None, primary_key=True)
42 | hashed_password: str
43 | must_change_password: Optional[bool] = False
44 |
45 | courses: List["Course"] = Relationship(
46 | back_populates="users", link_model=UserCourseLink)
47 |
48 | projects: List["Project"] = Relationship(back_populates="owner")
49 |
50 | homeworks: List["EditingHomework"] = Relationship(back_populates="owner")
51 |
52 |
53 | class UserSchema(BaseUser):
54 | password: str
55 |
56 |
57 | class Course(SQLModel, table=True):
58 | id: Optional[int] = Field(default=None, primary_key=True)
59 | name: str = Field(unique=True)
60 | color: str
61 | fontDark: bool
62 |
63 | users: List["User"] = Relationship(
64 | back_populates="courses", link_model=UserCourseLink)
65 | homeworks: List["Homework"] = Relationship(back_populates="course")
66 |
67 |
68 | class Project(SQLModel, table=True):
69 | id: Optional[int] = Field(default=None, primary_key=True)
70 | uuid: str = Field(unique=True)
71 | name: str
72 | description: str
73 | owner_id: int = Field(foreign_key="user.id")
74 | computation_time: Optional[int] = 10
75 | main_class: str
76 |
77 | owner: "User" = Relationship(back_populates="projects")
78 |
79 |
80 | class Homework(SQLModel, table=True):
81 | id: Optional[int] = Field(default=None, primary_key=True)
82 | course_id: int = Field(
83 | default=None, foreign_key="course.id")
84 | template_project_id: int = Field(
85 | default=None, foreign_key="project.id", unique=True)
86 | original_project_id: int = Field(
87 | default=None, foreign_key="project.id")
88 | deadline: str # datetime
89 | solution_project_id: Optional[int] = Field(
90 | default=None, foreign_key="project.id")
91 | # datetime, when the solution may be shown to pupils
92 | solution_start_showing: Optional[str] = ""
93 | enable_tests: Optional[bool] = True
94 |
95 | course: "Course" = Relationship(back_populates="homeworks")
96 |
97 |
98 | class EditingHomework(SQLModel, table=True):
99 | id: Optional[int] = Field(default=None, primary_key=True)
100 | # uuid: str = Field(unique=True) -> get uuid from howework->template-project
101 | homework_id: int = Field(
102 | default=None, foreign_key="homework.id")
103 | owner_id: int = Field(
104 | default=None, foreign_key="user.id") # owner_id = branch_name
105 |
106 | submission: Optional[str] = ""
107 | # submission having the structure {passed_tests: ..., failed_tests: ...}
108 |
109 | number_of_compilations: Optional[int] = 0
110 | number_of_runs: Optional[int] = 0
111 | number_of_tests: Optional[int] = 0
112 |
113 | owner: "User" = Relationship()
114 | homework: "Homework" = Relationship()
115 |
116 | # other models
117 |
118 |
119 | class newPupil(BaseModel):
120 | fullname: str
121 | username: str
122 | password: str
123 |
124 |
125 | class pupilsList(BaseModel):
126 | newPupils: List[newPupil]
127 | courseIDs: List[int]
128 |
129 |
130 | class setPassword(BaseModel):
131 | username: str
132 | password: str
133 |
134 |
135 | class AddUserCourseLink(BaseModel):
136 | user_id: int
137 | coursename: str
138 |
139 |
140 | class UserById(BaseModel):
141 | user_id: int
142 |
143 |
144 | class changeName(BaseModel):
145 | user_id: int
146 | name: str
147 |
148 |
149 | class newProject(BaseModel):
150 | projectName: str
151 | className: str
152 | projectDescription: str
153 | language: str
154 |
155 |
156 | class updateText(BaseModel):
157 | text: str
158 |
159 |
160 | class FileChanges(BaseModel):
161 | path: str
162 | content: str
163 | sha: str
164 |
165 |
166 | class FileChangesList(BaseModel):
167 | changes: List[FileChanges]
168 |
169 |
170 | class File(BaseModel):
171 | path: str
172 | content: str
173 |
174 |
175 | class filesList(BaseModel):
176 | files: List[File]
177 |
178 |
179 | class startCompile(BaseModel):
180 | port: int
181 | container_uuid: str
182 | save_output: bool
183 |
184 |
185 | class startExecute(BaseModel):
186 | port: int
187 | container_uuid: str
188 | save_output: bool
189 |
190 |
191 | class startTest(BaseModel):
192 | port: int
193 | container_uuid: str
194 |
195 |
196 | class create_homework(BaseModel):
197 | files: List[File]
198 | course_id: int
199 | deadline_date: str
200 | computation_time: int
201 | enable_tests: bool
202 |
203 |
204 | class homeworkId(BaseModel):
205 | id: int
206 |
207 |
208 | class courseID(BaseModel):
209 | id: int
210 |
211 |
212 | class RenameFile(BaseModel):
213 | old_path: str
214 | new_path: str
215 | content: str
216 | sha: str
217 |
218 |
219 | class RenameHomework(BaseModel):
220 | id: int
221 | new_name: str
222 |
223 |
224 | class ChangePassword(BaseModel):
225 | oldPassword: str
226 | newPassword: str
227 |
228 |
229 | class AddClass(BaseModel):
230 | className: str
231 | language: str
232 |
233 |
234 | class DeleteFile(BaseModel):
235 | path: str
236 | sha: str
237 |
238 |
239 | class UpdateHomeworkSettings(BaseModel):
240 | id: int
241 | deadline_date: str
242 | computation_time: int
243 | enable_tests: bool
244 |
245 |
246 | class UUID(BaseModel):
247 | uuid: str
248 |
249 |
250 | class Password(BaseModel):
251 | password: str
252 |
253 |
254 | class AddSolution(BaseModel):
255 | homework_id: int
256 | solution_id: int
257 | solution_start_showing: str
258 |
259 |
260 | class DeleteSolution(BaseModel):
261 | homework_id: int
262 |
263 |
264 | class EntryPoint(BaseModel):
265 | entry_point: str
266 |
267 |
268 | class SkipVersion(BaseModel):
269 | skip_version: str
270 |
271 |
272 | class ComputationTime(BaseModel):
273 | computation_time: int
274 |
--------------------------------------------------------------------------------
/fastapi/prestart.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | mkdir -p ./data/containers
4 | mkdir -p ./data/projects
5 |
6 | # call alembic upgrades
7 | alembic -c alembic_prod.ini upgrade head
8 |
--------------------------------------------------------------------------------
/fastapi/requirements.txt:
--------------------------------------------------------------------------------
1 | fastapi==0.86.0
2 | fastapi_jwt_auth==0.5.0
3 | jose==1.0.0
4 | passlib==1.7.4
5 | python_jose==3.3.0
6 | sqlmodel==0.0.8
7 | uvicorn
8 | python-multipart
9 | bcrypt==4.0.1
10 | pycurl==7.45.2
11 | docker
12 | fastapi_utils
13 | alembic
14 |
--------------------------------------------------------------------------------
/frontend/.env.development:
--------------------------------------------------------------------------------
1 | VITE_API_URL=http://lab:8000/api/
2 | #VITE_API_URL=http://0.0.0.0:8000/api/
3 | VITE_APP_TITLE=Schoco
--------------------------------------------------------------------------------
/frontend/.env.production:
--------------------------------------------------------------------------------
1 | VITE_API_URL=/api/
2 | VITE_APP_TITLE=Schoco
--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
1 | # Vue 3 + Vite
2 |
3 | This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `
12 |