1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
pub mod check {
    //! # Data extraxion
    //! Getting data out of the system
    //! 
    //! ## Configuration
    //! ### Leaderboard
    //! 
    //! Both the defaults and maxes are changeable from the defaults presented below via the command line.
    //! 
    //! ```toml
    //! [board.default]
    //! count = 10
    //! ordering = 'best_to_worst'
    //! 
    //! [board.max]
    //! count = 42
    //! ordering = 'best_to_worst'
    //! 
    //! 
    //! [person.default]
    //! count = 3
    //! ordering = 'best_to_worst'
    //! 
    //! [person.max]
    //! count = 10
    //! ordering = 'best_to_worst'
    //! ```
    //! 
    //! Both `default` and `max` are a [Strict Leaderboard Config](#leaderboard-config).
    //! 
    //! #### Leaderboard Config
    //! 
    //! The Leaderboard Config consists of two keys:
    //!   * `count` – `usize` – how many entries to return
    //!   * `ordering` – [`SolutionOrdering`](#solution-ordering) – how to order the returned entries
    //! 
    //! Leaderboard Configs are said to be Strict if they require all keys to be present.
    //! Otherwise they are Loose, and unspecified keys are filled in from the loaded defaults.
    //! 
    //! #### Solution Ordering
    //! 
    //! Any of:
    //!   * `best_to_worst`
    //!   * `worst_to_best`
    //! 
    //! #### Leaderboard Selector
    //! 
    //! Any of:
    //!   * `solutions`
    //!   * `users`
    //! 
    //! ### Activity
    //! 
    //! A session is considered active if last activity thereon occurred at most the specified duration before *now*.
    //! 
    //! Data extraxion requests do *not* count towards activity.
    //! 
    //! ## Retrieval
    //! ### Leaderboard
    //! 
    //! The request query is a Loose Leaderboard Config, with another optional `of` key of type [Leaderboard Selector](#leaderboard-selector).
    //! 
    //! That config is then clamped to the loaded max.
    //! 
    //! Depending on the `of` key value (or `solutions` by default), the response is an array of:
    //!   * [Solution messages](sudoku.md#solution-message), if `solutions`, or
    //!   * [Sanitised user data](user.md#sanitised-user-data), if `users`.
    //! 
    //! ### Activity
    //! 
    //! Request empty, returns a `usize`.

}

pub mod errors {
    //! # Errors
    //! Requests can fail, here's how they can fail.
    //! 
    //! ## Generic error
    //! 
    //! Used for most non-specific errors.
    //! 
    //! `reason` should be in an all-lowercase past-tense finishing-punctuation-free form (e.g. "failed to apply diff", "user with that name exists"),
    //!   `severity` is an enum, wherein `"warning"` indicates a non-fatal error, which is usually a user's fault, and `"danger"` indicates a potentially-fatal,
    //!   usually internal error or a malicious request.
    //! 
    //! ```json
    //! {
    //!     "reason": "string",
    //!     "severity": "warning | danger"
    //! }
    //! ```
    //! 
    //! ## Login error
    //! 
    //! <sub>&lt;secure&gt;</sub>
    //! 
    //! ```json
    //! {}
    //! ```

}

pub mod session {
    //! # Session management
    //! Cookie encrypted with secure key [as per `rocket` spec](https://api.rocket.rs/rocket/http/enum.Cookies.html#method.add_private).
    //! 
    //! Cookie data is an `INTEGER`, which is a `PRIMARY KEY` in the `sessions` table.
    //! 
    //! Cookie expiry date is set to 1 day in the future in UTC as determined by `chrono::Utc::now() + chrono::Duration::days(1)` without sub-second precision.
    //! 
    //! Cookie name is `"session_id"`.
    //! 
    //! See [`user.md`](user.md) for user and login details.
    //! 
    //! ## SQL table def
    //! 
    //! ```sql
    //! CREATE TABLE IF NOT EXISTS sessions (
    //!     id              INTEGER PRIMARY KEY ASC,                -- Unique session ID
    //!     expiry          DATETIME NOT NULL,                      -- Expiry datetime in RFC3339 format
    //!     is_admin        BOOLEAN NOT NULL DEFAULT 0,             -- Whether the user has authenticated as administrator
    //!     user_id         INTEGER REFERENCES users (id),          -- ID of user session is logged in as
    //! 
    //!     sudoku_board_id INTEGER REFERENCES sudoku_boards (id),  -- ID of board currently being solved
    //!     board_skeleton  TEXT,                                   -- The board skeleton sent to the user
    //!     solve_start     DATETIME,                               -- Time the solving started
    //! 
    //!     CHECK ((board_skeleton IS NULL) OR (LENGTH(board_skeleton) == 9 * 9))
    //! );
    //! ```

}

