diff --git a/.gitignore b/.gitignore index d3c652fd439d8383ba3a20db17a1b4500514e910..6c86200ee337b50675bc910ab0d31fb57ebb2642 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,7 @@ config.py .vscode/ documentation +/deploy/.env.dev +/deploy/dbdata/* +/deploy/log/nginx/access.log +/deploy/log/nginx/error.log diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..238646df1b09fdb832c528ed803acdc5b190a8c2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +FROM python:3.7-slim + +WORKDIR /srv/app + +COPY requirements.txt /srv/app + +RUN apt-get update && \ + apt-get install -y \ + build-essential \ + make \ + gcc \ + pkg-config + +#ython3-matplotlib + + +RUN pip install --upgrade pip -r requirements.txt + +COPY app/. /srv/app/app +COPY run.py /srv/app +COPY config.py /srv/app +COPY messages.pot /srv/app + +RUN mkdir logs + diff --git a/deploy/config/mysql/rating.cnf b/deploy/config/mysql/rating.cnf new file mode 100644 index 0000000000000000000000000000000000000000..45d4224d41dc28791e58629a162995149577eebe --- /dev/null +++ b/deploy/config/mysql/rating.cnf @@ -0,0 +1,2 @@ +[mysqld] +explicit_defaults_for_timestamp = 1 \ No newline at end of file diff --git a/deploy/config/mysql/schema.sql b/deploy/config/mysql/schema.sql new file mode 100644 index 0000000000000000000000000000000000000000..c12823cb96e14983b9f50014ab8180e3d2b134e9 --- /dev/null +++ b/deploy/config/mysql/schema.sql @@ -0,0 +1,229 @@ +/* +SQL initialization script + +Run: mysql -u rating -p -D rating_db < create_rating_db.sql + +This will create user 'admin' with password 'password'. + +*/ + +CREATE DATABASE IF NOT EXISTS rating_db; +USE rating_db; +GRANT ALL PRIVILEGES ON rating_db.* TO 'rating'@'%' WITH GRANT OPTION; + +/* Drop all tables (cascade removes foreign keys also) */ + +DROP TABLE IF EXISTS background_question_answer; +DROP TABLE IF EXISTS background_question_option; +DROP TABLE IF EXISTS background_question; +DROP TABLE IF EXISTS forced_id; +DROP TABLE IF EXISTS user; +DROP TABLE IF EXISTS trial_randomization; +DROP TABLE IF EXISTS embody_answer; +DROP TABLE IF EXISTS embody_question; +DROP TABLE IF EXISTS answer; +DROP TABLE IF EXISTS answer_set; +DROP TABLE IF EXISTS question; +DROP TABLE IF EXISTS page; +DROP TABLE IF EXISTS experiment; + +/* Experiment set */ +CREATE TABLE experiment ( + idexperiment INTEGER NOT NULL AUTO_INCREMENT, + name VARCHAR(120), + instruction TEXT, + directoryname VARCHAR(120), + language VARCHAR(120), + status VARCHAR(120), + randomization VARCHAR(120), + short_instruction TEXT, + single_sentence_instruction TEXT, + is_archived VARCHAR(120), + creator_name VARCHAR(120), + research_notification_filename VARCHAR(120), + creation_time DATETIME, + stimulus_size VARCHAR(120), + consent_text TEXT, + use_forced_id VARCHAR(120), + PRIMARY KEY (idexperiment) +); + +/* Answer set holds session information about users experiment */ +CREATE TABLE answer_set ( + idanswer_set INTEGER NOT NULL AUTO_INCREMENT, + experiment_idexperiment INTEGER, + session VARCHAR(120), + agreement VARCHAR(120), + answer_counter INTEGER, + registration_time DATETIME, + last_answer_time DATETIME, + PRIMARY KEY (idanswer_set), + FOREIGN KEY(experiment_idexperiment) REFERENCES experiment (idexperiment) +); + +/* Background questions are asked before the experiment begins */ +CREATE TABLE background_question ( + idbackground_question INTEGER NOT NULL AUTO_INCREMENT, + background_question VARCHAR(120), + experiment_idexperiment INTEGER, + PRIMARY KEY (idbackground_question) +); +CREATE TABLE background_question_option ( + idbackground_question_option INTEGER NOT NULL AUTO_INCREMENT, + background_question_idbackground_question INTEGER, + `option` VARCHAR(120), + PRIMARY KEY (idbackground_question_option), + FOREIGN KEY(background_question_idbackground_question) REFERENCES background_question (idbackground_question) +); +CREATE TABLE background_question_answer ( + idbackground_question_answer INTEGER NOT NULL AUTO_INCREMENT, + answer_set_idanswer_set INTEGER, + answer VARCHAR(120), + background_question_idbackground_question INTEGER, + PRIMARY KEY (idbackground_question_answer), + FOREIGN KEY(answer_set_idanswer_set) REFERENCES answer_set (idanswer_set), + FOREIGN KEY(background_question_idbackground_question) REFERENCES background_question (idbackground_question) +); + +/* Randomize experiment page order */ +CREATE TABLE trial_randomization ( + idtrial_randomization INTEGER NOT NULL AUTO_INCREMENT, + page_idpage INTEGER, + randomized_idpage INTEGER, + answer_set_idanswer_set INTEGER, + experiment_idexperiment INTEGER, + PRIMARY KEY (idtrial_randomization) +); + +CREATE TABLE user ( + id INTEGER NOT NULL AUTO_INCREMENT, + username VARCHAR(64), + email VARCHAR(120), + password_hash VARCHAR(128), + PRIMARY KEY (id) +); + +/* By using forced ID login subjects can only participate to a rating task by logging in with a pregenerated ID */ +CREATE TABLE forced_id ( + idforced_id INTEGER NOT NULL AUTO_INCREMENT, + experiment_idexperiment INTEGER, + pregenerated_id VARCHAR(120), + PRIMARY KEY (idforced_id), + FOREIGN KEY(experiment_idexperiment) REFERENCES experiment (idexperiment) +); + +/* Information about stimulus type and content on a page */ +CREATE TABLE page ( + idpage INTEGER NOT NULL AUTO_INCREMENT, + experiment_idexperiment INTEGER, + type VARCHAR(120), + text VARCHAR(120), + media VARCHAR(120), + PRIMARY KEY (idpage), + FOREIGN KEY(experiment_idexperiment) REFERENCES experiment (idexperiment) +); + +/* Slider question */ +CREATE TABLE question ( + idquestion INTEGER NOT NULL AUTO_INCREMENT, + experiment_idexperiment INTEGER, + question VARCHAR(120), + `left` VARCHAR(120), + `right` VARCHAR(120), + PRIMARY KEY (idquestion), + FOREIGN KEY(experiment_idexperiment) REFERENCES experiment (idexperiment) +); + +/* Slider answer */ +CREATE TABLE answer ( + idanswer INTEGER NOT NULL AUTO_INCREMENT, + question_idquestion INTEGER, + answer_set_idanswer_set INTEGER, + answer VARCHAR(120), + page_idpage INTEGER, + PRIMARY KEY (idanswer), + FOREIGN KEY(answer_set_idanswer_set) REFERENCES answer_set (idanswer_set), + FOREIGN KEY(page_idpage) REFERENCES page (idpage), + FOREIGN KEY(question_idquestion) REFERENCES question (idquestion) +); + +/* Create indexes for faster operations */ +CREATE INDEX ix_experiment_consent_text ON experiment (consent_text(255)); +CREATE INDEX ix_experiment_creation_time ON experiment (creation_time); +CREATE UNIQUE INDEX ix_experiment_directoryname ON experiment (directoryname); +CREATE INDEX ix_experiment_instruction ON experiment (instruction(255)); +CREATE INDEX ix_experiment_name ON experiment (name); +CREATE INDEX ix_experiment_short_instruction ON experiment (short_instruction(255)); +CREATE INDEX ix_experiment_single_sentence_instruction ON experiment (single_sentence_instruction(255)); +CREATE UNIQUE INDEX ix_user_email ON user (email); +CREATE UNIQUE INDEX ix_user_username ON user (username); +CREATE INDEX ix_answer_set_last_answer_time ON answer_set (last_answer_time); +CREATE INDEX ix_answer_set_registration_time ON answer_set (registration_time); +CREATE INDEX ix_page_media ON page (media); +CREATE INDEX ix_page_text ON page (text); +CREATE INDEX ix_page_type ON page (type); + + +/* New fields for updating embody tool to onni.utu.fi */ + +/* Embody picture/question information */ +CREATE TABLE embody_question ( + idembody INTEGER NOT NULL AUTO_INCREMENT, + experiment_idexperiment INTEGER, + picture TEXT, + question TEXT, + PRIMARY KEY (idembody), + FOREIGN KEY(experiment_idexperiment) REFERENCES experiment (idexperiment) +); + +/* Embody answer (coordinates). Answer is saved as a json object: + {x:[1,2,100,..], y:[3,4,101,..], r:[13,13,8,...]} */ +CREATE TABLE embody_answer ( + idanswer INTEGER NOT NULL AUTO_INCREMENT, + answer_set_idanswer_set INTEGER, + page_idpage INTEGER, + embody_question_idembody INTEGER DEFAULT 0, + coordinates TEXT, + PRIMARY KEY (idanswer), + FOREIGN KEY(answer_set_idanswer_set) REFERENCES answer_set (idanswer_set), + FOREIGN KEY(page_idpage) REFERENCES page (idpage) , + FOREIGN KEY(embody_question_idembody) REFERENCES embody_question (idembody) +); + + +/* Set flag if embody tool is enabled -> this is not the most modular solution, but works for now */ +ALTER TABLE experiment ADD COLUMN (embody_enabled BOOLEAN DEFAULT 0); + +/* Set current answer type (embody/slider/etc..) so returning users are routed to correct question */ +ALTER TABLE answer_set ADD COLUMN (answer_type VARCHAR(120)); + +INSERT INTO user VALUES(1,'admin',NULL,'pbkdf2:sha256:50000$6Cc6Mjmo$3fe413a88db1bacfc4d617f7c1547bd1ea4cbd6c5d675a58e78332201f6befc6'); + +/* eyelabs */ +INSERT INTO user VALUES(2,'eyelabs',NULL,'pbkdf2:sha256:50000$sdBu3Rjm$7ab97c6d2686460b85a2a20517b7012c15ffb341ba3fef5b0f17ed8354fc38d9'); + + +CREATE TABLE research_group ( + id INTEGER NOT NULL AUTO_INCREMENT, + name TEXT, + tag TEXT, + description TEXT, + PRIMARY KEY (id) +); + +INSERT INTO research_group(id, name, tag, description) VALUES(1, 'Human Emotion Systems', 'emotion', 'Welcome to the Human Emotion Systems -laboratory`s Onni-net laboratory! The experiments that are currently underway are listed below - you can participate for as many experiments you want.'); +INSERT INTO research_group(id, name, tag, description) VALUES(2, 'Turku Eye-tracking', 'eyelabs', 'Welcome to the Turku Eyelabs -laboratory`s Onni-net laboratory! The experiments that are currently underway are listed below - you can participate for as many experiments you want.'); + +CREATE TABLE user_in_group ( + idgroup INTEGER, + iduser INTEGER, + role TEXT, + FOREIGN KEY(idgroup) REFERENCES research_group (id), + FOREIGN KEY(iduser) REFERENCES user (id) +); + +INSERT INTO user_in_group VALUES (1,1, 'admin'); +INSERT INTO user_in_group VALUES (2,1, 'admin'); + +ALTER TABLE experiment ADD COLUMN (group_id INTEGER), ADD FOREIGN KEY(group_id) REFERENCES research_group(id); + diff --git a/deploy/config/nginx/rating.conf b/deploy/config/nginx/rating.conf new file mode 100644 index 0000000000000000000000000000000000000000..7fa4bb9e1e4efa8fb97be27b97ba1742cf1a3d6b --- /dev/null +++ b/deploy/config/nginx/rating.conf @@ -0,0 +1,64 @@ + +upstream flask_app { + server gunicorn:8000 fail_timeout=5s max_fails=5; +} + +server { + # NON-SSL + listen 80; + + # SSL + #listen 443 ssl http2; + #listen [::]:443 ssl http2; + + #ssl_certificate "/etc/nginx/tls/onni.utu.fi-rsa+chain.pem"; + #ssl_certificate_key "/etc/nginx/tls/private/onni.utu.fi-rsa.key"; + #ssl_protocols TLSv1.2; + #ssl_session_cache shared:SSL:1m; + #ssl_session_timeout 10m; + #ssl_ciphers HIGH:!aNULL:!MD5; + #ssl_prefer_server_ciphers on; + + #server_name _; + + client_max_body_size 0; + + location / { + proxy_pass http://flask_app; + + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $host; + proxy_redirect off; + + proxy_read_timeout 30s; + } + + location /create_embody { + #location /socket.io { + + proxy_pass http://flask_app/socket.io; + proxy_http_version 1.1; + proxy_redirect off; + proxy_buffering off; + + proxy_connect_timeout 1d; + proxy_send_timeout 1d; + proxy_read_timeout 1d; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + + } + + error_page 404 /404.html; + location = /40x.html { + } + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + } +} diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..8fea8eafdef17362d00fb5dc840bcb8d7d422345 --- /dev/null +++ b/deploy/docker-compose.yml @@ -0,0 +1,43 @@ +version: "3.3" + +services: + nginx: + #image: nginx:latest + build: ./nginx + restart: always + ports: + - "80:80" + #- "443:443" + volumes: + #- ./tls:/etc/nginx/tls + #- ./config/nginx/rating.conf:/etc/nginx/conf.d/rating.conf + - ./log/nginx:/var/log/nginx + depends_on: + - gunicorn + + gunicorn: + image: rating_app:latest + restart: always + ports: + - 8000:8000 + env_file: + - ./.env.dev + depends_on: + - db + command: sh -c "gunicorn run:app -b 0.0.0.0:8000 -k gevent --worker-connections=1000 --workers=1 --log-level debug -t 180" + #volumes: FOR DEV + #- ./application:/srv/app + + db: + image: mysql + ports: + - "3200:3306" + environment: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: rating_db + MYSQL_USER: rating + MYSQL_PASSWORD: rating_passwd + volumes: + - ./config/mysql/rating.cnf:/etc/mysql/conf.d/rating.cnf + - ./config/mysql/schema.sql:/docker-entrypoint-initdb.d/1.sql + - ./dbdata:/var/lib/mysql diff --git a/deploy/nginx/Dockerfile b/deploy/nginx/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..8e5916e0c933d2a9ec34f39b7d933bc744228c0c --- /dev/null +++ b/deploy/nginx/Dockerfile @@ -0,0 +1,4 @@ +FROM nginx:1.19.0-alpine + +RUN rm /etc/nginx/conf.d/default.conf +COPY nginx.conf /etc/nginx/conf.d \ No newline at end of file diff --git a/deploy/nginx/nginx.conf b/deploy/nginx/nginx.conf new file mode 100644 index 0000000000000000000000000000000000000000..7fa4bb9e1e4efa8fb97be27b97ba1742cf1a3d6b --- /dev/null +++ b/deploy/nginx/nginx.conf @@ -0,0 +1,64 @@ + +upstream flask_app { + server gunicorn:8000 fail_timeout=5s max_fails=5; +} + +server { + # NON-SSL + listen 80; + + # SSL + #listen 443 ssl http2; + #listen [::]:443 ssl http2; + + #ssl_certificate "/etc/nginx/tls/onni.utu.fi-rsa+chain.pem"; + #ssl_certificate_key "/etc/nginx/tls/private/onni.utu.fi-rsa.key"; + #ssl_protocols TLSv1.2; + #ssl_session_cache shared:SSL:1m; + #ssl_session_timeout 10m; + #ssl_ciphers HIGH:!aNULL:!MD5; + #ssl_prefer_server_ciphers on; + + #server_name _; + + client_max_body_size 0; + + location / { + proxy_pass http://flask_app; + + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $host; + proxy_redirect off; + + proxy_read_timeout 30s; + } + + location /create_embody { + #location /socket.io { + + proxy_pass http://flask_app/socket.io; + proxy_http_version 1.1; + proxy_redirect off; + proxy_buffering off; + + proxy_connect_timeout 1d; + proxy_send_timeout 1d; + proxy_read_timeout 1d; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + + } + + error_page 404 /404.html; + location = /40x.html { + } + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + } +} diff --git a/requirements.txt b/requirements.txt index 5b482fd0a7d61d18ef1ca1228b934c22a189553c..59c0e41fc73b6ddcaf9d18579fd987eda7945226 100644 --- a/requirements.txt +++ b/requirements.txt @@ -31,3 +31,4 @@ uuid==1.30 Werkzeug==0.14.1 WTForms==2.2.1 WTForms-SQLAlchemy==0.1 +cryptography