Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot] 8f0ae041cf
Bump terser from 5.9.0 to 5.14.2
Bumps [terser](https://github.com/terser/terser) from 5.9.0 to 5.14.2.
- [Release notes](https://github.com/terser/terser/releases)
- [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/terser/terser/commits)

---
updated-dependencies:
- dependency-name: terser
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-20 10:51:25 +00:00
144 changed files with 5663 additions and 28588 deletions

View File

@ -12,7 +12,6 @@ module.exports = {
plugins: ["@typescript-eslint"],
rules: {
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/ban-types": "off",
indent: ["error", 2],
"linebreak-style": ["error", "unix"],

2
.gitignore vendored
View File

@ -16,5 +16,3 @@ scripts/creds.py
__pycache__/
*.py[cod]
*$py.class
src/sailfish/
src/libcachebust_data.json

View File

@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT\n synced_till\n FROM\n survey_mcaptcha_campaign\n WHERE \n campaign_id = $1;",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "synced_till",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "05b7fe6d93a4c988e9eae32f4a57e369f9ddc703b8fd3251c6baa52b60c98a1d"
}

View File

@ -1,16 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO survey_admins \n (name , password, secret) VALUES ($1, $2, $3)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Text",
"Varchar"
]
},
"nullable": []
},
"hash": "0d22134cc5076304b7895827f006ee8269cc500f400114a7472b83f0f1c568b5"
}

View File

@ -1,26 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT name, id FROM survey_campaigns ORDER BY id;",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": []
},
"nullable": [
false,
false
]
},
"hash": "10924f3726a45c3bc709118375d691f2867bbcd50dc47a000ac9bf3ff878c97c"
}

View File

@ -1,28 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO survey_responses (\n user_id,\n campaign_id,\n device_user_provided,\n device_software_recognised,\n threads,\n submitted_at,\n submission_bench_type_id\n ) VALUES (\n $1, $2, $3, $4, $5, $6,\n (SELECT ID FROM survey_bench_type WHERE name = $7)\n )\n RETURNING ID;",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Uuid",
"Uuid",
"Varchar",
"Varchar",
"Int4",
"Timestamptz",
"Text"
]
},
"nullable": [
false
]
},
"hash": "117f1ae18f6a3936f27446b75b555951fe217d3a3cefe40a006fdd3cb31f0ac4"
}

View File

@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO survey_mcaptcha_upload_job_states \n (name) VALUES ($1) ON CONFLICT (name) DO NOTHING;",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar"
]
},
"nullable": []
},
"hash": "11ff04344412d1a2e5fdb1ab654fe4e90c2ba897bb4889426031ffacc2ae06e4"
}

View File

@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT password FROM survey_admins WHERE name = ($1)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "password",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "1373df097fa0e58b23a374753318ae53a44559aa0e7eb64680185baf1c481723"
}

View File

@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT\n survey_admins.name\n FROM\n survey_admins\n INNER JOIN survey_campaigns ON\n survey_admins.ID = survey_campaigns.user_id\n WHERE\n survey_campaigns.ID = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "name",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false
]
},
"hash": "15a8484de6f035e56c34ce3f6979eadea81f125933f76261c8b3c8319d43bbe0"
}

View File

@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE\n survey_mcaptcha_campaign\n SET\n synced_till = $1\n WHERE \n campaign_id = $2; ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int4",
"Text"
]
},
"nullable": []
},
"hash": "163a1ab861234bbf52b1b1c03bbac0d37bbbb539146f93c6fba24ffd80ad1485"
}

View File

@ -1,16 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE\n survey_mcaptcha_upload_jobs\n SET\n job_state = (SELECT ID FROM survey_mcaptcha_upload_job_states WHERE name = $1),\n scheduled_at = $2\n WHERE public_id = $3;",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Timestamptz",
"Text"
]
},
"nullable": []
},
"hash": "18495d6198079fdb8e4806d8a59aa0a1abee44a8b568ce74fa275ab936e8362f"
}

View File

@ -1,28 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT name, password FROM survey_admins WHERE email = ($1)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "password",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false
]
},
"hash": "19686bfe8772cbc6831d46d18994e2b9aa40c7181eae9a31e51451cce95f04e8"
}

View File

@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "DELETE \n FROM survey_campaigns \n WHERE \n user_id = (\n SELECT \n ID \n FROM \n survey_admins \n WHERE \n name = $1\n )\n AND\n id = ($2)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Uuid"
]
},
"nullable": []
},
"hash": "1972be28a6bda2c3a3764a836e95c8cb0c5db277fc4c8a9b19951a03166c6492"
}

View File

@ -1,17 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "insert into survey_admins \n (name , password, email, secret) values ($1, $2, $3, $4)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Text",
"Varchar",
"Varchar"
]
},
"nullable": []
},
"hash": "1b7e17bfc949fa97e8dec1f95e35a02bcf3aa1aa72a1f6f6c8884e885fc3b953"
}

View File

@ -1,64 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n survey_mcaptcha_upload_jobs.ID,\n survey_mcaptcha_upload_jobs.public_id,\n survey_mcaptcha_campaign.campaign_id,\n survey_mcaptcha_campaign.public_id as campaign_public_id,\n survey_mcaptcha_upload_job_states.name,\n survey_mcaptcha_upload_jobs.created_at,\n survey_mcaptcha_upload_jobs.scheduled_at,\n survey_mcaptcha_upload_jobs.finished_at\n\n FROM survey_mcaptcha_upload_jobs\n INNER JOIN\n survey_mcaptcha_upload_job_states\n ON\n survey_mcaptcha_upload_job_states.ID = survey_mcaptcha_upload_jobs.job_state\n INNER JOIN\n survey_mcaptcha_campaign\n ON\n survey_mcaptcha_campaign.ID = survey_mcaptcha_upload_jobs.campaign_id\n WHERE\n survey_mcaptcha_upload_job_states.name = $1;",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int4"
},
{
"ordinal": 1,
"name": "public_id",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "campaign_id",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "campaign_public_id",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 6,
"name": "scheduled_at",
"type_info": "Timestamptz"
},
{
"ordinal": 7,
"name": "finished_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
false,
false,
false,
false,
true,
true
]
},
"hash": "1e41c42d89762ff4dc4b60a534a54db2741b325727c01852cbc68ea8442d15ef"
}

View File

@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT\n public_id\n FROM\n survey_mcaptcha_campaign\n WHERE\n campaign_id = $1\n AND\n url_id = (SELECT ID FROM survey_mcaptcha_hostname WHERE secret = $2);",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "public_id",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false
]
},
"hash": "2904486838bed381aa00f6a1b1e9b860a74b07b15256f3764434901471ff820b"
}

View File

@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM survey_admins WHERE name = ($1)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "2ccaecfee4d2f29ef5278188b304017719720aa986d680d4727a1facbb869c7a"
}

View File

@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS (SELECT 1 from survey_mcaptcha_upload_job_states WHERE name = $1)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "2d18e0fad79c6df26465f82eca20cdfca35a710f34a54ac115d23435762a3038"
}

View File

@ -1,16 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO\n survey_mcaptcha_campaign (campaign_id, public_id, url_id)\n VALUES ($1, $2, (SELECT ID FROM survey_mcaptcha_hostname WHERE secret = $3));",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Text"
]
},
"nullable": []
},
"hash": "38a517b011519ec80d35d12ea463e7aed1f25290a5f3e8b19c5aa781da362ae3"
}

View File

@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO survey_users (created_at, id) VALUES($1, $2)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Timestamptz",
"Uuid"
]
},
"nullable": []
},
"hash": "43b3e771f38bf8059832169227705be06a28925af1b3799ffef5371d511fd138"
}

View File

@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT\n difficulty\n FROM\n survey_benches\n WHERE\n duration <= $1\n ORDER BY difficulty ASC LIMIT 1 OFFSET $2;",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "difficulty",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Float4",
"Int8"
]
},
"nullable": [
false
]
},
"hash": "52c16c2c0759140af6348ef7de56b74151a20532ceebc8ee41d079decee3acb5"
}

View File

@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS (SELECT 1 from survey_admins WHERE name = $1)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "536541ecf2e1c0403c74b6e2e09b42b73a7741ae4a348ff539ac410022e03ace"
}

View File

@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE survey_admins set password = $1\n WHERE name = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": []
},
"hash": "55dde28998a6d12744806035f0a648494a403c7d09ea3caf91bf54869a81aa73"
}

View File

@ -1,61 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT\n survey_responses.ID,\n survey_responses.device_software_recognised,\n survey_responses.threads,\n survey_responses.user_id,\n survey_responses.submitted_at,\n survey_responses.device_user_provided,\n survey_bench_type.name\n FROM\n survey_responses\n INNER JOIN survey_bench_type ON\n survey_responses.submission_bench_type_id = survey_bench_type.ID\n WHERE\n survey_responses.campaign_id = (\n SELECT ID FROM survey_campaigns\n WHERE\n ID = $1\n AND\n user_id = (SELECT ID FROM survey_admins WHERE name = $2)\n )\n LIMIT $3 OFFSET $4",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int4"
},
{
"ordinal": 1,
"name": "device_software_recognised",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "threads",
"type_info": "Int4"
},
{
"ordinal": 3,
"name": "user_id",
"type_info": "Uuid"
},
{
"ordinal": 4,
"name": "submitted_at",
"type_info": "Timestamptz"
},
{
"ordinal": 5,
"name": "device_user_provided",
"type_info": "Varchar"
},
{
"ordinal": 6,
"name": "name",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Uuid",
"Text",
"Int8",
"Int8"
]
},
"nullable": [
false,
false,
true,
false,
false,
false,
false
]
},
"hash": "57c673ad8529371d77aa305917cf680dd2273ead74c3583ef0322f472b1d33fd"
}

View File

@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT name \n FROM survey_campaigns\n WHERE \n id = $1\n AND\n user_id = (SELECT ID from survey_admins WHERE name = $2)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "name",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Uuid",
"Text"
]
},
"nullable": [
false
]
},
"hash": "58ec3b8f98c27e13ec2732f8ee23f6eb9845ac5d9fd97b1e5c9f2eed4b1f5693"
}

View File

@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS (SELECT 1 from survey_mcaptcha_hostname WHERE url = $1)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "5c1ad3208ece06ba7a503d650e15d06906e56018798cba2b4672c393327131aa"
}

View File

@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(difficulty) FROM survey_benches WHERE duration <= $1;",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Float4"
]
},
"nullable": [
null
]
},
"hash": "63370a30a4ff6d31292a3cb632c66184ccff75583e21df5ddf5e8872f710d3d2"
}

View File

@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE survey_admins set email = $1\n WHERE name = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Text"
]
},
"nullable": []
},
"hash": "683707dbc847b37c58c29aaad0d1a978c9fe0657da13af99796e4461134b5a43"
}

View File

@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS (SELECT 1 from survey_admins WHERE email = $1)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "6a26daa84578aed2b2085697cb8358ed7c0a50ba9597fd387b4b09b0a8a154db"
}

View File

@ -1,17 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO\n survey_mcaptcha_analytics (\n campaign_id, time, difficulty_factor, worker_type\n )\n VALUES ((\n SELECT\n ID\n FROM\n survey_mcaptcha_campaign\n WHERE \n campaign_id = $1\n ), $2, $3, $4\n );",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Int4",
"Int4",
"Varchar"
]
},
"nullable": []
},
"hash": "6c8fda20aa4a9174a5b008032d493773274ebfbf9dc204d89609cdff1ebc0335"
}

View File

@ -1,28 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT \n name, id\n FROM \n survey_campaigns \n WHERE\n user_id = (\n SELECT \n ID\n FROM \n survey_admins\n WHERE\n name = $1\n )",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false
]
},
"hash": "70cc7bfc9b6ff5b68db70c069c0947d51bfc4a53cedc020016ee25ff98586c93"
}

View File

@ -1,42 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT id, time, difficulty_factor, worker_type FROM survey_mcaptcha_analytics\n WHERE \n campaign_id = (\n SELECT \n ID FROM survey_mcaptcha_campaign \n WHERE \n public_id = $1\n )\n ORDER BY ID\n OFFSET $2 LIMIT $3\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int4"
},
{
"ordinal": 1,
"name": "time",
"type_info": "Int4"
},
{
"ordinal": 2,
"name": "difficulty_factor",
"type_info": "Int4"
},
{
"ordinal": 3,
"name": "worker_type",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text",
"Int8",
"Int8"
]
},
"nullable": [
false,
false,
false,
false
]
},
"hash": "714925a5209400a17bcafe23c34ce9546106e8bdd788c27ee579b278e671bcb0"
}

View File

@ -1,64 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n survey_mcaptcha_upload_jobs.ID,\n survey_mcaptcha_upload_jobs.public_id,\n survey_mcaptcha_campaign.campaign_id,\n survey_mcaptcha_campaign.public_id as campaign_public_id,\n survey_mcaptcha_upload_job_states.name,\n survey_mcaptcha_upload_jobs.created_at,\n survey_mcaptcha_upload_jobs.scheduled_at,\n survey_mcaptcha_upload_jobs.finished_at\n\n FROM survey_mcaptcha_upload_jobs\n INNER JOIN\n survey_mcaptcha_upload_job_states\n ON\n survey_mcaptcha_upload_job_states.ID = survey_mcaptcha_upload_jobs.job_state\n INNER JOIN\n survey_mcaptcha_campaign\n ON\n survey_mcaptcha_campaign.ID = survey_mcaptcha_upload_jobs.campaign_id\n WHERE\n survey_mcaptcha_upload_jobs.public_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int4"
},
{
"ordinal": 1,
"name": "public_id",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "campaign_id",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "campaign_public_id",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 6,
"name": "scheduled_at",
"type_info": "Timestamptz"
},
{
"ordinal": 7,
"name": "finished_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
false,
false,
false,
false,
true,
true
]
},
"hash": "722f2d297a318f9804c1388d427d069a315b45c0c85c0b344d34cd8928b22c9c"
}

View File

@ -1,62 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT\n survey_responses.ID,\n survey_responses.device_software_recognised,\n survey_responses.threads,\n survey_responses.user_id,\n survey_responses.submitted_at,\n survey_responses.device_user_provided,\n survey_bench_type.name\n FROM\n survey_responses\n INNER JOIN survey_bench_type ON\n survey_responses.submission_bench_type_id = survey_bench_type.ID\n WHERE\n survey_bench_type.name = $3\n AND\n survey_responses.campaign_id = (\n SELECT ID FROM survey_campaigns\n WHERE\n ID = $1\n AND\n user_id = (SELECT ID FROM survey_admins WHERE name = $2)\n )\n LIMIT $4 OFFSET $5",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int4"
},
{
"ordinal": 1,
"name": "device_software_recognised",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "threads",
"type_info": "Int4"
},
{
"ordinal": 3,
"name": "user_id",
"type_info": "Uuid"
},
{
"ordinal": 4,
"name": "submitted_at",
"type_info": "Timestamptz"
},
{
"ordinal": 5,
"name": "device_user_provided",
"type_info": "Varchar"
},
{
"ordinal": 6,
"name": "name",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Uuid",
"Text",
"Text",
"Int8",
"Int8"
]
},
"nullable": [
false,
false,
true,
false,
false,
false,
false
]
},
"hash": "74c41e33f91cf31ea13582c8b3ca464544374842450d580517ca2bd01d67402e"
}

View File

@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS (\n SELECT\n url\n FROM\n survey_mcaptcha_hostname\n WHERE secret = $1\n )",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "7d764a7b1c2991dda7498f243c6e4bd83fdf431e3510f9afb0ef5e9b10f35181"
}

View File

@ -1,18 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO survey_campaigns (\n user_id, ID, name, difficulties, created_at\n ) VALUES(\n (SELECT id FROM survey_admins WHERE name = $1),\n $2, $3, $4, $5\n );",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Uuid",
"Varchar",
"Int4Array",
"Timestamptz"
]
},
"nullable": []
},
"hash": "82feafc36533144e49ba374c8c47ca4aa0d6558a9803778ad28cfa7b62382c3e"
}

View File

@ -1,28 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT\n created_at,\n ID\n FROM\n survey_users\n WHERE\n ID = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 1,
"name": "id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
false
]
},
"hash": "858a4c06a5c1ba7adb79bcac7d42d106d09d0cbff10c197f2242dcb5c437a1df"
}

View File

@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO survey_mcaptcha_hostname (url, secret) VALUES ($1, $2)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar"
]
},
"nullable": []
},
"hash": "94205e3e65a8f6bf315a282ec8fcc64119dc08e5565925bb2a3f5fccf663b5ab"
}

View File

@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT difficulties FROM survey_campaigns WHERE id = $1;",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "difficulties",
"type_info": "Int4Array"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false
]
},
"hash": "9cdade613ce724631cc3f187510758ee0929e93ff3f8ce81fe35594756644246"
}

View File

@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS (\n SELECT\n ID\n FROM\n survey_mcaptcha_campaign\n WHERE\n campaign_id = $1\n AND\n url_id = (\n SELECT\n ID\n FROM\n survey_mcaptcha_hostname\n WHERE\n secret = $2\n )\n )",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
null
]
},
"hash": "9da39f618b9dea08360d4c1625650b5055de47a7e89f99ffc589b99d22b8ac9d"
}

View File

@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM survey_mcaptcha_hostname WHERE secret = $1 AND url =$2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": []
},
"hash": "a3cddc0ace32cfb7df70e171b2618c7fe6d7824bbfcbae8248905e927049528b"
}

View File

@ -1,16 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO survey_benches \n (resp_id, difficulty, duration) \n VALUES ($1, $2, $3);",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int4",
"Int4",
"Float4"
]
},
"nullable": []
},
"hash": "a721cfa249acf328c2f29c4cf8c2aeba1a635bcf49d18ced5474caa10b7cae4f"
}

View File

@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE survey_admins set secret = $1\n WHERE name = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Text"
]
},
"nullable": []
},
"hash": "ab951c5c318174c6538037947c2f52c61bcfe5e5be1901379b715e77f5214dd2"
}

View File

@ -1,28 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT\n duration,\n difficulty\n FROM\n survey_benches\n WHERE\n resp_id = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "duration",
"type_info": "Float4"
},
{
"ordinal": 1,
"name": "difficulty",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Int4"
]
},
"nullable": [
false,
false
]
},
"hash": "b2619292aa6bd1ac38dca152cbe607b795a151ddc212361a3c6d8c70ea1c93eb"
}

View File

@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE survey_mcaptcha_hostname set secret = $1 WHERE url = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Text"
]
},
"nullable": []
},
"hash": "baabef729999fe63426b3b2373f1ecbf294d4bfddbce04209269644f4a7511ed"
}

View File

@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE survey_admins set name = $1\n WHERE name = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Text"
]
},
"nullable": []
},
"hash": "c757589ef26a005e3285e7ab20d8a44c4f2e1cb125f8db061dd198cc380bf807"
}

View File

@ -1,65 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n survey_mcaptcha_upload_jobs.ID,\n survey_mcaptcha_upload_jobs.public_id,\n survey_mcaptcha_campaign.campaign_id,\n survey_mcaptcha_campaign.public_id as campaign_public_id,\n survey_mcaptcha_upload_job_states.name,\n survey_mcaptcha_upload_jobs.created_at,\n survey_mcaptcha_upload_jobs.scheduled_at,\n survey_mcaptcha_upload_jobs.finished_at\n\n FROM survey_mcaptcha_upload_jobs\n INNER JOIN\n survey_mcaptcha_upload_job_states\n ON\n survey_mcaptcha_upload_job_states.ID = survey_mcaptcha_upload_jobs.job_state\n INNER JOIN\n survey_mcaptcha_campaign\n ON\n survey_mcaptcha_campaign.ID = survey_mcaptcha_upload_jobs.campaign_id\n WHERE\n survey_mcaptcha_campaign.campaign_id = $1\n AND\n survey_mcaptcha_upload_job_states.name = $2;",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int4"
},
{
"ordinal": 1,
"name": "public_id",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "campaign_id",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "campaign_public_id",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 6,
"name": "scheduled_at",
"type_info": "Timestamptz"
},
{
"ordinal": 7,
"name": "finished_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false,
false,
false,
false,
false,
false,
true,
true
]
},
"hash": "ca41f4e15fa5c5657a525ed9385a92214b644194443ae165957d9659d30dc3f9"
}

View File