pub mod sudoku {
    //! # Sudoku processing
    //! How is sudoku formed?
    //! 
    //! ## Relevant data
    //! 
    //! A Sudoku board consists of the following:
    //! 
    //! ```sql
    //! CREATE TABLE IF NOT EXISTS sudoku_boards (
    //!     id            INTEGER PRIMARY KEY ASC,                                               -- Unique board ID
    //!     full_board    TEXT NOT NULL UNIQUE,                                                  -- The full "solved" board repr
    //!     difficulty    INTEGER NOT NULL,                                                      -- Board "difficulty", between one and three
    //!     creation_time DATETIME NOT NULL,                                                     -- Time the board was generated
    //! 
    //!     CHECK (((difficulty >= 1) AND (difficulty <= 3)) AND (LENGTH(full_board) == 9 * 9))
    //! );
    //! ```
    //! 
    //! ### Scoring formula
    //! 
    //! After game validates, points are awarded accordingly:
    //! 
    //! ```c
    //! point_count = difficulty * 50000 / (solve_time + 30))
    //! ```
    //! 
    //! Where `solve_time` is in seconds (but without the unit) and `difficulty ϵ {1, 2, 3}`.
    //! 
    //! ## Board transserialisation
    //! 
    //! Given the following sudoku board:
    //! 
    //! ```plaintext
    //! 5 | 3 | 4 || 6 | 7 | 8 || 9 | 1 |
    //! 6 | 7 |   || 1 | 9 | 5 || 3 | 4 | 8
    //! 1 | 9 | 8 || 3 | 4 | 2 || 5 | 6 | 7
    //! ———————————————————————————————————
    //! 8 | 5 | 9 || 7 | 6 | 1 || 4 | 2 | 3
    //! 4 |   | 6 ||   | 5 | 3 || 7 |   | 1
    //! 7 | 1 | 3 || 9 | 2 | 4 || 8 | 5 | 6
    //! ———————————————————————————————————
    //! 9 | 6 | 1 || 5 | 3 | 7 || 2 | 8 | 4
    //!   | 8 | 7 || 4 |   | 9 || 6 | 3 | 5
    //! 3 | 4 | 5 || 2 | 8 | 6 ||   | 7 | 9
    //! ```
    //! 
    //! Equivalently:
    //! 
    //! ```plaintext
    //! 53467891.
    //! 67.195348
    //! 198342567
    //! 859761423
    //! 4.6.537.1
    //! 713924856
    //! 961537284
    //! .874.9635
    //! 345286.79
    //! ```
    //! 
    //! Effectively:
    //! 
    //! ```plaintext
    //! 53467891.67.1953481983425678597614234.6.537.1713924856961537284.874.9635345286.79
    //! ```
    //! 
    //! ## Workflow
    //! 
    //! Au première, get the skeleton board with of your preferred difficulty by specifying `?difficulty=<diff>`, where `diff` is within [`difficulty`'s domain](#scoring-formula).
    //! 
    //! Thereafter, submit the solved board as a form as seen below and get the rating.
    //! 
    //! ### Board message
    //! 
    //! ```json
    //! {
    //!   "board_id": "number",
    //!   "board_skeleton": "string, last form under #board-transserialisation",
    //! 
    //!   "solved_board": "string, last form under #board-transserialisation"  // Only present when submitting a board solve
    //! }
    //! ```
    //! 
    //! ### Solution message
    //! 
    //! Effectively the row from [below](#leaderboards):
    //! 
    //! ```json
    //! {
    //!   "id": "number",
    //!   "display_name": "string",
    //!   "board_id": "number",
    //!   "skeleton": "string",
    //!   "difficulty": "number",
    //!   "solution_duration_secs": "number",
    //!   "score": "number",
    //!   "solution_time": "RFC3339 (string)",
    //! }
    //! ```
    //! 
    //! If user not logged in, username is randomised.
    //! 
    //! ## Leaderboards
    //! 
    //! Or, well, just a list of solutions, because what's the difference.
    //! 
    //! ```sql
    //! CREATE TABLE IF NOT EXISTS sudoku_solutions (
    //!     id                     INTEGER PRIMARY KEY ASC,                                                     -- Unique solution ID
    //!     display_name           TEXT NOT NULL,                                                               -- Solver's display name
    //!     board_id               INTEGER NOT NULL REFERENCES sudoku_boards (id),                              -- The solved board ID
    //!     skeleton               TEXT NOT NULL,                                                               -- The solved board skeleton
    //!     difficulty             INTEGER NOT NULL,                                                            -- Board "difficulty", between one and three
    //!     solution_duration_secs INTEGER NOT NULL,                                                            -- Time in seconds taken to achieve the solution
    //!     score                  INTEGER NOT NULL,                                                            -- Score achieved for the solve
    //!     solution_time          DATETIME NOT NULL,                                                           -- Time the solution occured at
    //! 
    //!     CHECK (((difficulty >= 1) AND (difficulty <= 3)) AND (solution_duration_secs > 0) AND (score > 0))
    //! );
    //! ```

}

