| | 1 | = Use-case 0001 Implementation - Register new account = |
| | 2 | |
| | 3 | '''Initiating actor:''' Visitor |
| | 4 | |
| | 5 | '''Other actors:''' — |
| | 6 | |
| | 7 | A new person creates an account on !EduBerza so that they can later log in as a |
| | 8 | Trader ([wiki:UseCase0002Implementation UseCase0002]). The Visitor enters a |
| | 9 | username, an e-mail address, a full name and a password. The system validates the |
| | 10 | input (required fields, an `@` in the e-mail, a password of at least 6 characters), |
| | 11 | refuses a username or e-mail that is already registered, and stores only a SHA-256 |
| | 12 | hash of the password, never the password itself. A new account starts with a cash |
| | 13 | balance of 0 USD; money is added later with a deposit |
| | 14 | ([wiki:UseCase0003Implementation UseCase0003]). |
| | 15 | |
| | 16 | Original use-case description (P3): [wiki:UseCase0001]. |
| | 17 | Implementation: `server/auth.go`, function `Register` (the password hash is computed |
| | 18 | by `hashPassword` in the same file; the code is shown at the end of this page). |
| | 19 | |
| | 20 | == Scenario == |
| | 21 | |
| | 22 | 1. '''Visitor''' chooses `[1] Register` in the anonymous menu (types `1`). |
| | 23 | 2. '''System''' prints `-- Register --` and asks, one prompt after another, for |
| | 24 | `Username:`, `Email:`, `Full name:` and `Password (min 6 chars):`. |
| | 25 | |
| | 26 | The screenshot shows steps 1–2: option `1` is chosen and the first prompt |
| | 27 | (`Username:`) is waiting for input. |
| | 28 | |
| | 29 | [[Image(uc0001_1_register.png)]] |
| | 30 | |
| | 31 | 3. '''Visitor''' enters the values: `marko`, `marko@example.com`, `Marko Markovski`, |
| | 32 | `secret1`. |
| | 33 | 4. '''System''' validates the input in Go, without accessing the database: |
| | 34 | * username, e-mail and password must be non-empty, otherwise it prints |
| | 35 | `Username, email and password are required.` and the scenario ends; |
| | 36 | * the e-mail must contain `@` (see alternate flow 3a); |
| | 37 | * the password must be at least 6 characters long, otherwise it prints |
| | 38 | `Password must be at least 6 characters.` and the scenario ends. |
| | 39 | 5. '''System''' checks whether the username or the e-mail already exists |
| | 40 | (`$1` = username, `$2` = e-mail): |
| | 41 | |
| | 42 | {{{ |
| | 43 | SELECT EXISTS(SELECT 1 FROM users WHERE username = $1 OR email = $2) |
| | 44 | }}} |
| | 45 | |
| | 46 | If the result is `true`, alternate flow 5a applies. |
| | 47 | |
| | 48 | 6. '''System''' creates the account (`$1` = username, `$2` = e-mail, `$3` = full name, |
| | 49 | `$4` = password hash). The hash is computed in Go by `hashPassword` as the |
| | 50 | hex-encoded SHA-256 of the password — the same value that P3's |
| | 51 | `encode(digest($4, 'sha256'), 'hex')` would produce in SQL. Hashing on the Go side |
| | 52 | keeps it identical to the check done at login (SQL as in the code, only the Go |
| | 53 | source indentation removed): |
| | 54 | |
| | 55 | {{{ |
| | 56 | INSERT INTO users (username, email, full_name, password_hash, available_balance) |
| | 57 | VALUES ($1, $2, $3, $4, 0) |
| | 58 | }}} |
| | 59 | |
| | 60 | 7. '''System''' prints `Account created. You can now log in.` and returns to the |
| | 61 | anonymous menu; the Visitor can continue with |
| | 62 | [wiki:UseCase0002Implementation UseCase0002]. |
| | 63 | |
| | 64 | The screenshot shows steps 3–7 of the successful attempt (bottom half): the |
| | 65 | entered values, the confirmation and the anonymous menu again. The top half is the |
| | 66 | earlier rejected attempt from alternate flow 3a. |
| | 67 | |
| | 68 | [[Image(uc0001_3_7_created.png)]] |
| | 69 | |
| | 70 | All statements are run on the `project` schema: the connection sets |
| | 71 | `search_path=project,public` (`server/db/db.go`), so `users` means `project.users`. |
| | 72 | |
| | 73 | === Alternate flow 3a — invalid e-mail === |
| | 74 | |
| | 75 | In step 3 the Visitor entered `marko.example.com` (no `@`). The check in step 4 |
| | 76 | fails, the system prints `Invalid email.` and no SQL statement is executed. In the |
| | 77 | prototype the system then shows the anonymous menu again and the Visitor chooses |
| | 78 | `[1] Register` once more, which returns the scenario to step 2. |
| | 79 | |
| | 80 | [[Image(uc0001_3a_invalid_email.png)]] |
| | 81 | |
| | 82 | === Alternate flow 5a — duplicate username or e-mail === |
| | 83 | |
| | 84 | After `marko` has been created, the Visitor tries to register again with username |
| | 85 | `marko`, e-mail `other@example.com`, full name `Marko Two`, password `secret2`. The |
| | 86 | query from step 5 (`$1` = `marko`, `$2` = `other@example.com`) returns `true` |
| | 87 | because the username is taken, so the system prints |
| | 88 | `Username or email already taken.`, does not run the `INSERT`, and the scenario |
| | 89 | ends in the anonymous menu. |
| | 90 | |
| | 91 | [[Image(uc0001_5a_duplicate.png)]] |
| | 92 | |
| | 93 | == How to reproduce == |
| | 94 | |
| | 95 | {{{ |
| | 96 | ./eduberza -init # optional: reset to a known state |
| | 97 | ./eduberza |
| | 98 | # [1] Register: marko / marko.example.com / Marko Markovski / secret1 -> Invalid email. |
| | 99 | # [1] Register: marko / marko@example.com / Marko Markovski / secret1 -> Account created. |
| | 100 | # [1] Register: marko / other@example.com / Marko Two / secret2 -> Username or email already taken. |
| | 101 | }}} |
| | 102 | |
| | 103 | All three screenshots come from one real run of exactly these inputs. |
| | 104 | |
| | 105 | == Source code == |
| | 106 | |
| | 107 | `server/auth.go` — `hashPassword` and `Register`: |
| | 108 | |
| | 109 | {{{ |
| | 110 | func hashPassword(pw string) string { |
| | 111 | sum := sha256.Sum256([]byte(pw)) |
| | 112 | return hex.EncodeToString(sum[:]) |
| | 113 | } |
| | 114 | |
| | 115 | // Register - UC0001 |
| | 116 | func Register() { |
| | 117 | fmt.Println("\n-- Register --") |
| | 118 | username := prompt("Username: ") |
| | 119 | email := prompt("Email: ") |
| | 120 | fullName := prompt("Full name: ") |
| | 121 | pw := prompt("Password (min 6 chars): ") |
| | 122 | |
| | 123 | if username == "" || email == "" || pw == "" { |
| | 124 | fmt.Println("Username, email and password are required.") |
| | 125 | return |
| | 126 | } |
| | 127 | if !strings.Contains(email, "@") { |
| | 128 | fmt.Println("Invalid email.") |
| | 129 | return |
| | 130 | } |
| | 131 | if len(pw) < 6 { |
| | 132 | fmt.Println("Password must be at least 6 characters.") |
| | 133 | return |
| | 134 | } |
| | 135 | |
| | 136 | var exists bool |
| | 137 | err := db.DB.QueryRow( |
| | 138 | `SELECT EXISTS(SELECT 1 FROM users WHERE username = $1 OR email = $2)`, |
| | 139 | username, email, |
| | 140 | ).Scan(&exists) |
| | 141 | if err != nil { |
| | 142 | fmt.Println("Database error:", err) |
| | 143 | return |
| | 144 | } |
| | 145 | if exists { |
| | 146 | fmt.Println("Username or email already taken.") |
| | 147 | return |
| | 148 | } |
| | 149 | |
| | 150 | _, err = db.DB.Exec( |
| | 151 | `INSERT INTO users (username, email, full_name, password_hash, available_balance) |
| | 152 | VALUES ($1, $2, $3, $4, 0)`, |
| | 153 | username, email, fullName, hashPassword(pw), |
| | 154 | ) |
| | 155 | if err != nil { |
| | 156 | fmt.Println("Failed to register:", err) |
| | 157 | return |
| | 158 | } |
| | 159 | fmt.Println("Account created. You can now log in.") |
| | 160 | } |
| | 161 | }}} |