@ -1,34 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT\n survey_mcaptcha_campaign.campaign_id,\n survey_mcaptcha_upload_jobs.public_id,\n survey_mcaptcha_hostname.url\n FROM\n survey_mcaptcha_campaign\n INNER JOIN\n survey_mcaptcha_upload_jobs\n ON\n survey_mcaptcha_upload_jobs.campaign_id = survey_mcaptcha_campaign.ID\n INNER JOIN\n survey_mcaptcha_hostname\n ON\n survey_mcaptcha_hostname.ID = survey_mcaptcha_campaign.url_id\n WHERE\n survey_mcaptcha_upload_jobs.job_state = (\n SELECT ID FROM survey_mcaptcha_upload_job_states WHERE name = $1\n )\n AND\n survey_mcaptcha_upload_jobs.finished_at is NULL\n AND\n survey_mcaptcha_upload_jobs.scheduled_at is NULL\n ORDER BY created_at ASC;",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "campaign_id",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "public_id",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "url",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
false
]
},
"hash": "d7a099c6f381fd02ad6a114b0146e4e52f7886f0164d05ccd3f1818a2a70cf67"
}

View File

@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM\n survey_mcaptcha_campaign\n WHERE\n campaign_id = $1\n AND\n url_id = (\n SELECT\n ID\n FROM\n survey_mcaptcha_hostname\n WHERE\n secret = $2\n )",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": []
},
"hash": "dbe5d5c450a50bb829a39e6149eb4e6307547120b10762140d250f163b584a23"
}

View File

@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT secret FROM survey_admins WHERE name = ($1)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "secret",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "e9cf5d6d8c9e8327d5c809d47a14a933f324e267f1e7dbb48e1caf1c021adc3f"
}

View File

@ -1,17 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO survey_mcaptcha_upload_jobs\n (campaign_id, job_state, created_at, public_id)\n VALUES (\n (SELECT ID FROM survey_mcaptcha_campaign WHERE campaign_id = $1),\n (SELECT ID FROM survey_mcaptcha_upload_job_states WHERE name = $2),\n $3, $4)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
"Timestamptz",
"Varchar"
]
},
"nullable": []
},
"hash": "ebfc456dd76b3fb2e5484f935703ad6aa4712c782222f2015b92916827f81079"
}

View File

@ -1,38 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT ID, name, difficulties, created_at FROM survey_campaigns",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "difficulties",
"type_info": "Int4Array"
},
{
"ordinal": 3,
"name": "created_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": []
},
"nullable": [
false,
false,
false,
false
]
},
"hash": "efa0e41910fa5bcb187ba9e2fc8f37bee5b25ffe9a2d175f39a69899bc559965"
}

View File

@ -1,16 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE\n survey_mcaptcha_upload_jobs\n SET\n job_state = (SELECT ID FROM survey_mcaptcha_upload_job_states WHERE name = $1),\n finished_at = $2\n WHERE public_id = $3;",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Timestamptz",
"Text"
]
},
"nullable": []
},
"hash": "fade9f99846165c34486f6492ece38148bf0dd2d79e1a4f97b8cbf04015ceff0"
}

View File

@ -1,16 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO survey_response_tokens \n (resp_id, user_id, id)\n VALUES ($1, $2, $3);",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int4",
"Uuid",
"Uuid"
]
},
"nullable": []
},
"hash": "fcdc5fe5d496eb516c805e64ec96d9626b74ab33cd6e75e5a08ae88967403b72"
}

View File

@ -1,59 +0,0 @@
pipeline:
backend:
image: rust
environment:
- DATABASE_URL=postgres://postgres:password@database:5432/postgres
- GIT_HASH=8e77345f1597e40c2e266cb4e6dee74888918a61 # dummy value
- OPEN_API_DOCS=8e77345f1597e40c2e266cb4e6dee74888918a61
- COMPILED_DATE=2021-07-21
commands:
- apt-get update
- apt-get install -y ca-certificates curl gnupg tar wget libssl-dev
- mkdir -p /etc/apt/keyrings
- curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
- NODE_MAJOR=18 echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
- apt-get -y install nodejs npm
- npm install --global yarn
- rustup component add rustfmt
- rustup component add clippy
- make dev-env
- make migrate
- make frontend
- make test
build_docker_img:
image: plugins/docker
when:
event: [pull_request]
settings:
dry_run: true
repo: mcaptcha/survey
tags: latest
build_and_publish_docker_img:
image: plugins/docker
when:
event: [push, tag, deployment]
settings:
username: mcaptcha
password:
from_secret: DOCKER_TOKEN
repo: mcaptcha/survey
tags: latest
# publish_bins:
# image: rust
# when:
# event: [push, tag, deployment]
# commands:
# - apt update
# - apt-get -y --no-install-recommends install gpg tar curl wget
# - echo -n "$RELEASE_BOT_GPG_SIGNING_KEY" | gpg --batch --import --pinentry-mode loopback
# - scripts/bin-publish.sh publish master latest $DUMBSERVE_PASSWORD
# secrets: [RELEASE_BOT_GPG_SIGNING_KEY, DUMBSERVE_PASSWORD, GPG_PASSWORD]
services:
database:
image: postgres
environment:
- POSTGRES_PASSWORD=password

3061
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,7 @@ documentation = "https://github.con/mCaptcha/survey"
readme = "https://github.com/mCaptcha/survey/blob/master/README.md"
license = "AGPLv3 or later version"
authors = ["realaravinth <realaravinth@batsense.net>"]
edition = "2021"
edition = "2018"
default-run = "survey"
build = "build.rs"
@ -22,58 +22,50 @@ name = "tests-migrate"
path = "./src/tests-migrate.rs"
[dependencies]
actix-web = "4.3"
actix-identity = "0.4.0"
actix-web = "4.0.1"
actix-identity = "0.4.0-beta.2"
actix-session = { version = "0.6.1", features = ["cookie-session"]}
actix-http = "3.0.4"
actix-rt = "2"
actix-cors = "0.6.1"
actix-files = "0.6.0"
actix-cors = "0.6.0-beta.2"
actix-service = "2.0.0"
#actix = "0.12"
actix-web-codegen-const-routes = "0.2.0"
my-codegen = {package = "actix-web-codegen", git ="https://github.com/realaravinth/actix-web"}
#libmcaptcha = { branch = "master", git = "https://github.com/mCaptcha/libmcaptcha", features = ["full"] }
futures = "0.3.15"
sqlx = { version = "0.7", features = [ "runtime-tokio-rustls", "postgres", "time", "uuid" ] }
sqlx = { version = "0.5.9", features = [ "runtime-actix-rustls", "postgres", "time", "offline" ] }
#argon2-creds = "0.2.3"
argon2-creds = { branch = "master", git = "https://github.com/realaravinth/argon2-creds"}
derive_builder = "0.11"
validator = { version = "0.14", features = ["derive"]}
derive_more = "0.99"
config = "0.13"
config = "0.11"
serde = "1"
serde_json = "1"
pretty_env_logger = "0.5"
pretty_env_logger = "0.4"
log = "0.4"
lazy_static = "1.4"
url = { version = "2.2", features = ["serde"] }
url = "2.2"
urlencoding = "2.1.0"
rand = "0.8"
uuid = { version = "1.4.1", features = ["v4", "serde"] }
uuid = { version="0.8.2", features = ["v4"]}
mime_guess = "2.0.3"
rust-embed = "6.0.0"
#libcachebust = "0.3.0"
cache-buster = { git = "https://github.com/realaravinth/cache-buster" }
mime = "0.3.16"
#sailfish = "0.3.2"
tracing = { version = "0.1.37", features = ["log"] }
tera = { version="1.17.1", features=["builtins"]}
tokio = { version = "1.25.0", features = ["fs", "macros"] }
csv-async = { version = "1.2.5", features = ["serde", "tokio"] }
async-trait = "0.1.68"
reqwest = { version = "0.11.18", features = ["json", "gzip", "native-tls-vendored"] }
sailfish = "0.3.2"
#tokio = "1.11.0"
@ -85,11 +77,9 @@ version = "0.2"
[build-dependencies]
sqlx = { version = "0.5.9", features = [ "runtime-actix-rustls", "uuid", "postgres", "time", "offline" ] }
#serde_yaml = "0.8.17"
sqlx = { version = "0.7", features = [ "runtime-tokio-rustls", "uuid", "postgres", "time"] }
serde_json = "1"
#yaml-rust = "0.4.5"
cache-buster = { version = "0.2.0", git = "https://github.com/realaravinth/cache-buster" }
mime = "0.3.16"
[dev-dependencies]
mktemp = "0.5.0"

View File

@ -1,4 +1,4 @@
FROM node:18-bookworm-slim as frontend
FROM node:16.11-bullseye-slim as frontend
LABEL org.opencontainers.image.source https://github.com/mCaptcha/survey
RUN apt-get update && apt-get install -y make
COPY package.json yarn.lock /src/
@ -8,14 +8,14 @@ RUN yarn install
COPY . .
RUN make frontend
FROM rust:latest as rust
FROM rust:1-slim-bullseye as rust
WORKDIR /src
RUN apt-get update && apt-get install -y git libssl-dev
RUN apt-get update && apt-get install -y git
COPY . /src
COPY --from=frontend /src/static/cache/bundle /src/static/cache/bundle
RUN cargo build --release
FROM debian:bookworm
FROM debian:bullseye-slim
RUN useradd -ms /bin/bash -u 1001 mcaptcha-survey
WORKDIR /home/mcaptcha-survey
COPY --from=rust /src/target/release/survey /usr/local/bin/

View File

@ -1,15 +1,3 @@
define deploy_dependencies
@-docker create --name ${db} \
-e POSTGRES_PASSWORD=password \
-p 5433:5432 \
postgres
docker start ${db}
endef
define run_migrations
cargo run --bin tests-migrate
endef
default: frontend ## Debug build
cargo build
@ -21,9 +9,6 @@ clean: ## Clean all build artifacts and dependencies
@-rm -rf ./static/cache/bundle
@-rm -rf ./assets
check: ## Check for syntax errors on all workspaces
cargo check --workspace --tests --all-features
coverage: migrate ## Generate HTML code coverage
cargo tarpaulin -t 1200 --out Html
@ -31,25 +16,11 @@ dev-env: ## Download development dependencies
cargo fetch
yarn install
env.db.recreate: ## Deploy dependencies
@-docker rm -f ${db}
$(call deploy_dependencies)
sleep 5
$(call run_migrations)
env.db: ## Deploy dependencies
$(call deploy_dependencies)
sleep 5
$(call run_migrations)
doc: ## Prepare documentation
cargo doc --no-deps --workspace --all-features
docker: ## Build docker images
docker buildx build -t mcaptcha/survey:master -t mcaptcha/survey:latest . --load
docker build -t mcaptcha/survey:master -t mcaptcha/survey:latest .
docker-publish: docker ## Build and publish docker images
docker push mcaptcha/survey:master
@ -69,7 +40,7 @@ lint: ## Lint codebase
yarn lint
migrate: ## Run database migrations
$(call run_migrations)
cargo run --bin tests-migrate
release: frontend ## Release build
cargo build --release
@ -77,16 +48,10 @@ release: frontend ## Release build
run: default ## Run debug build
cargo run
sqlx-offline-data: ## prepare sqlx offline data
cargo sqlx prepare \
--database-url=${DATABASE_URL} -- \
--all-features \
--bin survey
test: frontend ## Run tests
echo 'static/' && tree static || true
echo 'tree/' && tree assets || true
cargo test --all-features --no-fail-fast -j 1
cargo test --all-features --no-fail-fast
xml-test-coverage: migrate ## Generate cobertura.xml test coverage
cargo tarpaulin -t 1200 --out Xml

View File