pub mod user {
    //! # User list
    //! Users contain identifying data, status (admin/not), and points.
    //! 
    //! ## Authentication
    //! ### Client-side
    //! When the user presses "Log in" on the login page, the plaintext password is key-derived with [`scrypt`](https://github.com/ricmoo/scrypt-js).
    //! 
    //! The entered password is not normalised in any way. The arguments passed to the client-side `scrypt` function are
    //! 
    //! parameter |      value     | comment                                                                                                              |
    //! ----------|----------------|----------------------------------------------------------------------------------------------------------------------|
    //! `N`       | 2<sup>14</sup> | Recommended for passwords                                                                                            |
    //! `r`       | 8              | "Optimum value" specified in [paper](http://www.tarsnap.com/scrypt/scrypt.pdf) (page 12)                             |
    //! `p`       | 1              | "Optimum value" specified in [paper](http://www.tarsnap.com/scrypt/scrypt.pdf) (page 12)                             |
    //! `dkLen`   | 64             |                                                                                                                      |
    //! `salt`    | "Sudoku"       | That's what the project is called, and, honestly, it doesn't need to be *that* secure, just preferably not plaintext |
    //! 
    //! Nota bene: these values **must** be consistent once and for all, as other values will create different hashes, which will in turn create different passwords.
    //! 
    //! That value is then hex-string-encoded (case irrelevant), a JSON-stringified form is constructed, then base64-encoded as "data" and that is sent in a form.
    //! 
    //! In other words, with `data` being the key and value, and doubling as <span id="user-login-data">User Login Data</span> (all keys `string`s):
    //! ```js
    //! let data = base64(JSON.stringify({
    //!     username: raw_username,
    //!     email: raw_email, // See note below
    //!     password: scrypt(raw_password, /* With ^ params */)
    //! }));
    //! ```
    //! 
    //! The `email` key *shall only be present* for user registration – the form will be rejected if it contains an email and is used to log in;
    //!   *vice versa* – the registration request will be rejected if it doesn't contain the `email` key.
    //! 
    //! ### Server-side verification
    //! If a user with the specified username exists in the database,
    //! the server lower-cases the key-derived password, then compares it to the stored value.
    //! 
    //! Otherwise, the server shall return the same value as with above with unmatched password.
    //! 
    //! The returned status shall be `202 Accepted` on correct verification
    //!                          and `401 Unauthorized` otherwise.
    //! 
    //! The returned value shall be [Sanitised User data](#sanitised-user-data) on correct verification,
    //!                         and [Login Error](errors.md#login-error) otherwise.
    //! 
    //! The returned bundle shall *not* distinguish between any two cases of incorrect verification.
    //! 
    //! ### Server-side entry creation
    //! If a user with the specified username or e-mail doesn't exist in the database,
    //! the server lower-cases the key-derived password, then derives it *again* with parameters as guessed via `util::SCRYPT_IDEAL_PARAMS`.
    //! Finally, the server stores that username, e-mail, and
    //! doubly-derived password in the [`rscrypt format`](https://docs.rs/rust-crypto/0.2.36/crypto/scrypt/fn.scrypt_simple.html#format) in the `users` table.
    //! 
    //! The returned status shall be `201 Created` on correct creation,
    //!                              `409 Conflict` if a user with that name/e-mail already exists,
    //!                          and an otherwise implementation-defined relevant status on other errors.
    //! 
    //! The returned value shall be [Sanitised User data](#sanitised-user-data) on correct creation,
    //!                          an appropriately filled out [Generic Error](errors.md#generic-error) if a user with that name/e-mail already exists,
    //!                      and an otherwise implementation-defined relevant [Error](errors.md) on other errors.
    //! 
    //! ### Logging out
    //! Independently of whether the user is logged in, the server shall return `204 No Content`.
    //! 
    //! ## SQL table def
    //! 
    //! ```sql
    //! CREATE TABLE IF NOT EXISTS users (
    //!     id                 INTEGER PRIMARY KEY ASC,     -- Unique user ID
    //!     username           TEXT NOT NULL UNIQUE,        -- User's name or "login" or whatever you want to call it
    //!     password           TEXT NOT NULL,               -- Doubly scrypted password text, see above.
    //!     email              TEXT NOT NULL UNIQUE,        -- User's contact e-mail
    //!     created_at         DATETIME NOT NULL,           -- Time user was created
    //!     is_admin           BOOLEAN NOT NULL DEFAULT 0,  -- Whether the user has administrative privileges
    //!     points_total       INTEGER NOT NULL DEFAULT 0,  -- Sum total of the user's points, calculated according to sudoku.md#scoring-formula, non-negative
    //!     games_total        INTEGER NOT NULL DEFAULT 0,  -- Total amount of games played, non-negative
    //!     games_total_easy   INTEGER NOT NULL DEFAULT 0,  -- Amount of easy games played, non-negative
    //!     games_total_medium INTEGER NOT NULL DEFAULT 0,  -- Amount of medium games played, non-negative
    //!     games_total_hard   INTEGER NOT NULL DEFAULT 0,  -- Amount of hard games played, non-negative
    //! 
    //!     CHECK ((points_total >= 0) AND (games_total >= 0) AND (games_total_easy >= 0) AND (games_total_medium >= 0) AND (games_total_hard >= 0))
    //! );
    //! ```
    //! 
    //! ## Sanitised User data
    //! 
    //! ```json
    //! {
    //!     "username":           "string",
    //!     "created_at":         "RFC3339 (string)",
    //!     "is_admin":           "boolean",
    //!     "points_total":       "number",
    //!     "games_total":        "number",
    //!     "games_total_easy":   "number",
    //!     "games_total_medium": "number",
    //!     "games_total_hard":   "number"
    //! }
    //! ```
    //! 
    //! ## Admins
    //! 
    //! Admins are assigned manually, where `$1` is the username whose adminness to set:
    //! 
    //! <!-- no_run -->
    //! 
    //! ```sql
    //! UPDATE users
    //!     SET is_admin = 1
    //!     WHERE username = "$1";
    //! ```
    //! 
    //! Other keys will of course, per analogiam, work.

}