Browse Source

FEATURE: Email verification process on account registration

master
parent
commit
09710d48a9
  1. 1
      .gitignore
  2. 74
      pages/base.html
  3. 153
      pages/index.html
  4. 53
      pages/register.html
  5. 3
      scripts/api.js
  6. 47
      src/conf/conf.go
  7. 12
      src/db/db.go
  8. 29
      src/db/user.go
  9. 132
      src/db/verification.go
  10. 15
      src/email/auth.go
  11. 29
      src/email/email.go
  12. 16
      src/misc/codeNumeric.go
  13. 2
      src/misc/encryption.go
  14. 4
      src/server/api_test.go
  15. 129
      src/server/endpoints.go
  16. 19
      src/server/server.go
  17. 30
      src/server/validation.go

1
.gitignore vendored

@ -1,3 +1,4 @@
dela.zip
bin/
TODO
conf.json

74
pages/base.html

@ -6,51 +6,55 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>dela</title>
<title>Dela</title>
<link rel="shortcut icon" href="/static/images/favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="/static/bootstrap/css/bootstrap.min.css">
<script src="/static/bootstrap/js/bootstrap.min.js"></script>
<script src="/static/bootstrap/js/bootstrap.bundle.min.js"></script>
<style>
html * {
font-family: "Roboto" !important;
src: url("/static/fonts/Roboto-Regular.ttf");
}
html * {
font-family: "Roboto", sans-serif !important;
}
body {
background-color: #f8f9fa;
}
header {
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
</style>
</head>
<body class="w-100 h-100">
<header class="p-3 text-bg-primary">
<div class="container">
<div class="d-flex flex-wrap align-items-center justify-content-center justify-content-lg-start">
<a href="/" class="d-flex align-items-center mb-2 mb-lg-0 text-white text-decoration-none">
<a href="/" class="d-inline-flex link-body-emphasis text-decoration-none">
<img width="64" height="64" src="/static/images/android-chrome-192x192.png" alt="Dela">
</a>
</a>
<header class="p-3 text-bg-warning-emphasis">
<div class="container">
<div class="d-flex flex-wrap align-items-center justify-content-between">
<a href="/" class="d-flex align-items-center text-white text-decoration-none">
<img width="64" height="64" src="/static/images/android-chrome-192x192.png" alt="Dela">
</a>
<ul class="nav col-12 col-lg-auto me-lg-auto mb-2 justify-content-center mb-md-0">
<li><a href="/" class="nav-link px-2 text-white">Main</a></li>
<li><a href="/about" class="nav-link px-2 text-white">About</a></li>
</ul>
<nav class="nav col-12 col-lg-auto me-lg-auto mb-2 justify-content-center mb-md-0">
<a href="/" class="nav-link px-2 text-white">Main</a>
<a href="/about" class="nav-link px-2 text-white">About</a>
</nav>
<div class="text-end p-3">
<button id="theme-switch-btn" class="btn btn-secondary" onclick="toggleTheme();">
<img id="theme-svg" src="/static/images/brightness-high.svg" alt="Change theme">
</button>
<div class="d-flex align-items-center">
<button id="theme-switch-btn" class="btn btn-secondary me-2" onclick="toggleTheme();">
<img id="theme-svg" src="/static/images/brightness-high.svg" alt="Change theme">
</button>
<div id="bar-auth" class="d-flex">
<a href="/login" class="btn btn-outline-light me-2">Login</a>
<a href="/register" class="btn btn-warning">Sign-up</a>
</div>
</div>
</div>
</div>
<div class="text-end" id="bar-auth">
<a href="/login" class="btn btn-outline-light me-2">Login</a>
<a href="/register" class="btn btn-warning">Sign-up</a>
</div>
</div>
</div>
</div>
</header>
<!-- Content -->
{{ template "content" . }}
</header>
<!-- Content -->
<main class="container mt-4">
{{ template "content" . }}
</main>
</body>
</html>
<script src="/scripts/auth.js"></script>
@ -100,4 +104,4 @@ document.addEventListener('DOMContentLoaded', async function() {
}, false)
</script>
{{ end }}
{{ end }}

153
pages/index.html

@ -3,91 +3,90 @@
{{ define "content" }}
<!-- Main -->
<main>
<div class="d-flex flex-wrap">
<!-- Sidebar -->
<div id="sidebar" class="col border-right shadow-lg flex-shrink-1 p-2 d-flex flex-column align-items-stretch bg-body-tertiary">
<a href="/" class="d-flex align-items-center flex-shrink-0 p-3 link-body-emphasis text-decoration-none border-bottom">
<img class="bi pe-none me-2" width="30" height="24" src="/static/images/arrows-fullscreen.svg">
<span class="fs-5 fw-semibold">Categories</span>
</a>
<div class="list-group list-group-flush border-bottom scrollarea">
{{ range .Groups }}
<a href="/group/{{.ID}}" class="list-group-item list-group-item-action py-3 lh-sm" aria-current="true">
<div class="d-flex w-100 align-items-center justify-content-between">
<strong class="mb-1">{{ .Name }}</strong>
<small>{{ .TimeCreated }}</small>
</div>
{{ if not .Removable }}
<div class="col-10 mb-1 small">Not removable</div>
{{ end }}
</a>
{{ end }}
</div>
<main class="container mt-4">
<div class="d-flex flex-wrap">
<!-- Sidebar -->
<aside id="sidebar" class="col-md-3 col-lg-2 border-end shadow-lg flex-shrink-1 p-3 d-flex flex-column align-items-stretch bg-light">
<a href="/" class="d-flex align-items-center flex-shrink-0 p-3 link-body-emphasis text-decoration-none border-bottom">
<img class="bi pe-none me-2" width="30" height="24" src="/static/images/arrows-fullscreen.svg" alt="Logo">
<span class="fs-5 fw-semibold">Categories</span>
</a>
<div class="list-group list-group-flush border-bottom scrollarea">
{{ range .Groups }}
<a href="/group/{{.ID}}" class="list-group-item list-group-item-action py-3 lh-sm" aria-current="true">
<div class="d-flex w-100 align-items-center justify-content-between">
<strong class="mb-1">{{ .Name }}</strong>
<small>{{ .TimeCreated }}</small>
</div>
{{ if not .Removable }}
<div class="col-10 mb-1 small text-muted">Not removable</div>
{{ end }}
</a>
{{ end }}
</div>
<div class="input-group mb-3 py-md-5">
<input type="text" name="newCategory"aria-label="Category Name" aria-describedby="button-new-category" class="form-control" id="new-category-input" placeholder="Category Name">
<button id="button-new-category" onclick="createNewCategory();" class="btn btn-primary">Create</button >
</div>
</div>
<div class="input-group mb-3 mt-3">
<input type="text" name="newCategory" aria-label="Category Name" aria-describedby="button-new-category" class="form-control" id="new-category-input" placeholder="Category Name">
<button id="button-new-category" onclick="createNewCategory();" class="btn btn-primary">Create</button>
</div>
</aside>
<!-- Groups -->
<div class="d-flex flex-column flex-grow-1 flex-md-row p-4 gap-4 py-md-5">
<div class="list-group flex-grow-1">
{{ range .Groups }}
<div class="list-group-item list-group-item-action d-flex gap-3 py-3" aria-current="true">
<a href="/group/{{.ID}}" class="list-group-item list-group-item-action d-flex gap-3 py-3">
<img src="/static/images/box-arrow-up-right.svg" alt="Go to this category" width="32" height="32">
<div class="d-flex gap-2 w-100 justify-content-between">
<div>
<h6 class="mb-0">{{ .Name }}</h6>
<p class="mb-0 opacity-75">Jump here</p>
</div>
<small class="opacity-50 text-nowrap">{{ .TimeCreated }}</small>
<!-- Groups -->
<div class="col-md-9 col-lg-10 p-4">
<h2 class="mb-4">Available Categories</h2>
<div class="list-group">
{{ range .Groups }}
<div class="list-group-item list-group-item-action d-flex gap-3 py-3" aria-current="true">
<a href="/group/{{.ID}}" class="d-flex gap-3 w-100">
<img src="/static/images/box-arrow-up-right.svg" alt="Go to this category" width="32" height="32">
<div class="d-flex gap-2 w-100 justify-content-between">
<div>
<h6 class="mb-0">{{ .Name }}</h6>
<p class="mb-0 opacity-75">Jump here</p>
</div>
<small class="opacity-50 text-nowrap">{{ .TimeCreated }}</small>
</div>
</a>
{{ if .Removable }}
<div class="small">
<button class="btn btn-danger" onclick="deleteCategoryRefresh('{{.ID}}')">
<img src="/static/images/trash3-fill.svg" alt="Remove category" width="20">
</button>
</div>
{{ end }}
</div>
</a>
{{ if .Removable }}
<div class="small">
<button class="btn btn-danger" onclick="deleteCategoryRefresh('{{.ID}}')">
<img src="/static/images/trash3-fill.svg" alt="Remove category">
</button>
</div>
{{ end }}
</div>
{{ end }}
</div>
</div>
</div>
</div>
</main>
<script>
async function createNewCategory() {
let categoryInput = document.getElementById("new-category-input");
let newCategoryName = categoryInput.value;
if (newCategoryName.length < 1) {
categoryInput.setCustomValidity("At least one character is needed!");
return;
} else {
categoryInput.setCustomValidity("");
}
categoryInput.value = "";
// Post new category and refresh
await postNewGroup({
Name: newCategoryName
});
window.location.reload();
}
async function deleteCategoryRefresh(id) {
await deleteCategory(id);
window.location.reload();
}
</script>
<script>
async function createNewCategory() {
let categoryInput = document.getElementById("new-category-input");
let newCategoryName = categoryInput.value;
if (newCategoryName.length < 1) {
categoryInput.setCustomValidity("At least one character is needed!");
return;
} else {
categoryInput.setCustomValidity("");
}
categoryInput.value = "";
// Post new category and refresh
await postNewGroup({
Name: newCategoryName
});
window.location.reload();
}
async function deleteCategoryRefresh(id) {
await deleteCategory(id);
window.location.reload();
}
</script>
{{ end }}

53
pages/register.html

@ -40,7 +40,51 @@
</div>
</main>
<div class="modal fade" id="verificationModal" tabindex="-1" aria-labelledby="verificationModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="verificationModalLabel">Email Verification</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>Enter the verification code sent to your email address</p>
<div class="mb-3">
<label for="verificationCode" class="form-label">Verification Code</label>
<input type="text" id="input-code" class="form-control" id="verificationCode" placeholder="Enter your code" required>
</div>
<p class="text-danger"><span id="error-message-modal"></span></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" id="verifyButton" onclick="verify();">Verify</button>
</div>
</div>
</div>
</div>
<script>
function showVerificationModal() {
const verificationModal = new bootstrap.Modal(document.getElementById('verificationModal'), {});;
verificationModal.show();
}
async function verify() {
let emailInput = document.getElementById("input-email");
let email = String(emailInput.value).trim();
let codeInput = document.getElementById("input-code");
let code = String(codeInput.value).trim();
let response = await postEmailVerification(email, code);
if (response.ok) {
window.location.replace("/");
} else {
document.getElementById("error-message-modal").innerText = await response.text();
}
}
async function register() {
let emailInput = document.getElementById("input-email");
if (!emailInput.reportValidity()) {
@ -62,9 +106,14 @@ async function register() {
};
let response = await postNewUser(postData);
if (response.ok) {
window.location.replace("/");
let json = await response.json();
if (json.confirm_email) {
// Open email confirmation modal
showVerificationModal();
} else {
window.location.replace("/");
}
} else {
document.getElementById("error_message").innerText = await response.text();
}

3
scripts/api.js

@ -13,6 +13,9 @@ async function post(url, json) {
})
}
async function postEmailVerification(email, code) {
return post("/api/user/verify", {"email":email, "code":code});
}
async function postNewTodo(newTodo) {
return post("/api/todo/create", newTodo)

47
src/conf/conf.go

@ -1,6 +1,6 @@
/*
dela - web TODO list
Copyright (C) 2023 Kasyanov Nikolay Alexeyevich (Unbewohnte)
Copyright (C) 2023, 2025 Kasyanov Nikolay Alexeyevich (Unbewohnte)
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
@ -24,20 +24,49 @@ import (
"os"
)
type ServerConf struct {
Port uint16 `json:"port"`
CertFilePath string `json:"cert_file_path"`
KeyFilePath string `json:"key_file_path"`
}
type EmailerConf struct {
User string `json:"user"`
Host string `json:"host"`
HostPort uint16 `json:"host_port"`
Password string `json:"password"`
}
type EmailVerificationConf struct {
VerifyEmails bool `json:"verify_emails"`
Emailer EmailerConf `json:"emailer"`
}
type Conf struct {
Port uint16 `json:"port"`
CertFilePath string `json:"cert_file_path"`
KeyFilePath string `json:"key_file_path"`
BaseContentDir string `json:"base_content_dir"`
ProdDBName string `json:"production_db_name"`
Server ServerConf `json:"server"`
Verification EmailVerificationConf `json:"verification"`
BaseContentDir string `json:"base_content_dir"`
ProdDBName string `json:"production_db_name"`
}
// Creates a default server configuration
func Default() Conf {
return Conf{
Port: 8080,
CertFilePath: "",
KeyFilePath: "",
Server: ServerConf{
Port: 8080,
CertFilePath: "",
KeyFilePath: "",
},
Verification: EmailVerificationConf{
VerifyEmails: true,
Emailer: EmailerConf{
User: "you@example.com",
Host: "smtp.example.com",
HostPort: 587,
Password: "hostpassword",
},
},
BaseContentDir: ".",
ProdDBName: "dela.db",
}

12
src/db/db.go

@ -42,6 +42,18 @@ func setUpTables(db *DB) error {
return err
}
// User Email Verification
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS verifications(
id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE,
email TEXT NOT NULL,
code TEXT NOT NULL,
issued_unix INTEGER,
life_seconds INTEGER)`,
)
if err != nil {
return err
}
// Todo groups
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS todo_groups(
id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE,

29
src/db/user.go

@ -110,3 +110,32 @@ func (db *DB) DeleteUserClean(email string) error {
return nil
}
// Sets confirmed_email to true for given user
func (db *DB) UserSetEmailConfirmed(email string) error {
_, err := db.Exec(
"UPDATE users SET confirmed_email=? WHERE email=?",
true,
email,
)
return err
}
// Cleanly deletes user if email is not confirmed
func (db *DB) DeleteUnverifiedUserClean(email string) error {
user, err := db.GetUser(email)
if err != nil {
return err
}
if !user.ConfirmedEmail {
// Email is not verified, delete information on this user
err = db.DeleteUserClean(email)
if err != nil {
return err
}
}
return nil
}

132
src/db/verification.go

@ -0,0 +1,132 @@
package db
import "database/sql"
type Verification struct {
ID uint64 `json:"id"`
Email string `json:"email"`
Code string `json:"code"`
IssuedUnix uint64 `json:"issued_unix"`
LifeSeconds uint64 `json:"life_seconds"`
}
func NewVerification(email string, code string, issuedUnix uint64, lifeSeconds uint64) *Verification {
return &Verification{
Email: email,
Code: code,
IssuedUnix: issuedUnix,
LifeSeconds: lifeSeconds,
}
}
func scanVerification(rows *sql.Rows) (*Verification, error) {
var newVerification Verification
err := rows.Scan(
&newVerification.ID,
&newVerification.Email,
&newVerification.Code,
&newVerification.IssuedUnix,
&newVerification.LifeSeconds,
)
if err != nil {
return nil, err
}
return &newVerification, nil
}
// Retrieves a verification with given Id from the database
func (db *DB) GetVerification(id uint64) (*Verification, error) {
rows, err := db.Query(
"SELECT * FROM verifications WHERE id=?",
id,
)
if err != nil {
return nil, err
}
defer rows.Close()
rows.Next()
verification, err := scanVerification(rows)
if err != nil {
return nil, err
}
return verification, nil
}
// Returns the first email verification by email
func (db *DB) GetVerificationByEmail(email string) (*Verification, error) {
rows, err := db.Query(
"SELECT * FROM verifications WHERE email=?",
email,
)
if err != nil {
return nil, err
}
defer rows.Close()
rows.Next()
verification, err := scanVerification(rows)
if err != nil {
return nil, err
}
return verification, nil
}
// Retrieves information on ALL TODOs
func (db *DB) GetVerifications() ([]*Verification, error) {
var verifications []*Verification
rows, err := db.Query("SELECT * FROM verifications")
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
verification, err := scanVerification(rows)
if err != nil {
return verifications, err
}
verifications = append(verifications, verification)
}
return verifications, nil
}
// Creates a new verification in the database
func (db *DB) CreateVerification(verification Verification) error {
_, err := db.Exec(
"INSERT INTO verifications(email, code, issued_unix, life_seconds) VALUES(?, ?, ?, ?)",
verification.Email,
verification.Code,
verification.IssuedUnix,
verification.LifeSeconds,
)
return err
}
// Deletes information about a verification of certain ID from the database
func (db *DB) DeleteVerification(id uint64) error {
_, err := db.Exec(
"DELETE FROM verifications WHERE id=?",
id,
)
return err
}
// Updates verification
func (db *DB) UpdateVerification(verificationID uint64, updatedTodo Verification) error {
_, err := db.Exec(
"UPDATE verifications SET code=?, life_seconds=? WHERE id=?",
updatedTodo.Code,
updatedTodo.LifeSeconds,
verificationID,
)
return err
}

15
src/email/auth.go

@ -0,0 +1,15 @@
package email
import (
"Unbewohnte/dela/conf"
"net/smtp"
)
func Auth(conf conf.Conf) smtp.Auth {
return smtp.PlainAuth(
"",
conf.Verification.Emailer.User,
conf.Verification.Emailer.Password,
conf.Verification.Emailer.Host,
)
}

29
src/email/email.go

@ -13,15 +13,26 @@ type Email struct {
Body string
}
type MailSender struct {
Auth smtp.Auth
From string
func NewEmail(sender, subject, body string, to []string) Email {
return Email{
Sender: sender,
To: to,
Subject: subject,
Body: body,
}
}
type Emailer struct {
Auth smtp.Auth
Address string
From string
}
func NewMailSender(auth smtp.Auth, from string) MailSender {
return MailSender{
Auth: auth,
From: from,
func NewEmailer(auth smtp.Auth, addr string, from string) *Emailer {
return &Emailer{
Auth: auth,
Address: addr,
From: from,
}
}
@ -35,7 +46,7 @@ func buildEmail(mail Email) []byte {
return []byte(message)
}
func (ms *MailSender) SendEmail(addr string, mail Email) error {
err := smtp.SendMail(addr, ms.Auth, ms.From, mail.To, buildEmail(mail))
func (em *Emailer) SendEmail(mail Email) error {
err := smtp.SendMail(em.Address, em.Auth, em.From, mail.To, buildEmail(mail))
return err
}

16
src/misc/codeNumeric.go

@ -0,0 +1,16 @@
package misc
import (
"math/rand"
"strconv"
)
// Generates a pseudo-random numeric code of required length
func GenerateNumericCode(length uint) string {
code := ""
for i := 0; uint(i) < length; i++ {
code += strconv.Itoa(rand.Intn(10))
}
return code
}

2
src/encryption/encryption.go → src/misc/encryption.go

@ -16,7 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package encryption
package misc
import (
"crypto/sha256"

4
src/server/api_test.go

@ -63,7 +63,7 @@ func TestApi(t *testing.T) {
t.Fatalf("could not marshal new user JSON: %s", err)
}
resp, err := http.Post(fmt.Sprintf("http://localhost:%d/api/user", config.Port), "application/json", bytes.NewBuffer(newUserJsonBytes))
resp, err := http.Post(fmt.Sprintf("http://localhost:%d/api/user", config.Server.Port), "application/json", bytes.NewBuffer(newUserJsonBytes))
if err != nil {
t.Fatalf("failed to post a new user data: %s", err)
}
@ -124,7 +124,7 @@ func TestApi(t *testing.T) {
t.Fatalf("could not marshal new Todo: %s", err)
}
req, err := http.NewRequest("POST", fmt.Sprintf("http://localhost:%d/api/todo", config.Port), bytes.NewBuffer(newTodoBytes))
req, err := http.NewRequest("POST", fmt.Sprintf("http://localhost:%d/api/todo", config.Server.Port), bytes.NewBuffer(newTodoBytes))
if err != nil {
t.Fatalf("failed to create a new POST request to create a new TODO: %s", err)
}

129
src/server/endpoints.go

@ -20,6 +20,7 @@ package server
import (
"Unbewohnte/dela/db"
"Unbewohnte/dela/email"
"Unbewohnte/dela/logger"
"encoding/json"
"fmt"
@ -81,10 +82,127 @@ func (s *Server) EndpointUserCreate(w http.ResponseWriter, req *http.Request) {
))
if err != nil {
http.Error(w, "Failed to create default group", http.StatusInternalServerError)
logger.Error("[Server][EndpojntUserCreate] Failed to create a default group for %s: %s", user.Email, err)
logger.Error("[Server][EndpointUserCreate] Failed to create a default group for %s: %s", user.Email, err)
return
}
// Check if email verification is required
if !s.config.Verification.VerifyEmails {
// Do not verify email
// Send cookie
http.SetCookie(w, &http.Cookie{
Name: "auth",
Value: fmt.Sprintf("%s:%s", user.Email, user.Password),
SameSite: http.SameSiteStrictMode,
HttpOnly: false,
Path: "/",
Secure: false,
})
// Done
w.Write([]byte("{\"confirm_email\":false}"))
logger.Info("[Server][EndpointUserCreate] Successfully sent email notification to %s", user.Email)
return
}
// Send email verification message
verification, err := GenerateVerificationCode(s.db, user.Email)
if err != nil {
logger.Error("[Server][EndpointUserCreate] Failed to generate verification code for %s: %s", user.Email, err)
http.Error(w, "Failed to generate confirmation code", http.StatusInternalServerError)
return
}
// Send verification email
err = s.emailer.SendEmail(
email.NewEmail(
s.config.Verification.Emailer.User,
"Dela: Email verification",
fmt.Sprintf("Your email verification code: <b>%s</b>\nPlease, verify your email in %f hours.\nThis email was specified during Dela account creation. Ignore this message if it wasn't you", verification.Code, float32(verification.LifeSeconds)/3600),
[]string{user.Email},
),
)
if err != nil {
logger.Error("[Server][EndpointUserCreate] Failed to send verification email to %s: %s", user.Email, err)
http.Error(w, "Failed to send email verification message", http.StatusInternalServerError)
return
}
// Autodelete user account if email was not verified in time
time.AfterFunc(time.Second*time.Duration(verification.LifeSeconds), func() {
err = s.db.DeleteUnverifiedUserClean(user.Email)
if err != nil {
logger.Error("[Server][EndpointUserCreate] Failed to autodelete unverified user %s: %s", user.Email, err)
}
})
w.Write([]byte("{\"confirm_email\":true}"))
}
func (s *Server) EndpointUserVerify(w http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Retrieve data
defer req.Body.Close()
contents, err := io.ReadAll(req.Body)
if err != nil {
logger.Error("[Server][EndpointUserVerify] Failed to read request body: %s", err)
http.Error(w, "Failed to read request body", http.StatusInternalServerError)
return
}
type verificationAnswer struct {
Email string `json:"email"`
Code string `json:"code"`
}
var answer verificationAnswer
err = json.Unmarshal(contents, &answer)
if err != nil {
logger.Error("[Server][EndpointUserVerify] Failed to unmarshal verification answer: %s", err)
http.Error(w, "Verification answer JSON unmarshal error", http.StatusInternalServerError)
return
}
// Retrieve user
user, err := s.db.GetUser(answer.Email)
if err != nil {
logger.Error("[Server][EndpointUserVerify] Failed to retrieve information on \"%s\": %s", answer.Email, err)
http.Error(w, "Failed to get user information", http.StatusInternalServerError)
return
}
// Compare codes
dbCode, err := s.db.GetVerificationByEmail(user.Email)
if err != nil {
logger.Error("[Server][EndpointUserVerify] Could not get verification code from DB for %s: %s", user.Email, err)
http.Error(w, "Could not retrieve verification information for this email", http.StatusInternalServerError)
return
}
if answer.Code != dbCode.Code {
// Codes do not match!
logger.Error("[Server][EndpointUserVerify] %s sent wrong verification code", user.Email)
http.Error(w, "Wrong verification code!", http.StatusForbidden)
return
}
// All's good!
err = s.db.UserSetEmailConfirmed(user.Email)
if err != nil {
http.Error(w, "Failed to save confirmation information", http.StatusInternalServerError)
logger.Error("[Server][EndpointUserVerify] Failed to set confirmed_email to true for %s: %s", user.Email, err)
return
}
logger.Info("[Server][EndpointUserVerify] %s was successfully verified!", user.Email)
// Send cookie
http.SetCookie(w, &http.Cookie{
Name: "auth",
@ -122,14 +240,7 @@ func (s *Server) EndpointUserLogin(w http.ResponseWriter, req *http.Request) {
}
// Check auth data
userDB, err := s.db.GetUser(user.Email)
if err != nil {
logger.Error("[Server][EndpointUserLogin] Failed to fetch user information from DB: %s", err)
http.Error(w, "Failed to fetch user information", http.StatusInternalServerError)
return
}
if user.Password != userDB.Password {
if !IsUserAuthorized(s.db, user) {
http.Error(w, "Failed auth", http.StatusForbidden)
return
}

19
src/server/server.go

@ -21,6 +21,7 @@ package server
import (
"Unbewohnte/dela/conf"
"Unbewohnte/dela/db"
"Unbewohnte/dela/email"
"Unbewohnte/dela/logger"
"context"
"fmt"
@ -45,6 +46,7 @@ type Server struct {
db *db.DB
http http.Server
cookieJar *cookiejar.Jar
emailer *email.Emailer
}
// Creates a new server instance with provided config
@ -86,7 +88,7 @@ func New(config conf.Conf) (*Server, error) {
// start constructing an http server configuration
server.http = http.Server{
Addr: fmt.Sprintf(":%d", server.config.Port),
Addr: fmt.Sprintf(":%d", server.config.Server.Port),
}
// configure paths' callbacks
@ -218,6 +220,7 @@ func New(config conf.Conf) (*Server, error) {
mux.HandleFunc("/api/user/update", server.EndpointUserUpdate) // Non specific
mux.HandleFunc("/api/user/create", server.EndpointUserCreate) // Non specific
mux.HandleFunc("/api/user/login", server.EndpointUserLogin) // Non specific
mux.HandleFunc("/api/user/verify", server.EndpointUserVerify) // Non specific
mux.HandleFunc("/api/todo/create", server.EndpointTodoCreate) // Non specific
mux.HandleFunc("/api/todo/get", server.EndpointUserTodosGet) // Non specific
mux.HandleFunc("/api/todo/delete/", server.EndpointTodoDelete) // Specific
@ -232,6 +235,12 @@ func New(config conf.Conf) (*Server, error) {
jar, _ := cookiejar.New(nil)
server.cookieJar = jar
server.emailer = email.NewEmailer(
email.Auth(server.config),
fmt.Sprintf("%s:%d", server.config.Verification.Emailer.Host, server.config.Verification.Emailer.HostPort),
server.config.Verification.Emailer.User,
)
logger.Info("[Server] Created an HTTP server instance")
return &server, nil
@ -239,18 +248,18 @@ func New(config conf.Conf) (*Server, error) {
// Launches server instance
func (s *Server) Start() error {
if s.config.CertFilePath != "" && s.config.KeyFilePath != "" {
if s.config.Server.CertFilePath != "" && s.config.Server.KeyFilePath != "" {
logger.Info("[Server] Using TLS")
logger.Info("[Server] HTTP server is going live on port %d!", s.config.Port)
logger.Info("[Server] HTTP server is going live on port %d!", s.config.Server.Port)
err := s.http.ListenAndServeTLS(s.config.CertFilePath, s.config.KeyFilePath)
err := s.http.ListenAndServeTLS(s.config.Server.CertFilePath, s.config.Server.KeyFilePath)
if err != nil && err != http.ErrServerClosed {
logger.Error("[Server] Fatal server error: %s", err)
return err
}
} else {
logger.Info("[Server] Not using TLS")
logger.Info("[Server] HTTP server is going live on port %d!", s.config.Port)
logger.Info("[Server] HTTP server is going live on port %d!", s.config.Server.Port)
err := s.http.ListenAndServe()
if err != nil && err != http.ErrServerClosed {

30
src/server/validation.go

@ -20,9 +20,11 @@ package server
import (
"Unbewohnte/dela/db"
"Unbewohnte/dela/misc"
"fmt"
"net/http"
"strings"
"time"
)
const (
@ -52,13 +54,17 @@ func IsUserValid(user db.User) (bool, string) {
return true, ""
}
// Checks if such a user exists and compares passwords. Returns true if such user exists and passwords do match
// Checks if such user exists, passwords match and email is confirmed. Returns true if such user exists, passwords do match and email was verified
func IsUserAuthorized(db *db.DB, user db.User) bool {
userDB, err := db.GetUser(user.Email)
if err != nil {
return false
}
if !userDB.ConfirmedEmail {
return false
}
if userDB.Password != user.Password {
return false
}
@ -82,8 +88,8 @@ func AuthFromCookie(cookie *http.Cookie) (string, string) {
/*
Gets auth information from a request and
checks if such a user exists and compares passwords.
Returns true if such user exists and passwords do match
checks if such user exists and passwords match.
Returns true if such user exists, passwords do match and email is confirmed
*/
func IsUserAuthorizedReq(req *http.Request, dbase *db.DB) bool {
var email, password string
@ -118,3 +124,21 @@ func GetLoginFromReq(req *http.Request) string {
return email
}
/*
Generates a new verification code for given email with 8-digit numeric code,
current issue time and one hour lifetime.
Inserts newly created email verification into database.
*/
func GenerateVerificationCode(dbase *db.DB, email string) (*db.Verification, error) {
verification := db.NewVerification(
email, misc.GenerateNumericCode(8), uint64(time.Now().Unix()), uint64(time.Hour.Seconds()),
)
err := dbase.CreateVerification(*verification)
if err != nil {
return nil, err
}
return verification, nil
}

Loading…
Cancel
Save