@ -7,8 +7,9 @@
</p>
[![Docker](https://img.shields.io/docker/pulls/mcaptcha/survey)](https://hub.docker.com/r/mcaptcha/survey)
[![status-badge](https://ci.batsense.net/api/badges/mCaptcha/survey/status.svg)](https://ci.batsense.net/mCaptcha/survey)
[![Build](https://github.com/mCaptcha/survey/actions/workflows/linux.yml/badge.svg)](https://github.com/mCaptcha/survey/actions/workflows/linux.yml)
[![dependency status](https://deps.rs/repo/github/mCaptcha/survey/status.svg)](https://deps.rs/repo/github/mCaptcha/survey)
[![codecov](https://codecov.io/gh/mCaptcha/survey/branch/master/graph/badge.svg)](https://codecov.io/gh/mCaptcha/survey)
</div>
@ -40,21 +41,3 @@ published to fine-tune their CAPTCHA deployment.
## What data do you collect?
TODO: run program, record and share actual network traffic logs
## Funding
### NLnet
<div align="center">
<img
height="150px"
alt="NLnet NGIZero logo"
src="./docs/third-party/NGIZero-green.hex.svg"
/>
</div>
<br />
2023 development is funded through the [NGI0 Entrust
Fund](https://nlnet.nl/entrust), via [NLnet](https://nlnet.nl/). Please
see [here](https://nlnet.nl/project/mCaptcha/) for more details.

View File

@ -28,13 +28,8 @@ fn main() {
let git_hash = String::from_utf8(output.stdout).unwrap();
println!("cargo:rustc-env=GIT_HASH={}", git_hash);
let now = OffsetDateTime::now_utc();
println!(
"cargo:rustc-env=COMPILED_DATE={}-{}-{}",
now.year(),
now.month(),
now.day()
);
let now = OffsetDateTime::now_utc().format("%y-%m-%d");
println!("cargo:rustc-env=COMPILED_DATE={}", &now);
cache_bust();
}

View File

@ -1,8 +1,7 @@
debug = true
allow_registration = true
source_code = "https://github.com/mcaptcha/survey"
default_campaign = "4e951e01-71ee-4a18-9b97-782965495ae3"
support_email="support@example.org"
default_campaign = "b6b261fa-3ef9-4d7f-8852-339b8f81bb01"
[server]
# Please set a unique value, your kaizen instance's security depends on this being
@ -33,14 +32,3 @@ username = "postgres"
password = "password"
name = "postgres"
pool = 4
[publish]
dir = "/tmp/mcaptcha-survey"
duration = 3600
[footer]
about = "https://mcapthca.org/about"
donate = "https://mcapthca.org/donate"
thanks = "https://mcapthca.org/thanks"
privacy = "https://mcapthca.org/privacy"
security = "https://mcapthca.org/security"

View File

@ -1,103 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 16.0.4, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Ebene_1"
x="0px"
y="0px"
width="165.92125"
height="191.45087"
viewBox="0 0 165.92125 191.45086"
enable-background="new 0 0 198.425 198.425"
xml:space="preserve"
sodipodi:docname="NGIZero-green.svg"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"><metadata
id="metadata4142"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs4140" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1007"
id="namedview4138"
showgrid="false"
inkscape:zoom="1.6820179"
inkscape:cx="-191.39267"
inkscape:cy="54.855534"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="Ebene_1"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0" />
<polygon
points="36.911,63.104 36.911,66.116 36.911,132.309 36.911,135.321 39.346,136.825 96.715,169.921 99.273,171.419 101.853,169.921 159.319,136.825 161.938,135.321 161.938,132.309 161.938,66.116 161.938,63.104 159.308,61.6 101.841,28.504 99.234,27.006 96.629,28.504 39.347,61.6 "
id="polygon4013"
style="fill:#96c00a;fill-opacity:1"
transform="matrix(1.3249745,0,0,1.3249745,-48.642464,-35.674938)" />
<polygon
points="161.712,62.925 161.712,131.589 99.212,167.589 36.712,131.589 36.712,62.925 99.212,26.925 "
id="polygon4015"
style="fill:#97bf00;fill-opacity:0.91764706"
transform="matrix(1.3249745,0,0,1.3249745,-48.642464,-35.674938)" />
<polygon
stroke-miterlimit="10"
points="157.712,65.379 157.712,133.046 99.212,166.88 40.712,133.046 40.712,65.379 99.212,31.546 "
id="Outerline"
transform="matrix(1.3249745,0,0,1.3249745,-48.642464,-35.674938)"
style="fill:none;stroke:#ffffff;stroke-width:2;stroke-miterlimit:10"
inkscape:label="#outerline" />
<g
id="g4281"
transform="matrix(1.3249745,0,0,1.3249745,-47.067006,-23.859001)"><path
inkscape:connector-curvature="0"
id="path42"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.55783975"
d="m 133.45691,60.461638 v 0 c 2.27263,0 4.11462,1.841988 4.11462,4.114628 v 27.330241 c 0,2.27264 -1.84199,4.114628 -4.11462,4.114628 -2.27264,0 -4.11463,-1.841988 -4.11463,-4.114628 V 64.576266 c 0,-2.27264 1.84199,-4.114628 4.11463,-4.114628" /><g
transform="matrix(0.55783976,0,0,-0.55783976,120.13631,77.682765)"
id="g44"><path
inkscape:connector-curvature="0"
id="path46"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="M 0,0 H -0.506 C -0.57,0 -0.633,-0.008 -0.698,-0.01 -0.762,-0.008 -0.825,0 -0.89,0 h -7.283 c -3.929,0 -7.359,-2.965 -7.613,-6.885 -0.278,-4.296 3.124,-7.867 7.361,-7.867 0.776,0 1.343,-0.754 1.111,-1.494 -0.658,-2.088 -2.341,-3.751 -4.547,-4.333 -2.074,-0.547 -4.276,-0.821 -6.605,-0.821 -4.007,0 -7.574,0.865 -10.7,2.595 -3.127,1.73 -5.57,4.144 -7.331,7.24 -1.761,3.096 -2.641,6.617 -2.641,10.564 0,4.006 0.88,7.558 2.641,10.654 1.761,3.097 4.219,5.493 7.377,7.195 3.156,1.698 6.768,2.549 10.836,2.549 4.681,0 8.865,-1.269 12.55,-3.807 2.341,-1.612 5.524,-1.588 7.757,0.171 3.48,2.741 3.289,8.045 -0.315,10.452 -1.7,1.136 -3.538,2.112 -5.512,2.928 -4.553,1.881 -9.623,2.823 -15.208,2.823 -6.679,0 -12.69,-1.412 -18.03,-4.235 -5.344,-2.822 -9.517,-6.738 -12.522,-11.747 -3.005,-5.008 -4.508,-10.67 -4.508,-16.983 0,-6.315 1.503,-11.975 4.508,-16.984 3.005,-5.009 7.148,-8.924 12.43,-11.747 5.282,-2.824 11.231,-4.235 17.849,-4.235 4.613,0 9.197,0.699 13.751,2.095 0.045,0.014 0.091,0.028 0.136,0.042 7.104,2.202 11.884,8.86 11.884,16.297 v 9.047 C 6.486,-2.904 3.583,0 0,0" /></g><g
transform="matrix(0.55783976,0,0,-0.55783976,85.80763,64.525332)"
id="g48"><path
inkscape:connector-curvature="0"
id="path50"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 0,0 v -49.176 c 0,-4.023 -3.262,-7.285 -7.286,-7.285 h -1.381 c -2.181,0 -4.247,0.977 -5.631,2.662 l -24.229,29.505 c -1.804,2.197 -5.368,0.921 -5.368,-1.922 v -22.96 c 0,-4.023 -3.261,-7.285 -7.285,-7.285 -4.023,0 -7.285,3.262 -7.285,7.285 V 0 c 0,4.024 3.262,7.285 7.285,7.285 h 1.468 c 2.184,0 4.253,-0.979 5.636,-2.669 l 24.135,-29.475 c 1.802,-2.202 5.37,-0.927 5.37,1.918 V 0 c 0,4.024 3.261,7.285 7.285,7.285 C -3.262,7.285 0,4.024 0,0" /></g></g><g
aria-label="Z E R O"
transform="matrix(0.94681934,0,0,0.94681934,-209.97267,182.03385)"
style="font-variant:normal;font-weight:600;font-stretch:normal;font-size:31.76000023px;font-family:'Montserrat SemiBold';-inkscape-font-specification:Montserrat-SemiBold;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:0.7171717;fill-rule:nonzero;stroke:none"
id="text56"><path
inkscape:connector-curvature="0"
d="m 243.58117,-73.015206 h 19.46231 v 3.613321 l -12.42176,15.02707 h 12.77844 v 4.512774 h -20.17567 v -3.613321 l 12.42176,-15.02707 h -12.06508 z"
id="path2325" /><path
inkscape:connector-curvature="0"
d="m 278.7684,-73.015206 h 16.11262 v 4.512774 h -10.14211 v 4.311172 h 9.5373 v 4.512773 h -9.5373 v 5.303672 h 10.48328 v 4.512774 H 278.7684 Z"
id="path2327" /><path
inkscape:connector-curvature="0"
d="m 320.00367,-62.749034 q 1.87645,0 2.68285,-0.697851 0.82192,-0.697852 0.82192,-2.295157 0,-1.581796 -0.82192,-2.26414 -0.8064,-0.682344 -2.68285,-0.682344 h -2.51226 v 5.939492 z m -2.51226,4.125078 v 8.761915 h -5.97051 v -23.153165 h 9.11859 q 4.57481,0 6.69938,1.535274 2.14008,1.535273 2.14008,4.853945 0,2.295156 -1.11657,3.768399 -1.10105,1.473242 -3.33418,2.171093 1.22512,0.279141 2.18661,1.271641 0.97699,0.976992 1.96949,2.9775 l 3.24113,6.575313 h -6.3582 l -2.82242,-5.753399 q -0.85293,-1.736875 -1.73688,-2.372695 -0.86844,-0.635821 -2.32617,-0.635821 z"
id="path2329" /><path
inkscape:connector-curvature="0"
d="m 357.57911,-69.107237 q -2.72938,0 -4.23364,2.016016 -1.50425,2.016015 -1.50425,5.675859 0,3.644336 1.50425,5.660352 1.50426,2.016015 4.23364,2.016015 2.74488,0 4.24914,-2.016015 1.50426,-2.016016 1.50426,-5.660352 0,-3.659844 -1.50426,-5.675859 -1.50426,-2.016016 -4.24914,-2.016016 z m 0,-4.32668 q 5.58281,0 8.7464,3.19461 3.1636,3.194609 3.1636,8.823945 0,5.613828 -3.1636,8.808438 -3.16359,3.194609 -8.7464,3.194609 -5.56731,0 -8.74641,-3.194609 -3.16359,-3.19461 -3.16359,-8.808438 0,-5.629336 3.16359,-8.823945 3.1791,-3.19461 8.74641,-3.19461 z"
id="path2331" /></g></svg>

Before

Width:  |  Height:  |  Size: 7.3 KiB

View File

@ -1,7 +1,3 @@
-- SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
CREATE TABLE IF NOT EXISTS survey_users (
ID UUID PRIMARY KEY NOT NULL UNIQUE,
created_at TIMESTAMPTZ NOT NULL

View File

@ -1,7 +1,3 @@
-- SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
-- Add migration script here
CREATE TABLE IF NOT EXISTS survey_admins (
name VARCHAR(100) NOT NULL UNIQUE,

View File

@ -1,6 +0,0 @@
-- SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
ALTER TABLE survey_responses
ADD COLUMN submitted_at TIMESTAMPTZ NOT NULL DEFAULT now();

View File

@ -1,22 +0,0 @@
-- SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
CREATE TABLE IF NOT EXISTS survey_bench_type (
name VARCHAR(30) UNIQUE NOT NULL,
ID SERIAL PRIMARY KEY NOT NULL
);
INSERT INTO survey_bench_type (name) VALUES ('wasm');
INSERT INTO survey_bench_type (name) VALUES ('js');
CREATE OR REPLACE FUNCTION id_in_survey_bench_type(iname varchar)
RETURNS int LANGUAGE SQL AS $$
SELECT ID FROM survey_bench_type WHERE name = name;
$$;
ALTER TABLE survey_responses
ADD COLUMN submission_bench_type_id INTEGER references survey_bench_type(ID) NOT NULL
DEFAULT id_in_survey_bench_type('wasm');

View File

@ -1,42 +0,0 @@
-- SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
CREATE TABLE IF NOT EXISTS survey_mcaptcha_hostname (
url VARCHAR(3000) UNIQUE NOT NULL,
secret VARCHAR(100) UNIQUE NOT NULL,
ID SERIAL PRIMARY KEY NOT NULL
);
CREATE TABLE IF NOT EXISTS survey_mcaptcha_campaign (
campaign_id VARCHAR(100) NOT NULL,
public_id VARCHAR(100) NOT NULL,
url_id INTEGER NOT NULL references survey_mcaptcha_hostname(ID) ON DELETE CASCADE,
synced_till INTEGER NOT NULL DEFAULT 0,
ID SERIAL PRIMARY KEY NOT NULL
);
CREATE TABLE IF NOT EXISTS survey_mcaptcha_analytics (
campaign_id INTEGER references survey_mcaptcha_campaign(ID) ON DELETE CASCADE,
time INTEGER NOT NULL,
difficulty_factor INTEGER NOT NULL,
worker_type VARCHAR(100) NOT NULL,
ID SERIAL PRIMARY KEY NOT NULL
);
CREATE TABLE IF NOT EXISTS survey_mcaptcha_upload_job_states (
name VARCHAR(20) NOT NULL UNIQUE,
ID SERIAL PRIMARY KEY NOT NULL
);
CREATE TABLE IF NOT EXISTS survey_mcaptcha_upload_jobs (
campaign_id INTEGER references survey_mcaptcha_campaign(ID) ON DELETE CASCADE,
public_id varchar(100) NOT NULL UNIQUE,
created_at timestamptz NOT NULL DEFAULT now(),
scheduled_at timestamptz DEFAULT NULL,
finished_at timestamptz DEFAULT NULL,
job_state INTEGER references survey_mcaptcha_upload_job_states(ID) ON DELETE CASCADE,
ID SERIAL PRIMARY KEY NOT NULL
);

17771
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -16,7 +16,7 @@
"@types/sinon": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"@wasm-tool/wasm-pack-plugin": "^1.6.0",
"@wasm-tool/wasm-pack-plugin": "^1.4.0",
"dart-sass": "^1.25.0",
"eslint": "^8.0.1",
"jest": "^27.2.5",
@ -32,8 +32,7 @@
"webpack-dev-server": "^4.3.1"
},
"dependencies": {
"@mcaptcha/vanilla-glue": "^0.1.0-alpha-3",
"@mcaptcha/pow_sha256-polyfill": "^0.1.0-alpha-1",
"@mcaptcha/pow-wasm": "^0.1.0-alpha-1"
"@mcaptcha/vanilla-glue": "^0.1.0-alpha-1",
"mcaptcha-browser": "./vendor/pow/"
}
}

View File

@ -1,3 +1,376 @@
{
"db": "PostgreSQL"
"db": "PostgreSQL",
"03c9789e83a398bed96354924a0e63ccaa97bec667fda1b8277bb9afda9a6fcd": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Text",
"Uuid"
]
}
},
"query": "DELETE \n FROM survey_campaigns \n WHERE \n user_id = (\n SELECT \n ID \n FROM \n survey_admins \n WHERE \n name = $1\n )\n AND\n id = ($2)"
},
"0d22134cc5076304b7895827f006ee8269cc500f400114a7472b83f0f1c568b5": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Text",
"Varchar"
]
}
},
"query": "INSERT INTO survey_admins \n (name , password, secret) VALUES ($1, $2, $3)"
},
"1373df097fa0e58b23a374753318ae53a44559aa0e7eb64680185baf1c481723": {
"describe": {
"columns": [
{
"name": "password",
"ordinal": 0,
"type_info": "Text"
}
],
"nullable": [
false
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT password FROM survey_admins WHERE name = ($1)"
},
"19686bfe8772cbc6831d46d18994e2b9aa40c7181eae9a31e51451cce95f04e8": {
"describe": {
"columns": [
{
"name": "name",
"ordinal": 0,
"type_info": "Varchar"
},
{
"name": "password",
"ordinal": 1,
"type_info": "Text"
}
],
"nullable": [
false,
false
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT name, password FROM survey_admins WHERE email = ($1)"
},
"1b7e17bfc949fa97e8dec1f95e35a02bcf3aa1aa72a1f6f6c8884e885fc3b953": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Text",
"Varchar",
"Varchar"
]
}
},
"query": "insert into survey_admins \n (name , password, email, secret) values ($1, $2, $3, $4)"
},
"2ccaecfee4d2f29ef5278188b304017719720aa986d680d4727a1facbb869c7a": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "DELETE FROM survey_admins WHERE name = ($1)"
},
"43b3e771f38bf8059832169227705be06a28925af1b3799ffef5371d511fd138": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Timestamptz",
"Uuid"
]
}
},
"query": "\n INSERT INTO survey_users (created_at, id) VALUES($1, $2)"
},
"536541ecf2e1c0403c74b6e2e09b42b73a7741ae4a348ff539ac410022e03ace": {
"describe": {
"columns": [
{
"name": "exists",
"ordinal": 0,
"type_info": "Bool"
}
],
"nullable": [
null
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT EXISTS (SELECT 1 from survey_admins WHERE name = $1)"
},
"55dde28998a6d12744806035f0a648494a403c7d09ea3caf91bf54869a81aa73": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Text",
"Text"
]
}
},
"query": "UPDATE survey_admins set password = $1\n WHERE name = $2"
},
"58ec3b8f98c27e13ec2732f8ee23f6eb9845ac5d9fd97b1e5c9f2eed4b1f5693": {
"describe": {
"columns": [
{
"name": "name",
"ordinal": 0,
"type_info": "Varchar"
}
],
"nullable": [
false
],
"parameters": {
"Left": [
"Uuid",
"Text"
]
}
},
"query": "SELECT name \n FROM survey_campaigns\n WHERE \n id = $1\n AND\n user_id = (SELECT ID from survey_admins WHERE name = $2)"
},
"683707dbc847b37c58c29aaad0d1a978c9fe0657da13af99796e4461134b5a43": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Text"
]
}
},
"query": "UPDATE survey_admins set email = $1\n WHERE name = $2"
},
"6a26daa84578aed2b2085697cb8358ed7c0a50ba9597fd387b4b09b0a8a154db": {
"describe": {
"columns": [
{
"name": "exists",
"ordinal": 0,
"type_info": "Bool"
}
],
"nullable": [
null
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT EXISTS (SELECT 1 from survey_admins WHERE email = $1)"
},
"70cc7bfc9b6ff5b68db70c069c0947d51bfc4a53cedc020016ee25ff98586c93": {
"describe": {
"columns": [
{
"name": "name",
"ordinal": 0,
"type_info": "Varchar"
},
{
"name": "id",
"ordinal": 1,
"type_info": "Uuid"
}
],
"nullable": [
false,
false
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT \n name, id\n FROM \n survey_campaigns \n WHERE\n user_id = (\n SELECT \n ID\n FROM \n survey_admins\n WHERE\n name = $1\n )"
},
"82feafc36533144e49ba374c8c47ca4aa0d6558a9803778ad28cfa7b62382c3e": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Text",
"Uuid",
"Varchar",
"Int4Array",
"Timestamptz"
]
}
},
"query": "\n INSERT INTO survey_campaigns (\n user_id, ID, name, difficulties, created_at\n ) VALUES(\n (SELECT id FROM survey_admins WHERE name = $1),\n $2, $3, $4, $5\n );"
},
"8320dda2b3e107d1451fdfb35eb2a4b8e97364e7b1b74ffe4d6913faf132fb61": {
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Int4"
}
],
"nullable": [
false
],
"parameters": {
"Left": [
"Uuid",
"Text"
]
}
},
"query": "SELECT ID \n FROM survey_responses \n WHERE \n user_id = $1 \n AND \n device_software_recognised = $2;"
},
"9cdade613ce724631cc3f187510758ee0929e93ff3f8ce81fe35594756644246": {
"describe": {
"columns": [
{
"name": "difficulties",
"ordinal": 0,
"type_info": "Int4Array"
}
],
"nullable": [
false
],
"parameters": {
"Left": [
"Uuid"
]
}
},
"query": "SELECT difficulties FROM survey_campaigns WHERE id = $1;"
},
"a721cfa249acf328c2f29c4cf8c2aeba1a635bcf49d18ced5474caa10b7cae4f": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Int4",
"Int4",
"Float4"
]
}
},
"query": "INSERT INTO survey_benches \n (resp_id, difficulty, duration) \n VALUES ($1, $2, $3);"
},
"ab951c5c318174c6538037947c2f52c61bcfe5e5be1901379b715e77f5214dd2": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Text"
]
}
},
"query": "UPDATE survey_admins set secret = $1\n WHERE name = $2"
},
"b4cd1e5240de1968c8b6d56672cec639b22f41ebf2754dadbf00efe0948c7e68": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Uuid",
"Uuid",
"Varchar",
"Varchar",
"Int4"
]
}
},
"query": "INSERT INTO survey_responses (\n user_id, \n campaign_id,\n device_user_provided,\n device_software_recognised,\n threads\n ) VALUES ($1, $2, $3, $4, $5);"
},
"c757589ef26a005e3285e7ab20d8a44c4f2e1cb125f8db061dd198cc380bf807": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Text"
]
}
},
"query": "UPDATE survey_admins set name = $1\n WHERE name = $2"
},
"e9cf5d6d8c9e8327d5c809d47a14a933f324e267f1e7dbb48e1caf1c021adc3f": {
"describe": {
"columns": [
{
"name": "secret",
"ordinal": 0,
"type_info": "Varchar"
}
],
"nullable": [
false
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT secret FROM survey_admins WHERE name = ($1)"
},
"fcdc5fe5d496eb516c805e64ec96d9626b74ab33cd6e75e5a08ae88967403b72": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Int4",
"Uuid",
"Uuid"
]
}
},
"query": "INSERT INTO survey_response_tokens \n (resp_id, user_id, id)\n VALUES ($1, $2, $3);"
}
}

View File

@ -1,6 +1,18 @@
// Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
// SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/*
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
pub mod v1;

View File

@ -1,7 +1,19 @@
// Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
// SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/*
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use actix_identity::Identity;
use actix_web::{web, HttpResponse, Responder};
@ -10,7 +22,7 @@ use super::auth::runners::Password;
use crate::errors::*;
use crate::AppData;
#[actix_web_codegen_const_routes::post(
#[my_codegen::post(
path = "crate::V1_API_ROUTES.admin.account.delete",
wrap = "crate::api::v1::admin::get_admin_check_login()"
)]

View File

@ -1,8 +1,19 @@
// Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
// SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/*
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use std::borrow::Cow;
use actix_identity::Identity;
@ -18,9 +29,7 @@ pub struct Email {
pub email: String,
}
#[actix_web_codegen_const_routes::post(
path = "crate::V1_API_ROUTES.admin.account.email_exists"
)]
#[my_codegen::post(path = "crate::V1_API_ROUTES.admin.account.email_exists")]
pub async fn email_exists(
payload: web::Json<AccountCheckPayload>,
data: AppData,
@ -44,7 +53,7 @@ pub async fn email_exists(
}
/// update email
#[actix_web_codegen_const_routes::post(
#[my_codegen::post(
path = "crate::V1_API_ROUTES.admin.account.update_email",
wrap = "crate::api::v1::admin::get_admin_check_login()"
)]

View File

@ -1,7 +1,19 @@
// Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
// SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/*
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use serde::{Deserialize, Serialize};
@ -16,9 +28,7 @@ pub mod username;
pub use super::auth;
pub mod routes {
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub struct Account {
pub delete: &'static str,
pub email_exists: &'static str,

View File

@ -1,8 +1,19 @@
// Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
// SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/*
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use actix_identity::Identity;
use actix_web::{web, HttpResponse, Responder};
use argon2_creds::Config;
@ -57,7 +68,7 @@ async fn update_password_runner(
Ok(())
}
#[actix_web_codegen_const_routes::post(
#[my_codegen::post(
path = "crate::V1_API_ROUTES.admin.account.update_password",
wrap = "crate::api::v1::admin::get_admin_check_login()"
)]
@ -107,6 +118,7 @@ mod tests {
use actix_web::test;
use crate::api::v1::ROUTES;
use crate::data::Data;
use crate::tests::*;
#[actix_rt::test]
@ -115,12 +127,12 @@ mod tests {
const PASSWORD: &str = "longpassword2";
const EMAIL: &str = "updatepassuser@a.com";
let data = get_test_data().await;
{
let data = Data::new().await;
delete_user(NAME, &data).await;
}
let (_, signin_resp) = register_and_signin(&data, NAME, EMAIL, PASSWORD).await;
let (data, _, signin_resp) = register_and_signin(NAME, EMAIL, PASSWORD).await;
let cookies = get_cookie!(signin_resp);
let app = get_app!(data).await;
@ -153,7 +165,6 @@ mod tests {
};
bad_post_req_test(
&data,
NAME,
new_password,
ROUTES.admin.account.update_password,
@ -169,7 +180,6 @@ mod tests {
};
bad_post_req_test(
&data,
NAME,
new_password,
ROUTES.admin.account.update_password,

View File

@ -1,8 +1,19 @@
// Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
// SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/*
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use std::borrow::Cow;
use actix_identity::Identity;
@ -18,7 +29,7 @@ pub struct Secret {
pub secret: String,
}
#[actix_web_codegen_const_routes::get(
#[my_codegen::get(
path = "crate::V1_API_ROUTES.admin.account.get_secret",
wrap = "crate::api::v1::admin::get_admin_check_login()"
)]
@ -36,7 +47,7 @@ async fn get_secret(id: Identity, data: AppData) -> ServiceResult<impl Responder
Ok(HttpResponse::Ok().json(secret))
}
#[actix_web_codegen_const_routes::post(
#[my_codegen::post(
path = "crate::V1_API_ROUTES.admin.account.update_secret",
wrap = "crate::api::v1::admin::get_admin_check_login()"
)]

View File

