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