@ -1,7 +1,19 @@
// Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
// SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/*
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use actix_web::http::StatusCode;
use actix_web::test;
@ -11,6 +23,7 @@ use super::username::Username;
use super::*;
use crate::api::v1::admin::auth::runners::Password;
use crate::api::v1::ROUTES;
use crate::data::Data;
use crate::*;
use crate::errors::*;
@ -22,12 +35,12 @@ async fn uname_email_exists_works() {
const PASSWORD: &str = "longpassword2";
const EMAIL: &str = "testuserexists@a.com2";
let data = get_test_data().await;
{
let data = Data::new().await;
delete_user(NAME, &data).await;
}
let (_, signin_resp) = register_and_signin(&data, NAME, EMAIL, PASSWORD).await;
let (data, _, signin_resp) = register_and_signin(NAME, EMAIL, PASSWORD).await;
let cookies = get_cookie!(signin_resp);
let app = get_app!(data).await;
@ -112,14 +125,14 @@ async fn email_udpate_password_validation_del_userworks() {
const NAME2: &str = "eupdauser";
const EMAIL2: &str = "eupdauser@a.com";
let data = get_test_data().await;
{
let data = Data::new().await;
delete_user(NAME, &data).await;
delete_user(NAME2, &data).await;
}
let _ = register_and_signin(&data, NAME2, EMAIL2, PASSWORD).await;
let (_creds, signin_resp) = register_and_signin(&data, NAME, EMAIL, PASSWORD).await;
let _ = register_and_signin(NAME2, EMAIL2, PASSWORD).await;
let (data, _creds, signin_resp) = register_and_signin(NAME, EMAIL, PASSWORD).await;
let cookies = get_cookie!(signin_resp);
let app = get_app!(data).await;
@ -140,7 +153,6 @@ async fn email_udpate_password_validation_del_userworks() {
// check duplicate email while duplicate email
email_payload.email = EMAIL2.into();
bad_post_req_test(
&data,
NAME,
PASSWORD,
ROUTES.admin.account.update_email,
@ -154,7 +166,6 @@ async fn email_udpate_password_validation_del_userworks() {
password: NAME.into(),
};
bad_post_req_test(
&data,
NAME,
PASSWORD,
ROUTES.admin.account.delete,
@ -197,8 +208,9 @@ async fn username_update_works() {
const NAME2: &str = "terstusrtds";
const NAME_CHANGE: &str = "terstusrtdsxx";
let data = get_test_data().await;
{
let data = Data::new().await;
futures::join!(
delete_user(NAME, &data),
delete_user(NAME2, &data),
@ -206,8 +218,8 @@ async fn username_update_works() {
);
}
let _ = register_and_signin(&data, NAME2, EMAIL2, PASSWORD).await;
let (_creds, signin_resp) = register_and_signin(&data, NAME, EMAIL, PASSWORD).await;
let _ = register_and_signin(NAME2, EMAIL2, PASSWORD).await;
let (data, _creds, signin_resp) = register_and_signin(NAME, EMAIL, PASSWORD).await;
let cookies = get_cookie!(signin_resp);
let app = get_app!(data).await;
@ -227,7 +239,6 @@ async fn username_update_works() {
// check duplicate username with duplicate username
username_udpate.username = NAME2.into();
bad_post_req_test(
&data,
NAME_CHANGE,
PASSWORD,
ROUTES.admin.account.update_username,

View File

@ -1,8 +1,19 @@
// Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
// SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/*
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use std::borrow::Cow;
use actix_identity::Identity;
@ -13,9 +24,7 @@ use super::{AccountCheckPayload, AccountCheckResp};
use crate::errors::*;
use crate::AppData;
#[actix_web_codegen_const_routes::post(
path = "crate::V1_API_ROUTES.admin.account.username_exists"
)]
#[my_codegen::post(path = "crate::V1_API_ROUTES.admin.account.username_exists")]
async fn username_exists(
payload: web::Json<AccountCheckPayload>,
data: AppData,
@ -56,7 +65,7 @@ pub struct Username {
}
/// update username
#[actix_web_codegen_const_routes::post(
#[my_codegen::post(
path = "crate::V1_API_ROUTES.admin.account.update_username",
wrap = "crate::api::v1::admin::get_admin_check_login()"
)]

View File

@ -1,7 +1,19 @@
// Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
// SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/*
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use actix_identity::Identity;
use actix_web::http::header;
@ -14,9 +26,7 @@ use crate::AppData;
pub mod routes {
use actix_auth_middleware::GetLoginRoute;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub struct Auth {
pub logout: &'static str,
pub login: &'static str,
@ -143,7 +153,7 @@ pub mod runners {
payload: &Register,
data: &AppData,
) -> ServiceResult<()> {
if !data.settings.allow_registration {
if !crate::SETTINGS.allow_registration {
return Err(ServiceError::ClosedForRegistration);
}
@ -212,9 +222,7 @@ pub fn services(cfg: &mut web::ServiceConfig) {
cfg.service(login);
cfg.service(signout);
}
#[actix_web_codegen_const_routes::post(
path = "crate::V1_API_ROUTES.admin.auth.register"
)]
#[my_codegen::post(path = "crate::V1_API_ROUTES.admin.auth.register")]
async fn register(
payload: web::Json<runners::Register>,
data: AppData,
@ -223,7 +231,7 @@ async fn register(
Ok(HttpResponse::Ok())
}
#[actix_web_codegen_const_routes::post(path = "crate::V1_API_ROUTES.admin.auth.login")]
#[my_codegen::post(path = "crate::V1_API_ROUTES.admin.auth.login")]
async fn login(
id: Identity,
payload: web::Json<runners::Login>,
@ -242,7 +250,7 @@ async fn login(
Ok(HttpResponse::Ok().into())
}
}
#[actix_web_codegen_const_routes::get(
#[my_codegen::get(
path = "crate::V1_API_ROUTES.admin.auth.logout",
wrap = "crate::api::v1::admin::get_admin_check_login()"
)]

View File

@ -1,34 +1,37 @@
// Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
// SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/*
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use std::borrow::Cow;
use actix_identity::Identity;
use actix_web::{web, HttpResponse, Responder};
use serde::{Deserialize, Serialize};
use sqlx::types::time::OffsetDateTime;
use sqlx::types::Uuid;
use uuid::Uuid;
use super::{get_admin_check_login, get_uuid};
use crate::api::v1::bench::Bench;
use crate::api::v1::bench::SubmissionType;
use crate::errors::*;
use crate::AppData;
pub mod routes {
use serde::{Deserialize, Serialize};
use super::ResultsPage;
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub struct Campaign {
pub add: &'static str,
pub delete: &'static str,
// pub get_feedback: &'static str,
pub list: &'static str,
pub results: &'static str,
}
impl Campaign {
@ -37,14 +40,8 @@ pub mod routes {
let delete = "/admin/api/v1/campaign/{uuid}/delete";
// let get_feedback = "/api/v1/campaign/{uuid}/feedback";
let list = "/admin/api/v1/campaign/list";
let results = "/admin/api/v1/campaign/{uuid}/results";
Campaign {
add,
delete,
list,
results,
}
Campaign { add, delete, list }
}
// pub fn get_benches_route(&self, campaign_id: &str) -> String {
// self.get_feedback.replace("{uuid}", &campaign_id)
@ -53,45 +50,17 @@ pub mod routes {
pub fn get_delete_route(&self, campaign_id: &str) -> String {
self.delete.replace("{uuid}", campaign_id)
}
pub fn get_results_route(
&self,
campaign_id: &str,
modifier: Option<ResultsPage>,
) -> String {
let mut res = self.results.replace("{uuid}", campaign_id);
if let Some(modifier) = modifier {
if let Some(page) = modifier.page {
res = format!("{res}?page={page}");
}
if let Some(bench_type) = modifier.bench_type {
if modifier.page.is_some() {
res = format!("{res}&bench_type={}", bench_type.to_string());
} else {
res = format!("{res}?bench_type={}", bench_type.to_string());
}
}
}
res
}
}
}
pub mod runners {
use std::str::FromStr;
use futures::try_join;
use crate::api::v1::bench::Bench;
use super::*;
pub async fn add_runner(
username: &str,
payload: &mut AddCapmaign,
data: &AppData,
) -> ServiceResult<sqlx::types::Uuid> {
) -> ServiceResult<uuid::Uuid> {
let mut uuid;
let now = OffsetDateTime::now_utc();
@ -132,32 +101,6 @@ pub mod runners {
Ok(uuid)
}
pub async fn list_all_campaigns(
data: &AppData,
) -> ServiceResult<Vec<ListCampaignResp>> {
struct ListCampaign {
name: String,
id: Uuid,
}
let mut campaigns = sqlx::query_as!(
ListCampaign,
"SELECT name, id FROM survey_campaigns ORDER BY id;"
)
.fetch_all(&data.db)
.await?;
let mut list_resp = Vec::with_capacity(campaigns.len());
campaigns.drain(0..).for_each(|c| {
list_resp.push(ListCampaignResp {
name: c.name,
uuid: c.id.to_string(),
});
});
Ok(list_resp)
}
pub async fn list_campaign_runner(
username: &str,
data: &AppData,
@ -198,176 +141,86 @@ pub mod runners {
Ok(list_resp)
}
#[derive(Debug)]
struct InternalSurveyResp {
id: i32,
submitted_at: OffsetDateTime,
user_id: Uuid,
threads: Option<i32>,
device_user_provided: String,
device_software_recognised: String,
name: String,
}
#[derive(Debug)]
struct InnerU {
created_at: OffsetDateTime,
id: Uuid,
}
impl From<InnerU> for SurveyUser {
fn from(u: InnerU) -> Self {
Self {
id: uuid::Uuid::parse_str(&u.id.to_string()).unwrap(),
created_at: u.created_at.unix_timestamp(),
}
}
}
pub async fn get_results(
username: &str,
uuid: &Uuid,
data: &AppData,
page: usize,
limit: usize,
filter: Option<SubmissionType>,
) -> ServiceResult<Vec<SurveyResponse>> {
let mut db_responses = if let Some(filter) = filter {
sqlx::query_as!(
InternalSurveyResp,
"SELECT
survey_responses.ID,
survey_responses.device_software_recognised,
survey_responses.threads,
survey_responses.user_id,
survey_responses.submitted_at,
survey_responses.device_user_provided,
survey_bench_type.name
FROM
survey_responses
INNER JOIN survey_bench_type ON
survey_responses.submission_bench_type_id = survey_bench_type.ID
WHERE
survey_bench_type.name = $3
AND
survey_responses.campaign_id = (
SELECT ID FROM survey_campaigns
WHERE
ID = $1
AND
user_id = (SELECT ID FROM survey_admins WHERE name = $2)
)
LIMIT $4 OFFSET $5",
uuid,
username,
filter.to_string(),
limit as i32,
page as i32,
)
.fetch_all(&data.db)
.await?
} else {
#[derive(Debug)]
struct I {
id: i32,
submitted_at: OffsetDateTime,
user_id: Uuid,
threads: Option<i32>,
device_user_provided: String,
device_software_recognised: String,
name: String,
}
let mut i = sqlx::query_as!(
I,
"SELECT
survey_responses.ID,
survey_responses.device_software_recognised,
survey_responses.threads,
survey_responses.user_id,
survey_responses.submitted_at,
survey_responses.device_user_provided,
survey_bench_type.name
FROM
survey_responses
INNER JOIN survey_bench_type ON
survey_responses.submission_bench_type_id = survey_bench_type.ID
WHERE
survey_responses.campaign_id = (
SELECT ID FROM survey_campaigns
WHERE
ID = $1
AND
user_id = (SELECT ID FROM survey_admins WHERE name = $2)
)
LIMIT $3 OFFSET $4",
uuid,
username,
limit as i32,
page as i32,
)
.fetch_all(&data.db)
.await?;
let mut res = Vec::with_capacity(i.len());
i.drain(0..).for_each(|x| {
res.push(InternalSurveyResp {
id: x.id,
submitted_at: x.submitted_at,
user_id: x.user_id,
threads: x.threads,
device_user_provided: x.device_user_provided,
device_software_recognised: x.device_software_recognised,
name: x.name,
})
});
res
};
let mut responses = Vec::with_capacity(db_responses.len());
for r in db_responses.drain(0..) {
let benches_fut = sqlx::query_as!(
Bench,
"SELECT
duration,
difficulty
FROM
survey_benches
WHERE
resp_id = $1
",
r.id,
)
.fetch_all(&data.db);
let user_fut = sqlx::query_as!(
InnerU,
"SELECT
created_at,
ID
FROM
survey_users
WHERE
ID = $1
",
r.user_id,
)
.fetch_one(&data.db);
let (benches, user) = try_join!(benches_fut, user_fut)?;
let user = user.into();
responses.push(SurveyResponse {
benches,
user,
device_user_provided: r.device_user_provided,
device_software_recognised: r.device_software_recognised,
submitted_at: r.submitted_at.unix_timestamp(),
id: r.id as usize,
submission_type: SubmissionType::from_str(&r.name).unwrap(),
threads: r.threads.map(|t| t as usize),
})
}
Ok(responses)
}
// pub async fn get_benches(
// username: &str,
// uuid: &str,
// data: &AppData,
// ) -> ServiceResult<GetFeedbackResp> {
// let uuid = Uuid::parse_str(uuid).map_err(|_| ServiceError::NotAnId)?;
//
// struct FeedbackInternal {
// time: OffsetDateTime,
// description: String,
// helpful: bool,
// }
//
// struct Name {
// name: String,
// }
//
// let name_fut = sqlx::query_as!(
// Name,
// "SELECT name
// FROM survey_campaigns
// WHERE uuid = $1
// AND
// user_id = (
// SELECT
// ID
// FROM
// kaizen_users
// WHERE
// name = $2
// )
// ",
// uuid,
// username
// )
// .fetch_one(&data.db); //.await?;
//
// let feedback_fut = sqlx::query_as!(
// FeedbackInternal,
// "SELECT
// time, description, helpful
// FROM
// kaizen_feedbacks
// WHERE campaign_id = (
// SELECT uuid
// FROM
// survey_campaigns
// WHERE
// uuid = $1
// AND
// user_id = (
// SELECT
// ID
// FROM
// kaizen_users
// WHERE
// name = $2
// )
// )",
// uuid,
// username
// )
// .fetch_all(&data.db);
// let (name, mut feedbacks) = try_join!(name_fut, feedback_fut)?;
// //.await?;
//
// let mut feedback_resp = Vec::with_capacity(feedbacks.len());
// feedbacks.drain(0..).for_each(|f| {
// feedback_resp.push(Feedback {
// time: f.time.unix_timestamp() as u64,
// description: f.description,
// helpful: f.helpful,
// });
// });
//
// Ok(GetFeedbackResp {
// feedbacks: feedback_resp,
// name: name.name,
// })
// }
pub async fn delete(
uuid: &Uuid,
@ -380,11 +233,11 @@ pub mod runners {
WHERE
user_id = (
SELECT
ID
ID
FROM
survey_admins
survey_admins
WHERE
name = $1
name = $1
)
AND
id = ($2)",
@ -397,7 +250,7 @@ pub mod runners {
}
}
#[actix_web_codegen_const_routes::post(
#[my_codegen::post(
path = "crate::V1_API_ROUTES.admin.campaign.delete",
wrap = "get_admin_check_login()"
)]
@ -413,31 +266,41 @@ pub async fn delete(
Ok(HttpResponse::Ok())
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SurveyResponse {
pub user: SurveyUser,
pub device_user_provided: String,
pub device_software_recognised: String,
pub id: usize,
pub threads: Option<usize>,
pub submitted_at: i64,
pub submission_type: SubmissionType,
pub benches: Vec<Bench>,
}
//#[derive(Serialize, Deserialize)]
//pub struct Feedback {
// pub time: u64,
// pub description: String,
// pub helpful: bool,
//}
//
//#[derive(Serialize, Deserialize)]
//pub struct GetFeedbackResp {
// pub name: String,
// pub feedbacks: Vec<Feedback>,
//}
//
//#[my_codegen::post(
// path = "crate::V1_API_ROUTES.campaign.get_feedback",
// wrap = "crate::CheckLogin"
//)]
//pub async fn get_feedback(
// id: Identity,
// data: AppData,
// path: web::Path<String>,
//) -> ServiceResult<impl Responder> {
// let username = id.identity().unwrap();
// let path = path.into_inner();
// let feedback_resp = runners::get_feedback(&username, &path, &data).await?;
// Ok(HttpResponse::Ok().json(feedback_resp))
//}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SurveyUser {
pub created_at: i64, // OffsetDateTime,
pub id: uuid::Uuid,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Serialize, Deserialize)]
pub struct ListCampaignResp {
pub name: String,
pub uuid: String,
}
#[actix_web_codegen_const_routes::post(
#[my_codegen::post(
path = "crate::V1_API_ROUTES.admin.campaign.list",
wrap = "get_admin_check_login()"
)]
@ -451,13 +314,13 @@ pub async fn list_campaign(
Ok(HttpResponse::Ok().json(list_resp))
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Serialize, Deserialize)]
pub struct AddCapmaign {
pub name: String,
pub difficulties: Vec<i32>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Serialize, Deserialize)]
pub struct AddCapmaignResp {
pub campaign_id: String,
}
@ -466,48 +329,10 @@ pub fn services(cfg: &mut web::ServiceConfig) {
cfg.service(add);
cfg.service(delete);
cfg.service(list_campaign);
cfg.service(get_campaign_resutls);
//cfg.service(get_feedback);
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub struct ResultsPage {
page: Option<usize>,
pub bench_type: Option<SubmissionType>,
}
impl ResultsPage {
pub fn page(&self) -> usize {
self.page.unwrap_or(0)
}
pub fn new(page: Option<usize>, bench_type: Option<SubmissionType>) -> Self {
Self { page, bench_type }
}
}
#[actix_web_codegen_const_routes::get(
path = "crate::V1_API_ROUTES.admin.campaign.results",
wrap = "get_admin_check_login()"
)]
pub async fn get_campaign_resutls(
id: Identity,
query: web::Query<ResultsPage>,
path: web::Path<uuid::Uuid>,
data: AppData,
) -> ServiceResult<impl Responder> {
let username = id.identity().unwrap();
let query = query.into_inner();
let page = query.page();
let path = Uuid::parse_str(&path.to_string()).unwrap();
let results =
runners::get_results(&username, &path, &data, page, 50, query.bench_type)
.await?;
Ok(HttpResponse::Ok().json(results))
}
#[actix_web_codegen_const_routes::post(path = "crate::V1_API_ROUTES.admin.campaign.add")]
#[my_codegen::post(path = "crate::V1_API_ROUTES.admin.campaign.add")]
async fn add(
payload: web::Json<AddCapmaign>,
data: AppData,
@ -525,7 +350,7 @@ async fn add(
#[cfg(test)]
mod tests {
use crate::api::v1::bench::Submission;
use crate::api::v1::bench::SubmissionType;
use crate::data::Data;
use crate::errors::*;
use crate::tests::*;
use crate::*;
@ -535,7 +360,7 @@ mod tests {
#[actix_rt::test]
async fn test_bench_register_works() {
let data = get_test_data().await;
let data = Data::new().await;
let app = get_app!(data).await;
let signin_resp = test::call_service(
&app,
@ -574,17 +399,16 @@ mod tests {
const DEVICE_SOFTWARE_RECOGNISED: &str = "Foobar.v2";
const THREADS: i32 = 4;
let data = get_test_data().await;
{
let data = Data::new().await;
delete_user(NAME, &data).await;
}
let (_creds, signin_resp) =
register_and_signin(&data, NAME, EMAIL, PASSWORD).await;
let (data, _creds, signin_resp) =
register_and_signin(NAME, EMAIL, PASSWORD).await;
let cookies = get_cookie!(signin_resp);
let survey = get_survey_user(data.clone()).await;
let survey_cookie = get_cookie!(survey);
let app = get_app!(data).await;
let campaign = create_new_campaign(NAME, data.clone(), cookies.clone()).await;
let campaign_config =
@ -597,7 +421,6 @@ mod tests {
device_software_recognised: DEVICE_SOFTWARE_RECOGNISED.into(),
threads: THREADS,
benches: BENCHES.clone(),
submission_type: SubmissionType::Wasm,
};
let _proof =
@ -606,72 +429,7 @@ mod tests {
let list = list_campaings(data.clone(), cookies.clone()).await;
assert!(list.iter().any(|c| c.name == NAME));
let responses = super::runners::get_results(
NAME,
&sqlx::types::Uuid::parse_str(&campaign.campaign_id).unwrap(),
&AppData::new(data.clone()),
0,
50,
None,
)
.await
.unwrap();
assert_eq!(responses.len(), 1);
assert_eq!(responses[0].threads, Some(THREADS as usize));
let mut l = responses[0].benches.clone();
l.sort_by(|a, b| a.difficulty.cmp(&b.difficulty));
let mut r = BENCHES.clone();
r.sort_by(|a, b| a.difficulty.cmp(&b.difficulty));
assert_eq!(
super::runners::get_results(
NAME,
&sqlx::types::Uuid::parse_str(&campaign.campaign_id).unwrap(),
&AppData::new(data.clone()),
0,
50,
Some(SubmissionType::Wasm),
)
.await
.unwrap(),
responses
);
assert_eq!(
super::runners::get_results(
NAME,
&sqlx::types::Uuid::parse_str(&campaign.campaign_id).unwrap(),
&AppData::new(data.clone()),
0,
50,
Some(SubmissionType::Js),
)
.await
.unwrap(),
Vec::default()
);
assert_eq!(l, r);
assert_eq!(
responses[0].device_software_recognised,
DEVICE_SOFTWARE_RECOGNISED
);
assert_eq!(responses[0].device_user_provided, DEVICE_USER_PROVIDED);
let results_resp = get_request!(
&app,
&V1_API_ROUTES
.admin
.campaign
.get_results_route(&campaign.campaign_id, None),
cookies.clone()
);
assert_eq!(results_resp.status(), StatusCode::OK);
let res: Vec<super::SurveyResponse> = test::read_body_json(results_resp).await;
assert_eq!(responses, res);
bad_post_req_test_witout_payload(
&data,
NAME,
PASSWORD,
&V1_API_ROUTES.admin.campaign.delete.replace("{uuid}", NAME),

View File

@ -1,8 +1,19 @@
// Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
// SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/*
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use actix_auth_middleware::*;
use actix_web::web::ServiceConfig;
@ -13,6 +24,7 @@ pub mod campaigns;
mod tests;
pub use super::{get_random, get_uuid, RedirectQuery};
use crate::api::v1::bench::SURVEY_USER_ID;
pub fn services(cfg: &mut ServiceConfig) {
auth::services(cfg);
@ -28,9 +40,7 @@ pub mod routes {
use super::account::routes::Account;
use super::auth::routes::Auth;
use super::campaigns::routes::Campaign;
use serde::Serialize;
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct Admin {
pub auth: Auth,
pub account: Account,

View File

@ -1,13 +1,26 @@
// Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
// SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/*
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use actix_web::http::{header, StatusCode};
use actix_web::test;
use crate::api::v1::admin::auth::runners::{Login, Register};
use crate::api::v1::ROUTES;
use crate::data::Data;
use crate::errors::*;
use crate::*;
@ -15,7 +28,7 @@ use crate::tests::*;
#[actix_rt::test]
async fn auth_works() {
let data = get_test_data().await;
let data = Data::new().await;
const NAME: &str = "testuser";
const PASSWORD: &str = "longpassword";
const EMAIL: &str = "testuser1@a.com";
@ -41,11 +54,11 @@ async fn auth_works() {
delete_user(NAME, &data).await;
// 1. Register and signin
let (_, signin_resp) = register_and_signin(&data, NAME, EMAIL, PASSWORD).await;
let (_, _, signin_resp) = register_and_signin(NAME, EMAIL, PASSWORD).await;
let cookies = get_cookie!(signin_resp);
// Sign in with email
signin(&data, EMAIL, PASSWORD).await;
signin(EMAIL, PASSWORD).await;
// 2. check if duplicate username is allowed
let mut msg = Register {
@ -55,7 +68,6 @@ async fn auth_works() {
email: Some(EMAIL.into()),
};
bad_post_req_test(
&data,
NAME,
PASSWORD,
ROUTES.admin.auth.register,
@ -67,7 +79,6 @@ async fn auth_works() {
let name = format!("{}dupemail", NAME);
msg.username = name;
bad_post_req_test(
&data,
NAME,
PASSWORD,
ROUTES.admin.auth.register,
@ -82,7 +93,6 @@ async fn auth_works() {
password: msg.password.clone(),
};
bad_post_req_test(
&data,
NAME,
PASSWORD,
ROUTES.admin.auth.login,
@ -93,7 +103,6 @@ async fn auth_works() {
creds.login = "nonexistantuser@example.com".into();
bad_post_req_test(
&data,
NAME,
PASSWORD,
ROUTES.admin.auth.login,
@ -107,7 +116,6 @@ async fn auth_works() {
creds.password = NAME.into();
bad_post_req_test(
&data,
NAME,
PASSWORD,
ROUTES.admin.auth.login,
@ -138,7 +146,7 @@ async fn serverside_password_validation_works() {
const NAME: &str = "testuser542";
const PASSWORD: &str = "longpassword2";
let data = get_test_data().await;
let data = Data::new().await;
delete_user(NAME, &data).await;
let app = get_app!(data).await;

View File

@ -1,7 +1,18 @@
// Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
// SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/*
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
mod auth;
mod protected;

View File

@ -1,11 +1,24 @@
// Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
// SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/*
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use actix_web::http::StatusCode;
use actix_web::test;
use crate::data::Data;
use crate::*;
use crate::tests::*;
@ -17,13 +30,13 @@ async fn protected_routes_work() {
const EMAIL: &str = "testuser119@a.com2";
let get_protected_urls = [V1_API_ROUTES.admin.auth.logout];
let data = get_test_data().await;
{
let data = Data::new().await;
delete_user(NAME, &data).await;
}
let (_, signin_resp) = register_and_signin(&data, NAME, EMAIL, PASSWORD).await;
let (data, _, signin_resp) = register_and_signin(NAME, EMAIL, PASSWORD).await;
let cookies = get_cookie!(signin_resp);
let app = get_app!(data).await;

View File

@ -1,8 +1,19 @@
// Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
// SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/*
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use std::borrow::Cow;
use std::str::FromStr;
@ -13,7 +24,7 @@ use actix_web::{http, web, HttpResponse, Responder};
use futures::future::try_join_all;
use serde::{Deserialize, Serialize};
use sqlx::types::time::OffsetDateTime;
use sqlx::types::Uuid;
use uuid::Uuid;
use super::{get_uuid, RedirectQuery};
use crate::errors::*;
@ -22,11 +33,9 @@ use crate::AppData;
pub const SURVEY_USER_ID: &str = "survey_user_id";
pub mod routes {
use serde::{Deserialize, Serialize};
use actix_auth_middleware::GetLoginRoute;
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct Benches {
pub submit: &'static str,
pub register: &'static str,
@ -84,7 +93,7 @@ pub fn services(cfg: &mut web::ServiceConfig) {
pub mod runners {
use super::*;
pub async fn register_runner(data: &AppData) -> ServiceResult<sqlx::types::Uuid> {
pub async fn register_runner(data: &AppData) -> ServiceResult<uuid::Uuid> {
let mut uuid;
let now = OffsetDateTime::now_utc();
@ -116,7 +125,7 @@ pub mod runners {
}
}
#[actix_web_codegen_const_routes::get(path = "crate::V1_API_ROUTES.benches.register")]
#[my_codegen::get(path = "crate::V1_API_ROUTES.benches.register")]
async fn register(
data: AppData,
session: Session,
@ -146,53 +155,32 @@ async fn register(
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Clone)]
pub struct Bench {
pub duration: f32,
pub difficulty: i32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Clone)]
pub struct Submission {
pub device_user_provided: String,
pub device_software_recognised: String,
pub threads: i32,
pub benches: Vec<Bench>,
pub submission_type: SubmissionType,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SubmissionType {
Wasm,
Js,
}
impl ToString for SubmissionType {
fn to_string(&self) -> String {
let s = serde_json::to_string(&self).unwrap();
(&s[1..(s.len() - 1)]).to_string()
}
}
impl FromStr for SubmissionType {
type Err = serde_json::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
serde_json::from_str(&format!("\"{}\"", s))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Clone)]
pub struct SubmissionProof {
pub token: String,
pub proof: String,
}
fn is_session_authenticated(r: &HttpRequest, pl: &mut Payload) -> bool {
fn is_session_authenticated(r: &HttpRequest, mut pl: &mut Payload) -> bool {
use actix_web::FromRequest;
matches!(
Session::from_request(r, pl).into_inner().map(|x| {
Session::from_request(&r, &mut pl).into_inner().map(|x| {
let val = x.get::<String>(SURVEY_USER_ID);
println!("{:#?}", val);
val
}),
Ok(Ok(Some(_)))
@ -208,7 +196,7 @@ pub fn get_check_login() -> Authentication<routes::Benches> {
// }
//}
#[actix_web_codegen_const_routes::post(
#[my_codegen::post(
path = "crate::V1_API_ROUTES.benches.submit",
wrap = "get_check_login()"
)]
@ -224,38 +212,44 @@ async fn submit(
let user_id = Uuid::from_str(&username).unwrap();
let payload = payload.into_inner();
let now = OffsetDateTime::now_utc();
struct ID {
id: i32,
}
let resp_id = sqlx::query_as!(
ID,
sqlx::query!(
"INSERT INTO survey_responses (
user_id,
campaign_id,
device_user_provided,
device_software_recognised,
threads,
submitted_at,
submission_bench_type_id
) VALUES (
$1, $2, $3, $4, $5, $6,
(SELECT ID FROM survey_bench_type WHERE name = $7)
)
RETURNING ID;",
user_id,
campaign_id,
device_user_provided,
device_software_recognised,
threads
) VALUES ($1, $2, $3, $4, $5);",
&user_id,
&campaign_id,
&payload.device_user_provided,
&payload.device_software_recognised,
&payload.threads,
&now,
&payload.submission_type.to_string(),
&payload.threads
)
.execute(&data.db)
.await?;
struct ID {
id: i32,
}
let resp_id = sqlx::query_as!(
ID,
"SELECT ID
FROM survey_responses
WHERE
user_id = $1
AND
device_software_recognised = $2;",
&user_id,
&payload.device_software_recognised
)
.fetch_one(&data.db)
.await?;
let mut futs = Vec::with_capacity(payload.benches.len());
for bench in payload.benches.iter() {
let fut = sqlx::query!(
"INSERT INTO survey_benches
@ -265,7 +259,8 @@ async fn submit(
&bench.difficulty,
bench.duration
)
.execute(&data.db); //.await?;
.execute(&data.db);
futs.push(fut);
}
@ -307,12 +302,12 @@ async fn submit(
Ok(HttpResponse::Ok().json(resp))
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Serialize, Deserialize)]
pub struct BenchConfig {
pub difficulties: Vec<i32>,
}
#[actix_web_codegen_const_routes::get(
#[my_codegen::get(
path = "crate::V1_API_ROUTES.benches.fetch",
wrap = "get_check_login()"
)]
@ -329,14 +324,3 @@ async fn fetch(data: AppData, path: web::Path<String>) -> ServiceResult<impl Res
.await?;
Ok(HttpResponse::Ok().json(config))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn survey_response_type_no_panic_test() {
assert_eq!(SubmissionType::Wasm.to_string(), "wasm".to_string());
assert_eq!(SubmissionType::Js.to_string(), "js".to_string());
}
}

View File

@ -1,819 +0,0 @@
// Copyright (C) 2023 Aravinth Manivannan <realaravinth@batsense.net>
// SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use url::Url;
use uuid::Uuid;
use crate::api::v1::get_random;
use crate::db::{
JobState, JOB_STATES, JOB_STATE_CREATE, JOB_STATE_FINISH, JOB_STATE_RUNNING,
};
use crate::errors::*;
use crate::mcaptcha::PerformanceAnalytics;
use crate::Data;
use sqlx::types::time::OffsetDateTime;
fn now_unix_time_stamp() -> OffsetDateTime {
OffsetDateTime::now_utc()
}
impl Data {
/// Check if an mCaptcha instance is registered on the database
pub async fn mcaptcha_url_exists(&self, url: &str) -> ServiceResult<bool> {
let res = sqlx::query!(
"SELECT EXISTS (SELECT 1 from survey_mcaptcha_hostname WHERE url = $1)",
url
)
.fetch_one(&self.db)
.await?;
let mut resp = false;
if let Some(x) = res.exists {
if x {
resp = true;
}
}
Ok(resp)
}
/// Register an mCaptcha instance
pub async fn mcaptcha_register_instance(&self, url: &str) -> ServiceResult<String> {
let secret = get_random(32);
sqlx::query!(
"INSERT INTO survey_mcaptcha_hostname (url, secret) VALUES ($1, $2)",
url,
&secret,
)
.execute(&self.db)
.await?;
Ok(secret)
}
/// Update the secret of an mCaptcha instance
pub async fn mcaptcha_update_secret(&self, url: &str) -> ServiceResult<String> {
let secret = get_random(32);
sqlx::query!(
"UPDATE survey_mcaptcha_hostname set secret = $1 WHERE url = $2",
&secret,
url
)
.execute(&self.db)
.await?;
Ok(secret)
}
/// Authenticate an mCaptcha instance and return its URL
pub async fn mcaptcha_authenticate(&self, secret: &str) -> ServiceResult<()> {
let res = sqlx::query!(
"SELECT EXISTS (
SELECT
url
FROM
survey_mcaptcha_hostname
WHERE secret = $1
)",
secret
)
.fetch_one(&self.db)
.await?;
if !matches!(res.exists, Some(true)) {
return Err(ServiceError::WrongPassword);
}
Ok(())
}
/// Delete mCaptcha instance from database
pub async fn mcaptcha_delete_mcaptcha_instance(
&self,
url: &str,
secret: &str,
) -> ServiceResult<()> {
sqlx::query!(
"DELETE FROM survey_mcaptcha_hostname WHERE secret = $1 AND url =$2",
secret,
url
)
.execute(&self.db)
.await?;
Ok(())
}
/// Delete mCaptcha campaign from database
pub async fn mcaptcha_delete_mcaptcha_campaign(
&self,
campaign_id: &Uuid,
secret: &str,
) -> ServiceResult<()> {
let campaign_str = campaign_id.to_string();
sqlx::query!(
"DELETE FROM
survey_mcaptcha_campaign
WHERE
campaign_id = $1
AND
url_id = (
SELECT
ID
FROM
survey_mcaptcha_hostname
WHERE
secret = $2
)",
&campaign_str,
secret
)
.execute(&self.db)
.await?;
Ok(())
}
/// Check if an mCaptcha instance campaign is registered on DB
pub async fn mcaptcha_campaign_is_registered(
&self,
campaign_id: &Uuid,
secret: &str,
) -> ServiceResult<bool> {
let campaign_str = campaign_id.to_string();
let res = sqlx::query!(
"SELECT EXISTS (
SELECT
ID
FROM
survey_mcaptcha_campaign
WHERE
campaign_id = $1
AND
url_id = (
SELECT
ID
FROM
survey_mcaptcha_hostname
WHERE
secret = $2
)
)",
&campaign_str,
secret
)
.fetch_one(&self.db)
.await?;
let mut resp = false;
if let Some(x) = res.exists {
if x {
resp = true;
}
}
Ok(resp)
}
/// Register an mCaptcha instance campaign on DB
pub async fn mcaptcha_register_campaign(
&self,
campaign_id: &Uuid,
secret: &str,
) -> ServiceResult<()> {
let campaign_str = campaign_id.to_string();
let public_id = Uuid::new_v4();
sqlx::query!(
"INSERT INTO
survey_mcaptcha_campaign (campaign_id, public_id, url_id)
VALUES ($1, $2, (SELECT ID FROM survey_mcaptcha_hostname WHERE secret = $3));",
&campaign_str,
&public_id.to_string(),
secret,
)
.execute(&self.db)
.await?;
Ok(())
}
/// Register an mCaptcha instance campaign on DB
pub async fn mcaptcha_get_campaign_public_id(
&self,
campaign_id: &Uuid,
secret: &str,
) -> ServiceResult<Uuid> {
let campaign_str = campaign_id.to_string();
struct S {
public_id: String,
}
let res = sqlx::query_as!(
S,
"SELECT
public_id
FROM
survey_mcaptcha_campaign
WHERE
campaign_id = $1
AND
url_id = (SELECT ID FROM survey_mcaptcha_hostname WHERE secret = $2);",
&campaign_str,
secret,
)
.fetch_one(&self.db)
.await?;
Ok(Uuid::parse_str(&res.public_id).unwrap())
}
/// Get an mCaptcha instance campaign checkpoint
pub async fn mcaptcha_get_checkpoint(
&self,
campaign_id: &Uuid,
) -> ServiceResult<usize> {
let campaign_str = campaign_id.to_string();
struct CheckPoint {
synced_till: i32,
}
let checkpoint = sqlx::query_as!(
CheckPoint,
"SELECT
synced_till
FROM
survey_mcaptcha_campaign
WHERE
campaign_id = $1;",
&campaign_str,
)
.fetch_one(&self.db)
.await?;
let checkpoint = checkpoint.synced_till as usize;
Ok(checkpoint)
}
/// Set an mCaptcha instance campaign checkpoint
pub async fn mcaptcha_set_checkpoint(
&self,
campaign_id: &Uuid,
checkpoint: usize,
) -> ServiceResult<()> {
let campaign_str = campaign_id.to_string();
sqlx::query!(
"UPDATE
survey_mcaptcha_campaign
SET
synced_till = $1
WHERE
campaign_id = $2; ",
checkpoint as i32,
&campaign_str,
)
.execute(&self.db)
.await?;
Ok(())
}
/// Store mCaptcha instance campaign analytics
pub async fn mcaptcha_insert_analytics(
&self,
campaign_id: &Uuid,
r: &PerformanceAnalytics,
) -> ServiceResult<()> {
let campaign_str = campaign_id.to_string();
sqlx::query!(
"INSERT INTO
survey_mcaptcha_analytics (
campaign_id, time, difficulty_factor, worker_type
)
VALUES ((
SELECT
ID
FROM
survey_mcaptcha_campaign
WHERE
campaign_id = $1
), $2, $3, $4
);",
&campaign_str,
r.time as i32,
r.difficulty_factor as i32,
&r.worker_type,
)
.execute(&self.db)
.await?;
Ok(())
}
/// fetch PoW analytics
pub async fn mcaptcha_analytics_fetch(
&self,
public_id: &Uuid,
limit: usize,
offset: usize,
) -> ServiceResult<Vec<PerformanceAnalytics>> {
let public_id_str = public_id.to_string();
struct P {
id: i32,
time: i32,
difficulty_factor: i32,
worker_type: String,
}
impl From<P> for PerformanceAnalytics {
fn from(v: P) -> Self {
Self {
time: v.time as u32,
difficulty_factor: v.difficulty_factor as u32,
worker_type: v.worker_type,
id: v.id as usize,
}
}
}
let mut c = sqlx::query_as!(
P,
"SELECT id, time, difficulty_factor, worker_type FROM survey_mcaptcha_analytics
WHERE
campaign_id = (
SELECT
ID FROM survey_mcaptcha_campaign
WHERE
public_id = $1
)
ORDER BY ID
OFFSET $2 LIMIT $3
",
&public_id_str,
offset as i32,
limit as i32
)
.fetch_all(&self.db)
.await?;
let mut res = Vec::with_capacity(c.len());
for i in c.drain(0..) {
res.push(i.into())
}
Ok(res)
}
pub async fn get_next_job_to_run(&self) -> ServiceResult<Option<SchedulerJob>> {
let res = match sqlx::query_as!(
InnerSchedulerJob,
"SELECT
survey_mcaptcha_campaign.campaign_id,
survey_mcaptcha_upload_jobs.public_id,
survey_mcaptcha_hostname.url
FROM
survey_mcaptcha_campaign
INNER JOIN
survey_mcaptcha_upload_jobs
ON
survey_mcaptcha_upload_jobs.campaign_id = survey_mcaptcha_campaign.ID
INNER JOIN
survey_mcaptcha_hostname
ON
survey_mcaptcha_hostname.ID = survey_mcaptcha_campaign.url_id
WHERE
survey_mcaptcha_upload_jobs.job_state = (
SELECT ID FROM survey_mcaptcha_upload_job_states WHERE name = $1
)
AND
survey_mcaptcha_upload_jobs.finished_at is NULL
AND
survey_mcaptcha_upload_jobs.scheduled_at is NULL
ORDER BY created_at ASC;",
&JOB_STATE_CREATE.name
)
.fetch_one(&self.db)
.await
{
Ok(res) => Ok(Some(res.into())),
Err(sqlx::Error::RowNotFound) => Ok(None),
Err(e) => Err(e),
}?;
Ok(res)
}
pub async fn add_job(&self, campaign_id: &Uuid) -> ServiceResult<Uuid> {
let now = now_unix_time_stamp();
if let Some(unfinished_job) =
self.get_unfinished_job_for_campaign(campaign_id).await?
{
return Ok(unfinished_job.public_job_id);
}
let public_id = Uuid::new_v4();
let public_id_str = public_id.to_string();
let campaign_str = campaign_id.to_string();
sqlx::query!(
"INSERT INTO survey_mcaptcha_upload_jobs
(campaign_id, job_state, created_at, public_id)
VALUES (
(SELECT ID FROM survey_mcaptcha_campaign WHERE campaign_id = $1),
(SELECT ID FROM survey_mcaptcha_upload_job_states WHERE name = $2),
$3, $4)",
&campaign_str,
&JOB_STATE_CREATE.name,
now,
public_id_str
)
.execute(&self.db)
.await?;
Ok(public_id)
}
pub async fn get_unfinished_job_for_campaign(
&self,
campaign_id: &Uuid,
) -> ServiceResult<Option<Job>> {
let res = match sqlx::query_as!(
InnerJob,
"
SELECT
survey_mcaptcha_upload_jobs.ID,
survey_mcaptcha_upload_jobs.public_id,
survey_mcaptcha_campaign.campaign_id,
survey_mcaptcha_campaign.public_id as campaign_public_id,
survey_mcaptcha_upload_job_states.name,
survey_mcaptcha_upload_jobs.created_at,
survey_mcaptcha_upload_jobs.scheduled_at,
survey_mcaptcha_upload_jobs.finished_at
FROM survey_mcaptcha_upload_jobs
INNER JOIN
survey_mcaptcha_upload_job_states
ON
survey_mcaptcha_upload_job_states.ID = survey_mcaptcha_upload_jobs.job_state
INNER JOIN
survey_mcaptcha_campaign
ON
survey_mcaptcha_campaign.ID = survey_mcaptcha_upload_jobs.campaign_id
WHERE
survey_mcaptcha_campaign.campaign_id = $1
AND
survey_mcaptcha_upload_job_states.name = $2;",
&campaign_id.to_string(),
&JOB_STATE_CREATE.name
)
.fetch_one(&self.db)
.await {
Ok(res) => Ok(Some(res.into())),
Err(sqlx::Error::RowNotFound) => Ok(None),
Err(e) => Err(e),
}?;
Ok(res)
}
pub async fn get_job(&self, public_id: &uuid::Uuid) -> ServiceResult<Option<Job>> {
let res = match sqlx::query_as!(
InnerJob,
"
SELECT
survey_mcaptcha_upload_jobs.ID,
survey_mcaptcha_upload_jobs.public_id,
survey_mcaptcha_campaign.campaign_id,
survey_mcaptcha_campaign.public_id as campaign_public_id,
survey_mcaptcha_upload_job_states.name,
survey_mcaptcha_upload_jobs.created_at,
survey_mcaptcha_upload_jobs.scheduled_at,
survey_mcaptcha_upload_jobs.finished_at
FROM survey_mcaptcha_upload_jobs
INNER JOIN
survey_mcaptcha_upload_job_states
ON
survey_mcaptcha_upload_job_states.ID = survey_mcaptcha_upload_jobs.job_state
INNER JOIN
survey_mcaptcha_campaign
ON
survey_mcaptcha_campaign.ID = survey_mcaptcha_upload_jobs.campaign_id
WHERE
survey_mcaptcha_upload_jobs.public_id = $1",
&public_id.to_string()
)
.fetch_one(&self.db)
.await {
Ok(res) => Ok(Some(res.into())),
Err(sqlx::Error::RowNotFound) => Ok(None),
Err(e) => Err(e),
}?;
Ok(res)
}
pub async fn get_all_jobs_of_state(
&self,
state: &JobState,
) -> ServiceResult<Vec<Job>> {
let mut res = sqlx::query_as!(
InnerJob,
"
SELECT
survey_mcaptcha_upload_jobs.ID,
survey_mcaptcha_upload_jobs.public_id,
survey_mcaptcha_campaign.campaign_id,
survey_mcaptcha_campaign.public_id as campaign_public_id,
survey_mcaptcha_upload_job_states.name,
survey_mcaptcha_upload_jobs.created_at,
survey_mcaptcha_upload_jobs.scheduled_at,
survey_mcaptcha_upload_jobs.finished_at
FROM survey_mcaptcha_upload_jobs
INNER JOIN
survey_mcaptcha_upload_job_states
ON
survey_mcaptcha_upload_job_states.ID = survey_mcaptcha_upload_jobs.job_state
INNER JOIN
survey_mcaptcha_campaign
ON
survey_mcaptcha_campaign.ID = survey_mcaptcha_upload_jobs.campaign_id
WHERE
survey_mcaptcha_upload_job_states.name = $1;",
&state.name
)
.fetch_all(&self.db)
.await?;
let res = res.drain(0..).map(|r| r.into()).collect();
Ok(res)
}
pub async fn mark_job_scheduled(&self, job: &SchedulerJob) -> ServiceResult<()> {
let now = now_unix_time_stamp();
sqlx::query!(
"
UPDATE
survey_mcaptcha_upload_jobs
SET
job_state = (SELECT ID FROM survey_mcaptcha_upload_job_states WHERE name = $1),
scheduled_at = $2
WHERE public_id = $3;",
&JOB_STATE_RUNNING.name,
now,
&job.public_job_id.to_string(),
)
.execute(&self.db)
.await
?;
Ok(())
}
pub async fn mark_job_finished(&self, job: &SchedulerJob) -> ServiceResult<()> {
let now = now_unix_time_stamp();
sqlx::query!(
"
UPDATE
survey_mcaptcha_upload_jobs
SET
job_state = (SELECT ID FROM survey_mcaptcha_upload_job_states WHERE name = $1),
finished_at = $2
WHERE public_id = $3;",
&JOB_STATE_FINISH.name,
now,
&job.public_job_id.to_string(),
)
.execute(&self.db)
.await
?;
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SchedulerJob {
pub campaign_id: Uuid,
pub public_job_id: Uuid,
pub url: Url,
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct InnerSchedulerJob {
campaign_id: String,
public_id: String,
url: String,
}
impl From<InnerSchedulerJob> for SchedulerJob {
fn from(j: InnerSchedulerJob) -> Self {
SchedulerJob {
campaign_id: Uuid::parse_str(&j.campaign_id).unwrap(),
public_job_id: Uuid::parse_str(&j.public_id).unwrap(),
url: Url::parse(&j.url).unwrap(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Job {
pub state: JobState,
pub campaign_id: Uuid,
pub campaign_public_id: Uuid,
pub public_job_id: Uuid,
pub id: u32,
pub created_at: OffsetDateTime,
pub scheduled_at: Option<OffsetDateTime>,
pub finished_at: Option<OffsetDateTime>,
}
struct InnerJob {
name: String,
campaign_id: String,
public_id: String,
campaign_public_id: String,
id: i32,
created_at: OffsetDateTime,
scheduled_at: Option<OffsetDateTime>,
finished_at: Option<OffsetDateTime>,
}
impl From<InnerJob> for Job {
fn from(j: InnerJob) -> Self {
Job {
state: (JOB_STATES)
.iter()
.find(|d| d.name == j.name)
.unwrap()
.to_owned()
.to_owned(),
id: j.id as u32,
created_at: j.created_at,
scheduled_at: j.scheduled_at,
finished_at: j.finished_at,
campaign_id: Uuid::parse_str(&j.campaign_id).unwrap(),
campaign_public_id: Uuid::parse_str(&j.campaign_public_id).unwrap(),
public_job_id: Uuid::parse_str(&j.public_id).unwrap(),
}
}
}
#[cfg(test)]
mod tests {
use crate::{mcaptcha::PerformanceAnalytics, tests::*};
use super::*;
use url::Url;
#[actix_rt::test]
async fn test_db_mcaptcha_works() {
let url = Url::parse("http://test_add_campaign.example").unwrap();
let data = get_test_data().await;
let url_str = url.to_string();
if data.mcaptcha_url_exists(&url_str).await.unwrap() {
let secret = data.mcaptcha_update_secret(&url_str).await.unwrap();
data.mcaptcha_delete_mcaptcha_instance(&url_str, &secret)
.await
.unwrap();
}
assert!(!data.mcaptcha_url_exists(&url_str).await.unwrap());
let secret = data.mcaptcha_register_instance(&url_str).await.unwrap();
assert!(data.mcaptcha_url_exists(&url_str).await.unwrap());
let secret2 = data.mcaptcha_update_secret(&url_str).await.unwrap();
assert_ne!(secret2, secret);
let secret = secret2;
assert!(data.mcaptcha_authenticate(&secret).await.is_ok());
assert_eq!(
data.mcaptcha_authenticate("foo").await.err(),
Some(ServiceError::WrongPassword)
);
let uuid = Uuid::new_v4();
if data
.mcaptcha_campaign_is_registered(&uuid, &secret)
.await
.unwrap()
{
data.mcaptcha_delete_mcaptcha_campaign(&uuid, &secret)
.await
.unwrap();
}
assert!(!data
.mcaptcha_campaign_is_registered(&uuid, &secret)
.await
.unwrap());
data.mcaptcha_register_campaign(&uuid, &secret)
.await
.unwrap();
assert!(data
.mcaptcha_campaign_is_registered(&uuid, &secret)
.await
.unwrap());
assert_eq!(data.mcaptcha_get_checkpoint(&uuid).await.unwrap(), 0);
data.mcaptcha_set_checkpoint(&uuid, 1).await.unwrap();
assert_eq!(data.mcaptcha_get_checkpoint(&uuid).await.unwrap(), 1);
let analytics = PerformanceAnalytics {
id: 1,
time: 1,
difficulty_factor: 1,
worker_type: "foo".to_string(),
};
data.mcaptcha_insert_analytics(&uuid, &analytics)
.await
.unwrap();
let public_id = data
.mcaptcha_get_campaign_public_id(&uuid, &secret)
.await
.unwrap();
let db_analytics = data
.mcaptcha_analytics_fetch(&public_id, 50, 0)
.await
.unwrap();
assert_eq!(db_analytics.len(), 1);
assert_eq!(db_analytics[0].time, analytics.time);
assert_eq!(
db_analytics[0].difficulty_factor,
analytics.difficulty_factor
);
assert_eq!(db_analytics[0].worker_type, analytics.worker_type);
assert_eq!(
data.mcaptcha_analytics_fetch(&public_id, 50, 1)
.await
.unwrap(),
vec![]
);
// job related stuff
let job1_public_id = data.add_job(&uuid).await.unwrap();
let job = data.get_job(&job1_public_id).await.unwrap().unwrap();
assert_eq!(public_id, job.campaign_public_id);
assert_eq!(
data.get_unfinished_job_for_campaign(&uuid)
.await
.unwrap()
.unwrap(),
job
);
let job2_public_id = data.add_job(&uuid).await.unwrap();
let job2 = data.get_job(&job2_public_id).await.unwrap().unwrap();
assert_eq!(job2, job);
let scheduler_job = data.get_next_job_to_run().await.unwrap().unwrap();
assert_eq!(scheduler_job.url, url);
assert_eq!(
data.get_next_job_to_run()
.await
.unwrap()
.unwrap()
.public_job_id,
job.public_job_id
);
assert!(job.created_at < now_unix_time_stamp());
assert!(job.scheduled_at.is_none());
assert!(job.finished_at.is_none());
assert_eq!(
data.get_all_jobs_of_state(&JOB_STATE_CREATE).await.unwrap(),
vec![job.clone()]
);
data.mark_job_scheduled(&scheduler_job).await.unwrap();
assert!(data.get_next_job_to_run().await.unwrap().is_none(),);
let job = data.get_job(&job.public_job_id).await.unwrap().unwrap();
assert!(job.scheduled_at.is_some());
assert_eq!(
data.get_all_jobs_of_state(&JOB_STATE_RUNNING)
.await
.unwrap(),
vec![job.clone()]
);
data.mark_job_finished(&scheduler_job).await.unwrap();
let job = data.get_job(&job.public_job_id).await.unwrap().unwrap();
assert!(job.finished_at.is_some());
assert_eq!(
data.get_all_jobs_of_state(&JOB_STATE_FINISH).await.unwrap(),
vec![job.clone()]
);
let job2_public_id = data.add_job(&uuid).await.unwrap();
let job2 = data.get_job(&job2_public_id).await.unwrap().unwrap();
assert_ne!(job2.public_job_id, job.public_job_id);
assert_eq!(
data.get_next_job_to_run()
.await
.unwrap()
.unwrap()
.public_job_id,
job2.public_job_id
);
assert_eq!(public_id, job2.campaign_public_id);
}
}

View File

@ -1,260 +0,0 @@
// Copyright (C) 2023 Aravinth Manivannan <realaravinth@batsense.net>
// SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use actix_web::web::ServiceConfig;
use actix_web::{web, HttpResponse, Responder};
use serde::{Deserialize, Serialize};
use url::Url;
use uuid::Uuid;
use crate::api::v1::ROUTES;
use crate::errors::*;
use crate::AppData;
pub fn services(cfg: &mut ServiceConfig) {
cfg.service(register);
cfg.service(upload);
cfg.service(download);
}
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
pub struct MCaptchaInstance {
pub url: Url,
pub auth_token: String,
}
#[actix_web_codegen_const_routes::post(path = "ROUTES.mcaptcha.register")]
async fn register(
data: AppData,
payload: web::Json<MCaptchaInstance>,
) -> ServiceResult<impl Responder> {
/* Summary
* 1. Check if secret exists
* 2. If not, add hostname and create secret
* 3. Post to mCaptcha
*/
let url_str = payload.url.to_string();
let secret = if data.mcaptcha_url_exists(&url_str).await? {
data.mcaptcha_update_secret(&url_str).await?
} else {
data.mcaptcha_register_instance(&url_str).await?
};
let payload = payload.into_inner();
data.mcaptcha
.share_secret(payload.url, secret, payload.auth_token)
.await?;
Ok(HttpResponse::Ok())
}
#[derive(Deserialize, Serialize, Debug, Eq, PartialEq, Clone)]
pub struct UploadJobCreated {
id: Uuid,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq)]
pub struct Secret {
pub secret: String,
}
#[actix_web_codegen_const_routes::post(path = "ROUTES.mcaptcha.upload")]
async fn upload(
data: AppData,
campaign: web::Path<uuid::Uuid>,
payload: web::Json<Secret>,
) -> ServiceResult<impl Responder> {
/* TODO
* 1. Authenticate: Get URL from secret
* 2. Check if campaign exists
* 3. If not: create campaign
* 4. Get last known sync point
* 5. Download results
* 6. Update sync point
*/
data.mcaptcha_authenticate(&payload.secret).await?;
// let campaign_str = campaign.to_string();
if !data
.mcaptcha_campaign_is_registered(&campaign, &payload.secret)
.await?
{
data.mcaptcha_register_campaign(&campaign, &payload.secret)
.await?;
}
let res = UploadJobCreated {
id: data.add_job(&campaign).await?,
};
Ok(HttpResponse::Created().json(res))
}
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
pub struct Page {
pub page: usize,
}
#[actix_web_codegen_const_routes::get(path = "ROUTES.mcaptcha.download")]
async fn download(
data: AppData,
page: web::Query<Page>,
public_id: web::Path<uuid::Uuid>,
) -> ServiceResult<impl Responder> {
const LIMIT: usize = 50;
let offset = LIMIT as isize * ((page.page as isize) - 1);
let offset = if offset < 0 { 0 } else { offset };
let public_id = public_id.into_inner();
let resp = data
.mcaptcha_analytics_fetch(&public_id, LIMIT, offset as usize)
.await?;
Ok(HttpResponse::Ok().json(resp))
}
#[cfg(test)]
mod tests {
use super::Secret;
use crate::api::v1::get_random;
use crate::mcaptcha::PerformanceAnalytics;
use crate::tests::*;
use crate::*;
use actix_web::test;
#[actix_rt::test]
async fn mcaptcha_hooks_work() {
let mcaptcha_instance =
url::Url::parse("http://mcaptcha_hooks_work.example.org").unwrap();
let mcaptcha_instance_str = mcaptcha_instance.to_string();
let campaign_id = uuid::Uuid::new_v4();
let (data, client) = get_test_data_with_mcaptcha_client().await;
let app = get_app!(data).await;
let mcaptcha_downloader =
crate::mcaptcha::MCaptchaDownloader::new(AppData::new(data.clone()));
let (mcaptcha_downloader_killer, mcaptcha_downloader_job) =
mcaptcha_downloader.start_job().await.unwrap();
if data
.mcaptcha_url_exists(&mcaptcha_instance_str)
.await
.unwrap()
{
let secret = data
.mcaptcha_update_secret(&mcaptcha_instance_str)
.await
.unwrap();
data.mcaptcha_delete_mcaptcha_instance(&mcaptcha_instance_str, &secret)
.await
.unwrap();
}
let payload = super::MCaptchaInstance {
url: mcaptcha_instance.clone(),
auth_token: get_random(23),
};
let resp = test::call_service(
&app,
post_request!(&payload, V1_API_ROUTES.mcaptcha.register).to_request(),
)
.await;
assert_eq!(resp.status(), StatusCode::OK);
let secret = {
let mut mcaptcha = payload.url.clone();
mcaptcha.set_path("/api/v1/survey/secret");
let mut x = client.client.write().unwrap();
x.remove(&mcaptcha.to_string()).unwrap()
};
let resp2 = test::call_service(
&app,
post_request!(&payload, V1_API_ROUTES.mcaptcha.register).to_request(),
)
.await;
assert_eq!(resp2.status(), StatusCode::OK);
let secret2 = {
let mut mcaptcha = payload.url.clone();
mcaptcha.set_path("/api/v1/survey/secret");
let mut x = client.client.write().unwrap();
x.remove(&mcaptcha.to_string()).unwrap()
};
assert_ne!(secret, secret2);
let secret = secret2;
let payload = Secret {
secret: secret.clone(),
};
if data
.mcaptcha_campaign_is_registered(&campaign_id, &secret)
.await
.unwrap()
{
data.mcaptcha_delete_mcaptcha_campaign(&campaign_id, &secret)
.await
.unwrap();
}
let resp = test::call_service(
&app,
post_request!(
&payload,
&V1_API_ROUTES
.mcaptcha
.get_upload_route(&campaign_id.to_string())
)
.to_request(),
)
.await;
assert_eq!(resp.status(), StatusCode::CREATED);
let job: super::UploadJobCreated = test::read_body_json(resp).await;
loop {
if let Some(job) = data.get_job(&job.id).await.unwrap() {
if job.state == *crate::db::JOB_STATE_FINISH {
break;
}
}
tokio::time::sleep(std::time::Duration::new(1, 0)).await;
}
let public_id = data
.mcaptcha_get_campaign_public_id(&campaign_id, &secret)
.await
.unwrap();
let expected = crate::mcaptcha::tests::BENCHMARK.clone();
let got = data
.mcaptcha_analytics_fetch(&public_id, 50, 0)
.await
.unwrap();
for i in 0..2 {
assert_eq!(got[i].time, expected[i].time);
assert_eq!(got[i].difficulty_factor, expected[i].difficulty_factor);
assert_eq!(got[i].worker_type, expected[i].worker_type);
}
let resp = get_request!(
&app,
&V1_API_ROUTES
.mcaptcha
.get_download_route(&public_id.to_string(), 0)
);
assert_eq!(resp.status(), StatusCode::OK);
let resp: Vec<PerformanceAnalytics> = test::read_body_json(resp).await;
assert_eq!(resp.len(), 2);
assert_eq!(resp, got);
mcaptcha_downloader_killer.send(()).unwrap();
mcaptcha_downloader_job.await.unwrap();
}
}

View File

@ -1,46 +0,0 @@
// Copyright (C) 2023 Aravinth Manivannan <realaravinth@batsense.net>
// SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use actix_web::web::ServiceConfig;
pub mod db;
pub mod hooks;
pub fn services(cfg: &mut ServiceConfig) {
hooks::services(cfg);
}
pub mod routes {
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct Mcaptcha {
pub upload: &'static str,
pub download: &'static str,
pub register: &'static str,
}
impl Mcaptcha {
pub const fn new() -> Self {
Self {
register: "/mcaptcha/api/v1/register",
upload: "/mcaptcha/api/v1/{campaign_id}/upload",
download: "/mcapthca/api/v1/{campaign_id}/download",
}
}
pub fn get_download_route(&self, campaign_id: &str, page: usize) -> String {
format!(
"{}?page={}",
self.download.replace("{campaign_id}", campaign_id),
page
)
}
pub fn get_upload_route(&self, campaign_id: &str) -> String {
self.upload.replace("{campaign_id}", campaign_id)
}
}
}

View File

@ -1,7 +1,19 @@
// Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
// SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/*
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use actix_web::{web, HttpResponse, Responder};
use derive_builder::Builder;
@ -17,9 +29,6 @@ pub struct BuildDetails {
}
pub mod routes {
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct Meta {
pub build_details: &'static str,
pub health: &'static str,
@ -36,7 +45,7 @@ pub mod routes {
}
/// emmits build details of the bninary
#[actix_web_codegen_const_routes::get(path = "crate::V1_API_ROUTES.meta.build_details")]
#[my_codegen::get(path = "crate::V1_API_ROUTES.meta.build_details")]
async fn build_details() -> impl Responder {
let build = BuildDetails {
version: VERSION,
@ -52,7 +61,7 @@ pub struct Health {
}
/// checks all components of the system
#[actix_web_codegen_const_routes::get(path = "crate::V1_API_ROUTES.meta.health")]
#[my_codegen::get(path = "crate::V1_API_ROUTES.meta.health")]
async fn health(data: AppData) -> impl Responder {
use sqlx::Connection;
@ -78,7 +87,6 @@ mod tests {
use super::*;
use crate::api::v1::services;
use crate::tests::get_test_data;
use crate::*;
#[actix_rt::test]
@ -98,7 +106,7 @@ mod tests {
#[actix_rt::test]
async fn health_works() {
println!("{}", V1_API_ROUTES.meta.health);
let data = get_test_data().await;
let data = Data::new().await;
let app = get_app!(data).await;
let resp = test::call_service(

View File

@ -1,26 +1,33 @@
// Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
// SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/*
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use actix_web::web::ServiceConfig;
use serde::Deserialize;
use sqlx::types::Uuid;
use uuid::Uuid;
pub mod admin;
pub mod bench;
pub mod mcaptcha;
mod meta;
pub mod routes;
pub mod stats;
pub use routes::ROUTES;
pub fn services(cfg: &mut ServiceConfig) {
meta::services(cfg);
bench::services(cfg);
admin::services(cfg);
mcaptcha::services(cfg);
stats::services(cfg);
}
pub fn get_random(len: usize) -> String {
@ -37,7 +44,7 @@ pub fn get_random(len: usize) -> String {
}
pub fn get_uuid() -> Uuid {
Uuid::parse_str(&uuid::Uuid::new_v4().to_string()).unwrap()
Uuid::new_v4()
}
#[derive(Deserialize)]

View File

@ -1,25 +1,29 @@
// Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
// SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use serde::Serialize;
/*
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use super::admin::routes::Admin;
use super::bench::routes::Benches;
use super::mcaptcha::routes::Mcaptcha;
use super::meta::routes::Meta;
use super::stats::routes::Stats;
pub const ROUTES: Routes = Routes::new();
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct Routes {
pub admin: Admin,
pub meta: Meta,
pub benches: Benches,
pub mcaptcha: Mcaptcha,
pub stats: Stats,
}
impl Routes {
@ -28,8 +32,6 @@ impl Routes {
admin: Admin::new(),
meta: Meta::new(),
benches: Benches::new(),
mcaptcha: Mcaptcha::new(),
stats: Stats::new(),
}
}
}

View File

@ -1,256 +0,0 @@
// Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
// SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use actix_web::{web, HttpResponse, Responder};
use derive_builder::Builder;
use serde::{Deserialize, Serialize};
use crate::errors::*;
use crate::AppData;
#[derive(Clone, Debug, Deserialize, Builder, Serialize)]
pub struct BuildDetails {
pub version: &'static str,
pub git_commit_hash: &'static str,
}
pub mod routes {
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct Stats {
pub percentile_benches: &'static str,
}
impl Stats {
pub const fn new() -> Self {
Self {
percentile_benches: "/api/v1/stats/benches/percentile",
}
}
}
}
/// Get difficulty factor with max time limit for percentile of stats
#[actix_web_codegen_const_routes::post(
path = "crate::V1_API_ROUTES.stats.percentile_benches"
)]
async fn percentile_benches(
data: AppData,
payload: web::Json<PercentileReq>,
) -> ServiceResult<impl Responder> {
struct Count {
count: Option<i64>,
}
let count = sqlx::query_as!(
Count,
"SELECT COUNT(difficulty) FROM survey_benches WHERE duration <= $1;",
payload.time as f32
)
.fetch_one(&data.db)
.await?;
if count.count.is_none() {
return Ok(HttpResponse::Ok().json(PercentileResp {
difficulty_factor: None,
}));
}
let count = count.count.unwrap();
if count < 2 {
return Ok(HttpResponse::Ok().json(PercentileResp {
difficulty_factor: None,
}));
}
let location = ((count - 1) as f64 * (payload.percentile / 100.00)) + 1.00;
let fraction = location - location.floor();
async fn get_data_at_location(
data: &crate::Data,
time: u32,
location: i64,
) -> ServiceResult<Option<u32>> {
struct Difficulty {
difficulty: Option<i32>,
}
match sqlx::query_as!(
Difficulty,
"SELECT
difficulty
FROM
survey_benches
WHERE
duration <= $1
ORDER BY difficulty ASC LIMIT 1 OFFSET $2;",
time as f32,
location as i64 - 1,
)
.fetch_one(&data.db)
.await
{
Ok(res) => Ok(Some(res.difficulty.unwrap() as u32)),
Err(sqlx::Error::RowNotFound) => Ok(None),
Err(e) => Err(e.into()),
}
}
if fraction > 0.00 {
if let (Some(base), Some(ceiling)) = (
get_data_at_location(&data, payload.time, location.floor() as i64).await?,
get_data_at_location(&data, payload.time, location.floor() as i64 + 1)
.await?,
) {
let res = base as u32 + ((ceiling - base) as f64 * fraction).floor() as u32;
return Ok(HttpResponse::Ok().json(PercentileResp {
difficulty_factor: Some(res),
}));
}
} else {
if let Some(base) =
get_data_at_location(&data, payload.time, location.floor() as i64).await?
{
let res = base as u32;
return Ok(HttpResponse::Ok().json(PercentileResp {
difficulty_factor: Some(res),
}));
}
};
Ok(HttpResponse::Ok().json(PercentileResp {
difficulty_factor: None,
}))
}
#[derive(Clone, Debug, Deserialize, Builder, Serialize)]
/// Health check return datatype
pub struct PercentileReq {
time: u32,
percentile: f64,
}
#[derive(Clone, Debug, Deserialize, Builder, Serialize)]
/// Health check return datatype
pub struct PercentileResp {
difficulty_factor: Option<u32>,
}
pub fn services(cfg: &mut web::ServiceConfig) {
cfg.service(percentile_benches);
}
#[cfg(test)]
mod tests {
use actix_web::{http::StatusCode, test, App};
use super::*;
use crate::api::v1::services;
use crate::tests::get_test_data;
use crate::*;
#[actix_rt::test]
async fn stats_bench_work() {
use crate::tests::*;
const NAME: &str = "benchstatsuesr";
const EMAIL: &str = "benchstatsuesr@testadminuser.com";
const PASSWORD: &str = "longpassword2";
const DEVICE_USER_PROVIDED: &str = "foo";
const DEVICE_SOFTWARE_RECOGNISED: &str = "Foobar.v2";
const THREADS: i32 = 4;
let data = get_test_data().await;
{
delete_user(NAME, &data).await;
}
let (creds, signin_resp) =
register_and_signin(&data, NAME, EMAIL, PASSWORD).await;
let cookies = get_cookie!(signin_resp);
let app = get_app!(data).await;
let survey = get_survey_user(data.clone()).await;
let survey_cookie = get_cookie!(survey);
let campaign = create_new_campaign(NAME, data.clone(), cookies.clone()).await;
let campaign_config =
get_campaign_config(&campaign, data.clone(), survey_cookie.clone()).await;
assert_eq!(DIFFICULTIES.to_vec(), campaign_config.difficulties);
let submit_payload = crate::api::v1::bench::Submission {
device_user_provided: DEVICE_USER_PROVIDED.into(),
device_software_recognised: DEVICE_SOFTWARE_RECOGNISED.into(),
threads: THREADS,
benches: BENCHES.clone(),
submission_type: crate::api::v1::bench::SubmissionType::Wasm,
};
submit_bench(&submit_payload, &campaign, survey_cookie, data.clone()).await;
let msg = PercentileReq {
time: 1,
percentile: 99.00,
};
let resp = test::call_service(
&app,
post_request!(&msg, V1_API_ROUTES.stats.percentile_benches).to_request(),
)
.await;
assert_eq!(resp.status(), StatusCode::OK);
let resp: PercentileResp = test::read_body_json(resp).await;
assert!(resp.difficulty_factor.is_none());
let msg = PercentileReq {
time: 1,
percentile: 100.00,
};
let resp = test::call_service(
&app,
post_request!(&msg, V1_API_ROUTES.stats.percentile_benches).to_request(),
)
.await;
assert_eq!(resp.status(), StatusCode::OK);
let resp: PercentileResp = test::read_body_json(resp).await;
assert!(resp.difficulty_factor.is_none());
let msg = PercentileReq {
time: 2,
percentile: 100.00,
};
let resp = test::call_service(
&app,
post_request!(&msg, V1_API_ROUTES.stats.percentile_benches).to_request(),
)
.await;
assert_eq!(resp.status(), StatusCode::OK);
let resp: PercentileResp = test::read_body_json(resp).await;
assert_eq!(resp.difficulty_factor.unwrap(), 2);
let msg = PercentileReq {
time: 5,
percentile: 90.00,
};
let resp = test::call_service(
&app,
post_request!(&msg, V1_API_ROUTES.stats.percentile_benches).to_request(),
)
.await;
assert_eq!(resp.status(), StatusCode::OK);
let resp: PercentileResp = test::read_body_json(resp).await;
assert_eq!(resp.difficulty_factor.unwrap(), 4);
delete_user(NAME, &data).await;
}
}

View File

@ -1,423 +0,0 @@
// Copyright (C) 2023 Aravinth Manivannan <realaravinth@batsense.net>
// SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use sqlx::types::time::OffsetDateTime;
use sqlx::types::Uuid;
use tokio::fs;
use tokio::io::AsyncWriteExt;
use tokio::sync::oneshot::{self, error::TryRecvError, Sender};
use tokio::task::JoinHandle;
use crate::api::v1::admin::campaigns::runners::get_results;
use crate::api::v1::admin::campaigns::SurveyResponse;
use crate::{errors::ServiceResult, AppData, Settings};
const CAMPAIGN_INFO_FILE: &str = "campaign.json";
const BENCHMARK_FILE: &str = "benchmark.csv";
pub struct Archiver {
base_path: String,
}
pub struct Archive {
now: i64,
base_path: String,
campaign: Uuid,
}
impl Archive {
pub fn new(campaign: Uuid, base_path: String) -> Self {
let now = OffsetDateTime::now_utc().unix_timestamp();
Self {
now,
campaign,
base_path,
}
}
fn campaign_path(&self) -> PathBuf {
Path::new(&self.base_path).join(&self.campaign.to_string())
}
fn archive_path_now(&self) -> PathBuf {
self.campaign_path().join(self.now.to_string())
}
fn campaign_file_path(&self) -> PathBuf {
self.archive_path_now().join(CAMPAIGN_INFO_FILE)
}
fn benchmark_file_path(&self) -> PathBuf {
self.archive_path_now().join(BENCHMARK_FILE)
}
}
impl Archiver {
pub fn new(s: &Settings) -> Self {
Archiver {
base_path: s.publish.dir.clone(),
}
}
async fn create_dir_util(p: &PathBuf) -> ServiceResult<()> {
if p.exists() {
if !p.is_dir() {
fs::remove_file(&p).await.unwrap();
fs::create_dir_all(&p).await.unwrap();
}
} else {
fs::create_dir_all(&p).await.unwrap();
}
Ok(())
}
async fn write_campaign_file(&self, c: &Campaign, a: &Archive) -> ServiceResult<()> {
let archive_path = a.archive_path_now();
Self::create_dir_util(&archive_path).await?;
let campaign_file_path = a.campaign_file_path();
let contents = serde_json::to_string(c).unwrap();
// fs::write(campaign_file_path, contents).await.unwrap();
let mut file = fs::File::create(&campaign_file_path).await.unwrap();
file.write_all(contents.as_bytes()).await.unwrap();
file.flush().await.unwrap();
Ok(())
}
fn get_headers(c: &Campaign) -> Vec<String> {
let mut keys = vec![
"ID".to_string(),
"user".to_string(),
"device_user_provided".to_string(),
"device_software_recognised".to_string(),
"threads".to_string(),
"submitted_at".to_string(),
"submission_type".to_string(),
];
let mut diff_order = Vec::with_capacity(c.difficulties.len());
for d in c.difficulties.iter() {
diff_order.push(d);
keys.push(format!("Difficulty {}", d));
}
keys
}
fn extract_record(c: &Campaign, r: SurveyResponse) -> Vec<String> {
let mut rec = vec![
r.id.to_string(),
r.user.id.to_string(),
r.device_user_provided,
r.device_software_recognised,
r.threads.map_or_else(|| "-".into(), |v| v.to_string()),
r.submitted_at.to_string(),
r.submission_type.to_string(),
];
for d in c.difficulties.iter() {
let bench = r
.benches
.iter()
.find(|b| b.difficulty == *d as i32)
.map_or_else(|| "-".into(), |v| v.duration.to_string());
rec.push(bench);
}
rec
}
async fn write_benchmark_file(
&self,
c: &Campaign,
archive: &Archive,
data: &AppData,
) -> ServiceResult<()> {
let archive_path = archive.archive_path_now();
Self::create_dir_util(&archive_path).await?;
let benchmark_file_path = archive.benchmark_file_path();
struct Username {
name: String,
}
let owner = sqlx::query_as!(
Username,
"SELECT
survey_admins.name
FROM
survey_admins
INNER JOIN survey_campaigns ON
survey_admins.ID = survey_campaigns.user_id
WHERE
survey_campaigns.ID = $1
",
&Uuid::parse_str(&c.id.to_string()).unwrap()
)
.fetch_one(&data.db)
.await?;
let mut page = 0;
let limit = 50;
let file = fs::OpenOptions::new()
.read(true)
.append(true)
.create(true)
.open(&benchmark_file_path)
.await
.unwrap();
let mut wri = csv_async::AsyncWriter::from_writer(file);
let keys = Self::get_headers(c);
wri.write_record(&keys).await.unwrap();
loop {
let mut resp = get_results(
&owner.name,
&Uuid::parse_str(&c.id.to_string()).unwrap(),
data,
page,
limit,
None,
)
.await?;
for r in resp.drain(0..) {
let rec = Self::extract_record(c, r);
wri.write_record(&rec).await.unwrap();
wri.flush().await.unwrap();
}
if resp.len() < limit {
break;
} else {
page += 1
}
}
Ok(())
}
pub async fn init_archive_job(
self,
data: AppData,
) -> ServiceResult<(Sender<bool>, JoinHandle<()>)> {
let (tx, mut rx) = oneshot::channel();
fn can_run(rx: &mut oneshot::Receiver<bool>) -> bool {
match rx.try_recv() {
Err(TryRecvError::Empty) => true,
_ => false,
}
}
let job = async move {
loop {
if !can_run(&mut rx) {
log::info!("Killing archive loop: received signal");
break;
}
for _ in 0..data.settings.publish.duration {
if !can_run(&mut rx) {
log::info!("Killing archive loop: received signal");
break;
}
tokio::time::sleep(std::time::Duration::new(1, 0)).await;
}
let _ = self.archive(&data).await;
}
};
let job_fut = tokio::spawn(job);
Ok((tx, job_fut))
}
pub async fn archive(&self, data: &AppData) -> ServiceResult<()> {
let mut db_campaigns = sqlx::query_as!(
InnerCampaign,
"SELECT ID, name, difficulties, created_at FROM survey_campaigns"
)
.fetch_all(&data.db)
.await?;
for c in db_campaigns.drain(0..) {
let archive = Archive::new(c.id.clone(), self.base_path.clone());
let campaign: Campaign = c.into();
self.write_campaign_file(&campaign, &archive).await?;
self.write_benchmark_file(&campaign, &archive, data).await?;
}
Ok(())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct InnerCampaign {
id: Uuid,
name: String,
difficulties: Vec<i32>,
created_at: OffsetDateTime,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct Campaign {
pub id: uuid::Uuid,
pub name: String,
pub difficulties: Vec<u32>,
pub created_at: i64,
}
impl From<InnerCampaign> for Campaign {
fn from(i: InnerCampaign) -> Self {
Self {
id: uuid::Uuid::parse_str(&i.id.to_string()).unwrap(),
name: i.name,
difficulties: i.difficulties.iter().map(|d| *d as u32).collect(),
created_at: i.created_at.unix_timestamp(),
}
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use csv_async::StringRecord;
use futures::stream::StreamExt;
use crate::api::v1::bench::Submission;
use crate::api::v1::bench::SubmissionType;
use crate::*;
use super::*;
use mktemp::Temp;
#[test]
fn archive_path_works() {
let mut settings = Settings::new().unwrap();
let tmp_dir = Temp::new_dir().unwrap();
settings.publish.dir = tmp_dir.join("base_path").to_str().unwrap().into();
let uuid = Uuid::new_v4();
let archive = Archive::new(uuid.clone(), settings.publish.dir.clone());
let archive_path = archive.archive_path_now();
assert_eq!(
archive_path,
Path::new(&settings.publish.dir)
.join(&uuid.to_string())
.join(&archive.now.to_string())
);
let campaign_file_path = archive.campaign_file_path();
assert_eq!(
campaign_file_path,
Path::new(&settings.publish.dir)
.join(&uuid.to_string())
.join(&archive.now.to_string())
.join(CAMPAIGN_INFO_FILE)
);
let benchmark_file_path = archive.benchmark_file_path();
assert_eq!(
benchmark_file_path,
Path::new(&settings.publish.dir)
.join(&uuid.to_string())
.join(&archive.now.to_string())
.join(BENCHMARK_FILE)
);
}
#[actix_rt::test]
async fn archive_is_correct_test() {
use crate::tests::*;
const NAME: &str = "arciscorrecttesuser";
const EMAIL: &str = "archive_is_correct_testuser@testadminuser.com";
const PASSWORD: &str = "longpassword2";
const DEVICE_USER_PROVIDED: &str = "foo";
const DEVICE_SOFTWARE_RECOGNISED: &str = "Foobar.v2";
const THREADS: i32 = 4;
let data = get_test_data().await;
{
delete_user(NAME, &data).await;
}
//let campaign: Campaign = c.into();
//let archive = Archive::new(campaign.id.clone(), self.base_path.clone());
//self.write_campaign_file(&campaign, &archive).await?;
//self.write_benchmark_file(&campaign, &archive, data).await?;
let (creds, signin_resp) =
register_and_signin(&data, NAME, EMAIL, PASSWORD).await;
let cookies = get_cookie!(signin_resp);
let survey = get_survey_user(data.clone()).await;
let survey_cookie = get_cookie!(survey);
let campaign = create_new_campaign(NAME, data.clone(), cookies.clone()).await;
let campaign_config =
get_campaign_config(&campaign, data.clone(), survey_cookie.clone()).await;
assert_eq!(DIFFICULTIES.to_vec(), campaign_config.difficulties);
let submit_payload = Submission {
device_user_provided: DEVICE_USER_PROVIDED.into(),
device_software_recognised: DEVICE_SOFTWARE_RECOGNISED.into(),
threads: THREADS,
benches: BENCHES.clone(),
submission_type: SubmissionType::Wasm,
};
let _proof =
submit_bench(&submit_payload, &campaign, survey_cookie, data.clone()).await;
let campaign_id = Uuid::from_str(&campaign.campaign_id).unwrap();
let db_campaign = sqlx::query_as!(
InnerCampaign,
"SELECT ID, name, difficulties, created_at FROM survey_campaigns WHERE ID = $1",
campaign_id,
)
.fetch_one(&data.db)
.await.unwrap();
let campaign: Campaign = db_campaign.into();
let archive = Archive::new(
Uuid::parse_str(&campaign.id.to_string()).unwrap(),
data.settings.publish.dir.clone(),
);
let archiver = Archiver::new(&data.settings);
archiver.archive(&AppData::new(data.clone())).await.unwrap();
let contents: Campaign = serde_json::from_str(
&fs::read_to_string(&archive.campaign_file_path())
.await
.unwrap(),
)
.unwrap();
assert_eq!(contents, campaign);
let page = 0;
let limit = 10;
let mut responses = get_results(
NAME,
&campaign_id,
&AppData::new(data.clone()),
page,
limit,
None,
)
.await
.unwrap();
assert_eq!(responses.len(), 1);
let r = responses.pop().unwrap();
let rec = Archiver::extract_record(&campaign, r);
let mut rdr = csv_async::AsyncReader::from_reader(
fs::File::open(archive.benchmark_file_path()).await.unwrap(),
);
let mut records = rdr.records();
assert_eq!(
records.next().await.unwrap().unwrap(),
StringRecord::from(rec)
);
}
}

View File

@ -1,3 +0,0 @@
SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
SPDX-License-Identifier: AGPL-3.0-or-later

View File

@ -1,8 +1,19 @@
// Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
// SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/*
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
//! App data: database connections, etc.
use std::sync::Arc;
use std::thread;
@ -11,17 +22,13 @@ use argon2_creds::{Config, ConfigBuilder, PasswordPolicy};
use sqlx::postgres::PgPoolOptions;
use sqlx::PgPool;
use crate::mcaptcha::*;
use crate::settings::Settings;
use crate::SETTINGS;
/// App data
pub struct Data {
/// database pool
/// databse pool
pub db: PgPool,
pub creds: Config,
pub settings: Settings,
pub mcaptcha: Box<dyn MCaptchaClient>,
}
impl Data {
@ -37,10 +44,7 @@ impl Data {
#[cfg(not(tarpaulin_include))]
/// create new instance of app data
pub async fn new(
settings: Settings,
mcaptcha: Box<dyn MCaptchaClient>,
) -> Arc<Self> {
pub async fn new() -> Arc<Self> {
let creds = Self::get_creds();
let c = creds.clone();
#[allow(unused_variables)]
@ -51,19 +55,14 @@ impl Data {
});
let db = PgPoolOptions::new()
.max_connections(settings.database.pool)
.connect(&settings.database.url)
.max_connections(SETTINGS.database.pool)
.connect(&SETTINGS.database.url)
.await
.expect("Unable to form database pool");
#[cfg(not(debug_assertions))]
init.join().unwrap();
let data = Data {
db,
creds,
settings,
mcaptcha,
};
let data = Data { db, creds };
Arc::new(data)
}

View File

@ -1,91 +0,0 @@
// SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
#[derive(Deserialize, Serialize, Clone, Debug, Eq, PartialEq)]
pub struct JobState {
pub name: String,
}
impl JobState {
pub fn new(name: String) -> Self {
Self { name }
}
}
lazy_static! {
pub static ref JOB_STATE_CREATE: JobState = JobState::new("job.state.create".into());
pub static ref JOB_STATE_FINISH: JobState = JobState::new("job.state.finish".into());
pub static ref JOB_STATE_RUNNING: JobState =
JobState::new("job.state.running".into());
pub static ref JOB_STATES: [&'static JobState; 3] =
[&*JOB_STATE_CREATE, &*JOB_STATE_FINISH, &*JOB_STATE_RUNNING];
}
async fn job_state_exists(
db: &PgPool,
job_state: &JobState,
) -> sqlx::error::Result<bool> {
let res = sqlx::query!(
"SELECT EXISTS (SELECT 1 from survey_mcaptcha_upload_job_states WHERE name = $1)",
job_state.name,
)
.fetch_one(db)
.await?;
let mut resp = false;
if let Some(x) = res.exists {
resp = x;
}
Ok(resp)
}
async fn create_job_states(db: &PgPool) -> sqlx::error::Result<()> {
for j in &*JOB_STATES {
if !job_state_exists(db, j).await? {
sqlx::query!(
"INSERT INTO survey_mcaptcha_upload_job_states
(name) VALUES ($1) ON CONFLICT (name) DO NOTHING;",
j.name
)
.execute(db)
.await?;
}
}
Ok(())
}
pub async fn migrate_db(db: &PgPool) -> sqlx::error::Result<()> {
sqlx::migrate!("./migrations/").run(db).await?;
create_job_states(db).await?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[actix_rt::test]
async fn test_mcaptcha_job_states_exist() {
// can't use crate::tests::get_test_data because this module is used by
// ./src/tests-migrate.rs too, which doesn't load tests module
let settings = crate::settings::Settings::new().unwrap();
let db = sqlx::postgres::PgPoolOptions::new()
.max_connections(2)
.connect(&settings.database.url)
.await
.expect("Unable to form database pool");
migrate_db(&db).await.unwrap();
for e in (*JOB_STATES).iter() {
println!("checking job state {}", e.name);
assert!(job_state_exists(&db, e).await.unwrap());
}
}
}

View File

@ -1,10 +1,22 @@
// Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
// SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/*
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use std::convert::From;
//use actix::MailboxError;
use argon2_creds::errors::CredsError;
use actix_web::{
@ -13,9 +25,11 @@ use actix_web::{
HttpResponse, HttpResponseBuilder,
};
use derive_more::{Display, Error};
//use libmcaptcha::errors::CaptchaError;
use serde::{Deserialize, Serialize};
//use tokio::sync::oneshot::error::RecvError;
#[derive(Debug, Display, PartialEq, Eq, Error)]
#[derive(Debug, Display, PartialEq, Error)]
#[cfg(not(tarpaulin_include))]
pub enum ServiceError {
#[display(fmt = "internal server error")]
@ -26,7 +40,7 @@ pub enum ServiceError {
UsernameTaken,
#[display(
fmt = "This server is is closed for registration. Contact admin if this is unexpected"
fmt = "This server is is closed for registration. Contact admin if this is unexpecter"
)]
ClosedForRegistration,
@ -65,12 +79,8 @@ pub enum ServiceError {
PasswordTooLong,
#[display(fmt = "Passwords don't match")]
PasswordsDontMatch,
#[display(fmt = "Campaign doesn't exist")]
CampaignDoesntExist,
#[display(fmt = "Not a number: only numeral data is accepted")]
NotANumber,
// #[display(fmt = "{}", _0)]
// CaptchaError(CaptchaError),
}
#[derive(Serialize, Deserialize)]
@ -113,13 +123,24 @@ impl ResponseError for ServiceError {
ServiceError::PasswordTooShort => StatusCode::BAD_REQUEST,
ServiceError::PasswordTooLong => StatusCode::BAD_REQUEST,
ServiceError::PasswordsDontMatch => StatusCode::BAD_REQUEST,
ServiceError::CampaignDoesntExist => StatusCode::NOT_FOUND,
ServiceError::NotANumber => StatusCode::BAD_REQUEST,
// ServiceError::CaptchaError(e) => {
// log::error!("{}", e);
// match e {
// CaptchaError::MailboxError => StatusCode::INTERNAL_SERVER_ERROR,
// _ => StatusCode::BAD_REQUEST,
// }
// }
}
}
}
//#[cfg(not(tarpaulin_include))]
//impl From<CaptchaError> for ServiceError {
// fn from(e: CaptchaError) -> ServiceError {
// ServiceError::CaptchaError(e)
// }
//}
//
#[cfg(not(tarpaulin_include))]
impl From<sqlx::Error> for ServiceError {
#[cfg(not(tarpaulin_include))]
@ -143,5 +164,95 @@ impl From<CredsError> for ServiceError {
}
}
//#[cfg(not(tarpaulin_include))]
//impl From<RecvError> for ServiceError {
// #[cfg(not(tarpaulin_include))]
// fn from(e: RecvError) -> Self {
// log::error!("{:?}", e);
// ServiceError::InternalServerError
// }
//}
//#[cfg(not(tarpaulin_include))]
//impl From<MailboxError> for ServiceError {
// #[cfg(not(tarpaulin_include))]
// fn from(e: MailboxError) -> Self {
// log::error!("{:?}", e);
// ServiceError::InternalServerError
// }
//}
#[cfg(not(tarpaulin_include))]
pub type ServiceResult<V> = std::result::Result<V, ServiceError>;
#[derive(Debug, Display, PartialEq, Error)]
#[cfg(not(tarpaulin_include))]
pub enum PageError {
#[display(fmt = "Something weng wrong: Internal server error")]
InternalServerError,
#[display(fmt = "The page you are looking for doesn't exist")]
PageDoesntExist,
#[display(fmt = "{}", _0)]
ServiceError(ServiceError),
}
#[cfg(not(tarpaulin_include))]
impl From<sqlx::Error> for PageError {
#[cfg(not(tarpaulin_include))]
fn from(_: sqlx::Error) -> Self {
PageError::InternalServerError
}
}
#[cfg(not(tarpaulin_include))]
impl From<ServiceError> for PageError {
#[cfg(not(tarpaulin_include))]
fn from(e: ServiceError) -> Self {
PageError::ServiceError(e)
}
}
impl ResponseError for PageError {
fn error_response(&self) -> HttpResponse {
use crate::PAGES;
match self.status_code() {
StatusCode::INTERNAL_SERVER_ERROR => HttpResponse::Found()
.append_header((header::LOCATION, PAGES.errors.internal_server_error))
.finish(),
_ => HttpResponse::Found()
.append_header((header::LOCATION, PAGES.errors.unknown_error))
.finish(),
}
}
#[cfg(not(tarpaulin_include))]
fn status_code(&self) -> StatusCode {
match self {
PageError::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR,
PageError::PageDoesntExist => StatusCode::NOT_FOUND,
PageError::ServiceError(e) => e.status_code(),
}
}
}
#[cfg(not(tarpaulin_include))]
pub type PageResult<V> = std::result::Result<V, PageError>;
#[cfg(test)]
mod tests {
use super::*;
use crate::PAGES;
#[test]
fn error_works() {
let resp: HttpResponse = PageError::InternalServerError.error_response();
assert_eq!(resp.status(), StatusCode::FOUND);
let headers = resp.headers();
assert_eq!(
headers.get(header::LOCATION).unwrap(),
PAGES.errors.internal_server_error
);
}
}

View File

@ -1,12 +1,22 @@
// Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
// SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/*
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use std::env;
use std::sync::Arc;
use actix_files::Files;
use actix_identity::{CookieIdentityPolicy, IdentityService};
use actix_session::{storage::CookieSessionStore, SessionMiddleware};
use actix_web::{
@ -17,11 +27,8 @@ use lazy_static::lazy_static;
use log::info;
mod api;
mod archive;
mod data;
mod db;
mod errors;
mod mcaptcha;
mod pages;
mod settings;
mod static_assets;
@ -31,25 +38,41 @@ mod tests;
pub use crate::data::Data;
pub use api::v1::ROUTES as V1_API_ROUTES;
pub use pages::routes::PAGES;
pub use pages::routes::ROUTES as PAGES;
pub use settings::Settings;
pub use static_assets::static_files::assets;
use static_assets::FileMap;
lazy_static! {
pub static ref SETTINGS: Settings = Settings::new().unwrap();
pub static ref FILES: FileMap = FileMap::new();
pub static ref CSS: &'static str =
FILES.get("./static/cache/bundle/css/main.css").unwrap();
pub static ref MOBILE_CSS: &'static str =
FILES.get("./static/cache/bundle/css/mobile.css").unwrap();
pub static ref JS: &'static str =
FILES.get("./static/cache/bundle/bundle.js").unwrap();
pub static ref GLUE: &'static str =
FILES.get("./static/cache/bundle/glue.js").unwrap();
}
pub const DOWNLOAD_SCOPE: &str = "/download";
/// points to source files matching build commit
pub static ref SOURCE_FILES_OF_INSTANCE: String = {
let mut url = SETTINGS.source_code.clone();
if !url.ends_with('/') {
url.push('/');
}
let mut base = url::Url::parse(&url).unwrap();
base = base.join("tree/").unwrap();
base = base.join(GIT_COMMIT_HASH).unwrap();
base.into()
};
}
pub const CACHE_AGE: u32 = 604800;
@ -65,9 +88,7 @@ pub type AppData = actix_web::web::Data<Arc<crate::data::Data>>;
#[cfg(not(tarpaulin_include))]
#[actix_web::main]
async fn main() -> std::io::Result<()> {
if env::var("RUST_LOG").is_err() {
env::set_var("RUST_LOG", "info");
}
env::set_var("RUST_LOG", "info");
pretty_env_logger::init();
@ -76,24 +97,11 @@ async fn main() -> std::io::Result<()> {
PKG_NAME, PKG_DESCRIPTION, PKG_HOMEPAGE, VERSION, GIT_COMMIT_HASH
);
let settings = Settings::new().unwrap();
let mcaptcha: Box<dyn mcaptcha::MCaptchaClient> =
Box::new(mcaptcha::MCaptchaClientReqwest::default());
let data = Data::new(settings.clone(), mcaptcha).await;
db::migrate_db(&data.db).await.unwrap();
let data = Data::new().await;
sqlx::migrate!("./migrations/").run(&data.db).await.unwrap();
let data = actix_web::web::Data::new(data);
let arch = archive::Archiver::new(&data.settings);
let (archive_kiler, archive_job) =
arch.init_archive_job(data.clone()).await.unwrap();
let mcaptcha_downloader = mcaptcha::MCaptchaDownloader::new(data.clone());
let (mcaptcha_downloader_killer, mcaptcha_downloader_job) =
mcaptcha_downloader.start_job().await.unwrap();
let ip = settings.server.get_ip();
println!("Starting server on: http://{}", ip);
println!("Starting server on: http://{}", SETTINGS.server.get_ip());
HttpServer::new(move || {
App::new()
@ -102,29 +110,20 @@ async fn main() -> std::io::Result<()> {
.app_data(get_json_err())
.wrap(
actix_middleware::DefaultHeaders::new()
.add(("Permissions-Policy", "interest-cohort=()")),
.header("Permissions-Policy", "interest-cohort=()"),
)
.wrap(get_survey_session(&settings))
.wrap(get_identity_service(&settings))
.wrap(get_survey_session())
.wrap(get_identity_service())
.wrap(actix_middleware::NormalizePath::new(
actix_middleware::TrailingSlash::Trim,
))
.service(
Files::new(DOWNLOAD_SCOPE, &settings.publish.dir).show_files_listing(),
)
.configure(services)
.app_data(data.clone())
})
.bind(ip)
.bind(SETTINGS.server.get_ip())
.unwrap()
.run()
.await
.unwrap();
let _ = mcaptcha_downloader_killer.send(());
let _ = archive_kiler.send(true);
let _ = tokio::join!(archive_job, mcaptcha_downloader_job);
Ok(())
}
#[cfg(not(tarpaulin_include))]
@ -136,14 +135,12 @@ pub fn get_json_err() -> JsonConfig {
}
#[cfg(not(tarpaulin_include))]
pub fn get_survey_session(
settings: &Settings,
) -> actix_session::SessionMiddleware<CookieSessionStore> {
pub fn get_survey_session() -> actix_session::SessionMiddleware<CookieSessionStore> {
use actix_web::cookie::Key;
let cookie_secret = &settings.server.cookie_secret2;
let cookie_secret = &SETTINGS.server.cookie_secret2;
let key = Key::from(cookie_secret.as_bytes());
SessionMiddleware::builder(CookieSessionStore::default(), key)
.cookie_domain(Some(settings.server.domain.clone()))
.cookie_domain(Some(SETTINGS.server.domain.clone()))
.cookie_name("survey-id".into())
.cookie_path("/".to_string())
.cookie_secure(false)
@ -152,16 +149,14 @@ pub fn get_survey_session(
}
#[cfg(not(tarpaulin_include))]
pub fn get_identity_service(
settings: &Settings,
) -> IdentityService<CookieIdentityPolicy> {
let cookie_secret = &settings.server.cookie_secret;
pub fn get_identity_service() -> IdentityService<CookieIdentityPolicy> {
let cookie_secret = &SETTINGS.server.cookie_secret;
IdentityService::new(
CookieIdentityPolicy::new(cookie_secret.as_bytes())
.path("/admin/")
.name("survey-admin-auth")
.max_age_secs(60 * 60 * 24 * 365)
.domain(&settings.server.domain)
.domain(&SETTINGS.server.domain)
.secure(false),
)
}

View File

@ -1,303 +0,0 @@
// Copyright (C) 2023 Aravinth Manivannan <realaravinth@batsense.net>
// SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use std::time::Duration;
use async_trait::async_trait;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use tokio::sync::oneshot;
use tokio::task::JoinHandle;
use tokio::time::sleep;
use url::Url;
use crate::{api::v1::mcaptcha::db::SchedulerJob, errors::*, AppData};
/* TODO:
* 1. Define traits to interact with mCaptcha
* 2. Implement trait with request 3. Implement mocking for testing
* 4. Load to crate::data::Data
*/
#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq)]
/// Proof-of-Work CAPTCHA performance analytics
pub struct PerformanceAnalytics {
/// log ID
pub id: usize,
/// time taken to generate proof
pub time: u32,
/// difficulty factor for which the proof was generated
pub difficulty_factor: u32,
/// worker/client type: wasm, javascript, python, etc.
pub worker_type: String,
}
#[async_trait]
pub trait MCaptchaClient:
std::marker::Send + std::marker::Sync + CloneMCaptchaClient
{
async fn share_secret(
&self,
mut mcaptcha: Url,
secret: String,
auth_token: String,
) -> ServiceResult<()>;
async fn download_benchmarks(
&self,
mut mcaptcha: Url,
campaign_id: &str,
page: usize,
) -> ServiceResult<Vec<PerformanceAnalytics>>;
}
/// Trait to clone MCaptchaClient
pub trait CloneMCaptchaClient {
/// clone client
fn clone_client(&self) -> Box<dyn MCaptchaClient>;
}
impl<T> CloneMCaptchaClient for T
where
T: MCaptchaClient + Clone + 'static,
{
fn clone_client(&self) -> Box<dyn MCaptchaClient> {
Box::new(self.clone())
}
}
impl Clone for Box<dyn MCaptchaClient> {
fn clone(&self) -> Self {
(**self).clone_client()
}
}
#[derive(Clone)]
pub struct MCaptchaClientReqwest {
client: Client,
}
impl Default for MCaptchaClientReqwest {
fn default() -> Self {
Self {
client: Client::new(),
}
}
}
#[async_trait]
impl MCaptchaClient for MCaptchaClientReqwest {
async fn share_secret(
&self,
mut mcaptcha: Url,
secret: String,
auth_token: String,
) -> ServiceResult<()> {
#[derive(Serialize)]
struct S {
secret: String,
auth_token: String,
}
let msg = S { secret, auth_token };
mcaptcha.set_path("/api/v1/survey/secret");
self.client.post(mcaptcha).json(&msg).send().await.unwrap();
Ok(())
}
async fn download_benchmarks(
&self,
mut mcaptcha: Url,
campaign_id: &str,
page: usize,
) -> ServiceResult<Vec<PerformanceAnalytics>> {
mcaptcha.set_path(&format!("/api/v1/survey/takeout/{campaign_id}/get"));
mcaptcha.set_query(Some(&format!("page={page}")));
let res = self
.client
.get(mcaptcha)
.send()
.await
.unwrap()
.json()
.await
.unwrap();
Ok(res)
}
}
#[derive(Clone)]
pub struct MCaptchaDownloader {
data: AppData,
}
impl MCaptchaDownloader {
pub fn new(data: AppData) -> Self {
Self { data }
}
fn can_run(rx: &mut oneshot::Receiver<()>) -> bool {
matches!(rx.try_recv(), Err(oneshot::error::TryRecvError::Empty))
}
pub async fn start_job(
&self,
) -> ServiceResult<(oneshot::Sender<()>, JoinHandle<()>)> {
let (tx, mut rx) = oneshot::channel();
let this = self.clone();
let fut = async move {
loop {
if !Self::can_run(&mut rx) {
log::info!("stopping survey uploads");
break;
}
let task = this.data.get_next_job_to_run().await.unwrap();
if task.is_none() {
for _ in 0..5 {
if !Self::can_run(&mut rx) {
log::info!("Stopping survey uploads");
break;
}
sleep(Duration::new(1, 0)).await;
}
continue;
}
let task = task.unwrap();
this.data.mark_job_scheduled(&task).await.unwrap();
this.exec_job(&task, &mut rx).await.unwrap();
}
};
let handle = tokio::spawn(fut);
Ok((tx, handle))
}
async fn exec_job(
&self,
job: &SchedulerJob,
rx: &mut oneshot::Receiver<()>,
) -> ServiceResult<()> {
let checkpoint = self.data.mcaptcha_get_checkpoint(&job.campaign_id).await?;
const LIMIT: usize = 50;
let mut page = 1 + (checkpoint / LIMIT);
let campaign_str = job.campaign_id.to_string();
log::info!("getting page {page} from {campaign_str}");
loop {
if !Self::can_run(rx) {
log::info!("Stopping survey downloads");
break;
}
let mut res = self
.data
.mcaptcha
.download_benchmarks(job.url.clone(), &campaign_str, page)
.await?;
if !Self::can_run(rx) {
log::info!("Stopping survey downloads");
break;
}
let skip = checkpoint - ((page - 1) * LIMIT);
let new_records = res.len() - skip as usize;
let mut skip = skip as isize;
for r in res.drain(0..) {
if skip > 0 {
skip -= 1;
continue;
}
self.data
.mcaptcha_insert_analytics(&job.campaign_id, &r)
.await?;
}
self.data
.mcaptcha_set_checkpoint(&job.campaign_id, new_records)
.await?;
if !Self::can_run(rx) {
log::info!("Stopping survey downloads");
break;
}
page += 1;
if res.len() < LIMIT {
break;
}
}
self.data.mark_job_finished(job).await.unwrap();
Ok(())
}
}
#[cfg(test)]
pub mod tests {
use super::*;
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use lazy_static::lazy_static;
lazy_static! {
pub static ref BENCHMARK: Vec<PerformanceAnalytics> = vec![
PerformanceAnalytics {
id: 1,
time: 2,
difficulty_factor: 3,
worker_type: "foo".to_string(),
},
PerformanceAnalytics {
id: 4,
time: 5,
difficulty_factor: 6,
worker_type: "bar".to_string(),
},
];
}
#[derive(Clone)]
pub struct TestClient {
pub client: Arc<RwLock<HashMap<String, String>>>,
}
impl Default for TestClient {
fn default() -> Self {
Self {
client: Arc::new(RwLock::new(HashMap::default())),
}
}
}
#[async_trait]
impl MCaptchaClient for TestClient {
async fn share_secret(
&self,
mut mcaptcha: Url,
secret: String,
auth_token: String,
) -> ServiceResult<()> {
mcaptcha.set_path("/api/v1/survey/secret");
let mut x = self.client.write().unwrap();
x.insert(mcaptcha.to_string(), secret);
drop(x);
Ok(())
}
async fn download_benchmarks(
&self,
mcaptcha: Url,
campaign_id: &str,
page: usize,
) -> ServiceResult<Vec<PerformanceAnalytics>> {
println!(
"mcaptcha_url {}, campaign_id {}, page: {page}",
mcaptcha, campaign_id
);
let res = BENCHMARK.clone();
Ok(res)
}
}
}

Some files were not shown because too many files have changed in this diff Show More