mirror of
https://github.com/Floriansylvain/RenewCMS.git
synced 2026-08-19 11:43:22 +02:00
feat: WIP mail verification flow
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
ENVIRONMENT=development
|
ENVIRONMENT=development
|
||||||
|
HOST=example.com
|
||||||
PORT=8080
|
PORT=8080
|
||||||
|
JWT_SECRET=abc123
|
||||||
CORS_ALLOWED_ORIGINS=*
|
CORS_ALLOWED_ORIGINS=*
|
||||||
DB_FILE=./gohcms.db
|
DB_FILE=./gohcms.db
|
||||||
DOCKER_DB_FOLDER=./data
|
DOCKER_DB_FOLDER=./data
|
||||||
|
|||||||
@@ -39,17 +39,19 @@ of course required, but not necessarily via the `.env` file.
|
|||||||
|
|
||||||
### Environment variables
|
### Environment variables
|
||||||
|
|
||||||
| Name | Type | Description | Comment |
|
| Name | Type | Description | Comment |
|
||||||
|----------------------|--------|:------------------------------------|-----------------------------------------------------------------------------------------------------------------------|
|
|----------------------|--------|:----------------------------------------------|-----------------------------------------------------------------------------------------------------------------------|
|
||||||
| ENVIRONMENT | string | environment the API is running in | required, `development` or `production` |
|
| ENVIRONMENT | string | environment the API is running in | required, `development` or `production` |
|
||||||
| PORT | int | port the API will use | required |
|
| HOST | string | the host domain name for emails and callbacks | required |
|
||||||
| CORS_ALLOWED_ORIGINS | string | allowed origins for CORS | required, semicolon separated list |
|
| PORT | int | port the API will use | required |
|
||||||
| DB_FILE | string | path to the sqlite db file | required, can be at the root but name still required (e.g. `./gohcms.db`) ; have to end up with `.db` |
|
| JWT_SECRET | string | secret for the jwt auth | required |
|
||||||
| DOCKER_DB_FOLDER | string | path to the sqlite db file's folder | required with docker, this will basically be the host machine folder (e.g. `./data`) that contains the sqlite db file |
|
| CORS_ALLOWED_ORIGINS | string | allowed origins for CORS | required, semicolon separated list |
|
||||||
| SMTP_EMAIL | string | sender email | required |
|
| DB_FILE | string | path to the sqlite db file | required, can be at the root but name still required (e.g. `./gohcms.db`) ; have to end up with `.db` |
|
||||||
| SMTP_PASSWORD | string | smtp account password | required |
|
| DOCKER_DB_FOLDER | string | path to the sqlite db file's folder | required with docker, this will basically be the host machine folder (e.g. `./data`) that contains the sqlite db file |
|
||||||
| SMTP_HOST | string | smtp server address | required |
|
| SMTP_EMAIL | string | sender email | required |
|
||||||
| SMTP_PORT | int | smtp server port | required |
|
| SMTP_PASSWORD | string | smtp account password | required |
|
||||||
|
| SMTP_HOST | string | smtp server address | required |
|
||||||
|
| SMTP_PORT | int | smtp server port | required |
|
||||||
|
|
||||||
## API Usage
|
## API Usage
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body>
|
<body>
|
||||||
<p>salut bebou</p>
|
<h1>
|
||||||
|
<img src="{{.Host}}/static/gohcms-favicon-128.png" alt="G" style="width: 2.5rem"/>
|
||||||
|
GohCMS
|
||||||
|
</h1>
|
||||||
|
<main>
|
||||||
|
<p>You or someone tried to create a GohCMS admin account with this e-mail address. If you are not responsible for
|
||||||
|
this, please do not validate the account creation.</p>
|
||||||
|
<p><b>If you want to validate the account creation, please <a href="{{.Host}}/register/validate?c={{.VerificationCode}}" target="_blank">click HERE</a>.</b>
|
||||||
|
</p>
|
||||||
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -38,12 +38,14 @@ func (m MailRepository) Send(receiverAddress string, templateName string, data i
|
|||||||
}
|
}
|
||||||
|
|
||||||
msg := gomail.NewMessage()
|
msg := gomail.NewMessage()
|
||||||
msg.SetHeader("From", "GohCMS <"+from+">")
|
msg.SetHeaders(map[string][]string{
|
||||||
msg.SetHeader("To", receiverAddress)
|
"From": {"GohCMS <" + from + ">"},
|
||||||
msg.SetHeader("MIME-version", "1.0")
|
"To": {receiverAddress},
|
||||||
msg.SetHeader("Content-Type", "text/html")
|
"MIME-version": {"1.0"},
|
||||||
msg.SetHeader("charset", "UTF-8")
|
"Content-Type": {"text/html"},
|
||||||
msg.SetHeader("Subject", "GohCMS | Action required")
|
"charset": {"UTF-8"},
|
||||||
|
"Subject": {"GohCMS | Action required"},
|
||||||
|
})
|
||||||
msg.SetBody("text/html", body.String())
|
msg.SetBody("text/html", body.String())
|
||||||
|
|
||||||
if err := d.DialAndSend(msg); err != nil {
|
if err := d.DialAndSend(msg); err != nil {
|
||||||
|
|||||||
@@ -7,10 +7,13 @@ import (
|
|||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
gorm.Model
|
gorm.Model
|
||||||
ID uint32 `gorm:"primaryKey;autoIncrement"`
|
ID uint32 `gorm:"primaryKey;autoIncrement"`
|
||||||
Username string `gorm:"unique;not null"`
|
Username string `gorm:"unique;not null"`
|
||||||
Password string `gorm:"not null"`
|
Password string `gorm:"not null"`
|
||||||
Email string `gorm:"unique;not null"`
|
Email string `gorm:"unique;not null"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
IsVerified bool `gorm:"default=false;not null"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
VerificationCode string `gorm:"unique;not null"`
|
||||||
|
VerificationExpiration time.Time `gorm:"not null"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,16 @@ func NewUserRepository(db *gorm.DB) *UserRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func mapUserToDomain(user entity.User) domain.User {
|
func mapUserToDomain(user entity.User) domain.User {
|
||||||
return domain.FromDb(user.ID, user.Username, user.Password, user.Email, user.CreatedAt, user.UpdatedAt)
|
return domain.FromDb(
|
||||||
|
user.ID,
|
||||||
|
user.Username,
|
||||||
|
user.Password,
|
||||||
|
user.Email,
|
||||||
|
user.IsVerified,
|
||||||
|
user.VerificationCode,
|
||||||
|
user.VerificationExpiration,
|
||||||
|
user.CreatedAt, user.UpdatedAt,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UserRepository) Get(id uint32) (domain.User, error) {
|
func (u *UserRepository) Get(id uint32) (domain.User, error) {
|
||||||
@@ -32,11 +41,13 @@ func (u *UserRepository) Get(id uint32) (domain.User, error) {
|
|||||||
|
|
||||||
func (u *UserRepository) Create(user domain.User) (domain.User, error) {
|
func (u *UserRepository) Create(user domain.User) (domain.User, error) {
|
||||||
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(user.Password), 14)
|
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(user.Password), 14)
|
||||||
|
hashedVerificationCode, _ := bcrypt.GenerateFromPassword([]byte(user.VerificationCode), 14)
|
||||||
|
|
||||||
creationResult := u.db.Create(&entity.User{
|
creationResult := u.db.Create(&entity.User{
|
||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
Password: string(hashedPassword),
|
Password: string(hashedPassword),
|
||||||
Email: user.Email,
|
Email: user.Email,
|
||||||
|
VerificationCode: string(hashedVerificationCode),
|
||||||
})
|
})
|
||||||
if creationResult.Error != nil {
|
if creationResult.Error != nil {
|
||||||
return domain.User{}, creationResult.Error
|
return domain.User{}, creationResult.Error
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>GohCMS | Setup</title>
|
||||||
|
{{.Head}}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.form-container {
|
||||||
|
max-width: 24rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="d-flex min-vh-100 vw-100 justify-content-center align-items-center text-dark">
|
||||||
|
<div class="container">
|
||||||
|
<div class="d-flex flex-column gap-5 m-auto form-container">
|
||||||
|
<div>
|
||||||
|
<h1>GohCMS</h1>
|
||||||
|
<h2>Verify your email</h2>
|
||||||
|
</div>
|
||||||
|
<p>An e-mail with the validation link was sent to the address you just registered.</p>
|
||||||
|
<button id="finalSetupFormButton" class="btn btn-outline-primary" type="button">
|
||||||
|
<span class="finalSetupFormButtonLoading visually-hidden spinner-border spinner-border-sm"
|
||||||
|
role="status"></span>
|
||||||
|
<span class="finalSetupFormButtonDefault">Cancel</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>GohCMS | Setup</title>
|
||||||
|
{{.Head}}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.form-container {
|
||||||
|
max-width: 24rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="d-flex min-vh-100 vw-100 justify-content-center align-items-center text-dark">
|
||||||
|
<div class="container">
|
||||||
|
<div class="d-flex flex-column gap-5 m-auto form-container">
|
||||||
|
<div>
|
||||||
|
<h1>GohCMS</h1>
|
||||||
|
<h2>E-mail verification</h2>
|
||||||
|
</div>
|
||||||
|
<p>Verifying your e-mail, please wait...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<title>GohCMS | Setup</title>
|
|
||||||
{{.Head}}
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.form-container {
|
|
||||||
max-width: 24rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body class="d-flex min-vh-100 vw-100 justify-content-center align-items-center text-dark">
|
|
||||||
<div class="container">
|
|
||||||
<div class="d-flex flex-column gap-5 m-auto form-container">
|
|
||||||
<div>
|
|
||||||
<h1>GohCMS</h1>
|
|
||||||
<h2>Setup done!</h2>
|
|
||||||
</div>
|
|
||||||
<p>GohCMS is now ready.</p>
|
|
||||||
<button id="finalSetupFormButton" class="btn btn-primary" type="button">
|
|
||||||
<span class="finalSetupFormButtonLoading visually-hidden spinner-border spinner-border-sm"
|
|
||||||
role="status"></span>
|
|
||||||
<span class="finalSetupFormButtonDefault">Finish</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const button = document.querySelector("#finalSetupFormButton")
|
|
||||||
|
|
||||||
function setButtonLoading() {
|
|
||||||
button.classList.add("disabled")
|
|
||||||
button.querySelector(".finalSetupFormButtonDefault").classList.add("visually-hidden")
|
|
||||||
button.querySelector(".finalSetupFormButtonLoading").classList.remove("visually-hidden")
|
|
||||||
}
|
|
||||||
|
|
||||||
function onFinalSetupFormSubmit() {
|
|
||||||
setButtonLoading()
|
|
||||||
window.location.replace("/home")
|
|
||||||
}
|
|
||||||
|
|
||||||
button.addEventListener('click', onFinalSetupFormSubmit)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
+23
-5
@@ -6,6 +6,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/go-chi/jwtauth/v5"
|
"github.com/go-chi/jwtauth/v5"
|
||||||
|
"github.com/google/uuid"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@@ -61,6 +62,13 @@ func IsLoggedIn(r *http.Request) bool {
|
|||||||
return token != nil && err == nil
|
return token != nil && err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsVerified(r *http.Request) bool {
|
||||||
|
_, claims, _ := jwtauth.FromContext(r.Context())
|
||||||
|
userIDClaim, _ := claims["user_id"].(uint32)
|
||||||
|
currentUser, _ := Container.GetUserUseCase.GetUser(userIDClaim)
|
||||||
|
return currentUser.IsVerified
|
||||||
|
}
|
||||||
|
|
||||||
func getUserFromCredentials(credentials LoginCredentials) (user.User, error) {
|
func getUserFromCredentials(credentials LoginCredentials) (user.User, error) {
|
||||||
dbUser, err := Container.GetUserUseCase.GetUserByUsername(credentials.Username)
|
dbUser, err := Container.GetUserUseCase.GetUserByUsername(credentials.Username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -75,6 +83,19 @@ func getUserFromCredentials(credentials LoginCredentials) (user.User, error) {
|
|||||||
return dbUser, nil
|
return dbUser, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getNewUser(newUserCredentials RegisterCredentials, verificationCode string) (user.User, error) {
|
||||||
|
createdUser, err := Container.CreateUserUseCase.CreateUser(useCases.CreateUserCommand{
|
||||||
|
Username: newUserCredentials.Username,
|
||||||
|
Password: newUserCredentials.Password,
|
||||||
|
Email: newUserCredentials.Email,
|
||||||
|
VerificationCode: verificationCode,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return user.User{}, err
|
||||||
|
}
|
||||||
|
return createdUser, nil
|
||||||
|
}
|
||||||
|
|
||||||
func login(w http.ResponseWriter, r *http.Request) {
|
func login(w http.ResponseWriter, r *http.Request) {
|
||||||
var credentials LoginCredentials
|
var credentials LoginCredentials
|
||||||
|
|
||||||
@@ -120,11 +141,8 @@ func register(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
createdUser, err := Container.CreateUserUseCase.CreateUser(useCases.CreateUserCommand{
|
verificationCode := uuid.NewString()
|
||||||
Username: credentials.Username,
|
createdUser, err := getNewUser(credentials, verificationCode)
|
||||||
Password: credentials.Password,
|
|
||||||
Email: credentials.Email,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
|||||||
+17
-1
@@ -47,6 +47,16 @@ func IsLoggedInMiddleware(next http.Handler) http.Handler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsVerifiedMiddleware(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !IsVerified(r) {
|
||||||
|
http.Redirect(w, r, "/register/pending", http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func GetLogin(w http.ResponseWriter, r *http.Request) {
|
func GetLogin(w http.ResponseWriter, r *http.Request) {
|
||||||
http.Redirect(w, r, LoginRoute, http.StatusPermanentRedirect)
|
http.Redirect(w, r, LoginRoute, http.StatusPermanentRedirect)
|
||||||
}
|
}
|
||||||
@@ -91,7 +101,13 @@ func NewPageRouter() http.Handler {
|
|||||||
|
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
r.Use(IsLoggedInMiddleware)
|
r.Use(IsLoggedInMiddleware)
|
||||||
r.Get("/register-confirm", GetRegisterConfirmPage)
|
r.Get("/register/pending", GetRegisterPendingPage)
|
||||||
|
r.Get("/register/validate", GetRegisterValidatePage)
|
||||||
|
})
|
||||||
|
|
||||||
|
r.Group(func(r chi.Router) {
|
||||||
|
r.Use(IsLoggedInMiddleware)
|
||||||
|
r.Use(IsVerifiedMiddleware)
|
||||||
r.Get("/home", GetHomePage)
|
r.Get("/home", GetHomePage)
|
||||||
r.Get("/post", GetPostsPage)
|
r.Get("/post", GetPostsPage)
|
||||||
r.Get("/post/edit", GetPostEditPage)
|
r.Get("/post/edit", GetPostEditPage)
|
||||||
|
|||||||
+19
-19
@@ -1,8 +1,7 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"github.com/google/uuid"
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
@@ -29,7 +28,7 @@ func GetRegisterPageHandler(registerPage *RegisterPage) http.HandlerFunc {
|
|||||||
PostRegisterPage(w, r)
|
PostRegisterPage(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
bs, err := Container.GetPageUseCase.GetPage("setup1", map[string]interface{}{
|
bs, err := Container.GetPageUseCase.GetPage("register", map[string]interface{}{
|
||||||
"PageError": registerPage.PageError,
|
"PageError": registerPage.PageError,
|
||||||
"Username": registerPage.Username,
|
"Username": registerPage.Username,
|
||||||
"Email": registerPage.Email,
|
"Email": registerPage.Email,
|
||||||
@@ -45,11 +44,12 @@ func GetRegisterPageHandler(registerPage *RegisterPage) http.HandlerFunc {
|
|||||||
func PostRegisterPage(w http.ResponseWriter, r *http.Request) {
|
func PostRegisterPage(w http.ResponseWriter, r *http.Request) {
|
||||||
_ = r.ParseForm()
|
_ = r.ParseForm()
|
||||||
|
|
||||||
credentials, err := json.Marshal(&RegisterCredentials{
|
credentials := RegisterCredentials{
|
||||||
Username: r.FormValue("username"),
|
Username: r.FormValue("username"),
|
||||||
Password: r.FormValue("password"),
|
Password: r.FormValue("password"),
|
||||||
Email: r.FormValue("email"),
|
Email: r.FormValue("email"),
|
||||||
})
|
}
|
||||||
|
err := validate.Struct(credentials)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.Method = http.MethodGet
|
r.Method = http.MethodGet
|
||||||
GetRegisterPageHandler(&RegisterPage{
|
GetRegisterPageHandler(&RegisterPage{
|
||||||
@@ -60,22 +60,22 @@ func PostRegisterPage(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response, err := http.Post(
|
verificationCode := uuid.NewString()
|
||||||
"http://localhost:"+os.Getenv("PORT")+"/v1/auth/register",
|
createdUser, err := getNewUser(credentials, verificationCode)
|
||||||
"application/json",
|
if err != nil {
|
||||||
bytes.NewBuffer(credentials))
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
|
||||||
if err != nil || response.StatusCode != http.StatusOK {
|
|
||||||
r.Method = http.MethodGet
|
|
||||||
GetRegisterPageHandler(&RegisterPage{
|
|
||||||
PageError: NewPageError("Username should be between 3 and 20 characters long, password should be between 8 and 20 characters long, and email should be a valid email address."),
|
|
||||||
Username: r.FormValue("username"),
|
|
||||||
Email: r.FormValue("email"),
|
|
||||||
})(w, r)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Set-Cookie", response.Header.Get("Set-Cookie"))
|
err = Container.SendMailUseCase.SendMail(createdUser.Email, "mailValidation", map[string]string{
|
||||||
|
"Host": os.Getenv("HOST"),
|
||||||
|
"VerificationCode": verificationCode,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
http.Redirect(w, r, "/register-confirm", http.StatusSeeOther)
|
_ = SetJwtCookie(&w, createdUser.ID)
|
||||||
|
|
||||||
|
http.Redirect(w, r, "/register/pending", http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
func GetRegisterConfirmPage(w http.ResponseWriter, _ *http.Request) {
|
|
||||||
registerConfirmTmpl, _ := Container.GetPageUseCase.GetPage("setup2", map[string]interface{}{
|
|
||||||
"Head": headTmpl,
|
|
||||||
})
|
|
||||||
_, _ = w.Write(registerConfirmTmpl)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
func GetRegisterPendingPage(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
registerPendingTmpl, _ := Container.GetPageUseCase.GetPage("registerPending", map[string]interface{}{
|
||||||
|
"Head": headTmpl,
|
||||||
|
})
|
||||||
|
_, _ = w.Write(registerPendingTmpl)
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-chi/jwtauth/v5"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetRegisterValidatePage(w http.ResponseWriter, r *http.Request) {
|
||||||
|
queryVerificationCode := r.URL.Query().Get("c")
|
||||||
|
if queryVerificationCode == "" {
|
||||||
|
http.Redirect(w, r, "/register/pending", http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, claims, _ := jwtauth.FromContext(r.Context())
|
||||||
|
userId, _ := claims["user_id"].(uint32)
|
||||||
|
|
||||||
|
user, _ := Container.GetUserUseCase.GetUser(userId)
|
||||||
|
errorMessage := ""
|
||||||
|
|
||||||
|
if user.VerificationCode != queryVerificationCode || user.VerificationExpiration.Before(time.Now()) {
|
||||||
|
errorMessage = "Verification link is incorrect or has expired."
|
||||||
|
} else {
|
||||||
|
// TODO New usecase "UpdateUserUseCase" to update its verification status
|
||||||
|
}
|
||||||
|
|
||||||
|
registerValidateTmpl, _ := Container.GetPageUseCase.GetPage("registerValidate", map[string]interface{}{
|
||||||
|
"Head": headTmpl,
|
||||||
|
"PageError": NewPageError(errorMessage),
|
||||||
|
})
|
||||||
|
_, _ = w.Write(registerValidateTmpl)
|
||||||
|
}
|
||||||
+31
-16
@@ -1,25 +1,34 @@
|
|||||||
package user
|
package user
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
ID uint32 `json:"id"`
|
ID uint32 `json:"id"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
IsVerified bool `gorm:"default=false;not null"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
VerificationCode string `gorm:"unique;not null"`
|
||||||
|
VerificationExpiration time.Time `gorm:"not null"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func FromApi(
|
func FromApi(
|
||||||
username string,
|
username string,
|
||||||
password string,
|
password string,
|
||||||
email string,
|
email string,
|
||||||
|
verificationCode string,
|
||||||
) User {
|
) User {
|
||||||
return User{
|
return User{
|
||||||
Username: username,
|
Username: username,
|
||||||
Password: password,
|
Password: password,
|
||||||
Email: email,
|
Email: email,
|
||||||
|
IsVerified: false,
|
||||||
|
VerificationCode: verificationCode,
|
||||||
|
VerificationExpiration: time.Now().Add(2 * time.Hour),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,15 +37,21 @@ func FromDb(
|
|||||||
username string,
|
username string,
|
||||||
password string,
|
password string,
|
||||||
email string,
|
email string,
|
||||||
|
isVerified bool,
|
||||||
|
verificationCode string,
|
||||||
|
verificationExpiration time.Time,
|
||||||
createdAt time.Time,
|
createdAt time.Time,
|
||||||
updatedAt time.Time,
|
updatedAt time.Time,
|
||||||
) User {
|
) User {
|
||||||
return User{
|
return User{
|
||||||
ID: id,
|
ID: id,
|
||||||
Username: username,
|
Username: username,
|
||||||
Password: password,
|
Password: password,
|
||||||
Email: email,
|
Email: email,
|
||||||
CreatedAt: createdAt,
|
IsVerified: isVerified,
|
||||||
UpdatedAt: updatedAt,
|
VerificationCode: verificationCode,
|
||||||
|
VerificationExpiration: verificationExpiration,
|
||||||
|
CreatedAt: createdAt,
|
||||||
|
UpdatedAt: updatedAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 // indirect
|
github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 // indirect
|
||||||
|
github.com/beevik/guid v1.0.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
@@ -27,7 +28,7 @@ require (
|
|||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/goccy/go-json v0.10.2 // indirect
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
github.com/google/uuid v1.3.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.5 // indirect
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
github.com/kr/text v0.2.0 // indirect
|
github.com/kr/text v0.2.0 // indirect
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ github.com/MadAppGang/httplog v1.3.0 h1:1XU54TO8kiqTeO+7oZLKAM3RP/cJ7SadzslRcKsp
|
|||||||
github.com/MadAppGang/httplog v1.3.0/go.mod h1:gpYEdkjh/Cda6YxtDy4AB7KY+fR7mb3SqBZw74A5hJ4=
|
github.com/MadAppGang/httplog v1.3.0/go.mod h1:gpYEdkjh/Cda6YxtDy4AB7KY+fR7mb3SqBZw74A5hJ4=
|
||||||
github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 h1:ZBbLwSJqkHBuFDA6DUhhse0IGJ7T5bemHyNILUjvOq4=
|
github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 h1:ZBbLwSJqkHBuFDA6DUhhse0IGJ7T5bemHyNILUjvOq4=
|
||||||
github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2/go.mod h1:VSw57q4QFiWDbRnjdX8Cb3Ow0SFncRw+bA/ofY6Q83w=
|
github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2/go.mod h1:VSw57q4QFiWDbRnjdX8Cb3Ow0SFncRw+bA/ofY6Q83w=
|
||||||
|
github.com/beevik/guid v1.0.0 h1:XhTlrl9h5+TlkB7MB3SBwAm2+ZdFE62O0D+g7LDFqqI=
|
||||||
|
github.com/beevik/guid v1.0.0/go.mod h1:FyB4y08P/8c0J0xhRHR6xVjdXIpGDwpMXzmGV6vWDj4=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
@@ -36,6 +38,8 @@ github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MG
|
|||||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f h1:7LYC+Yfkj3CTRcShK0KOL/w6iTiKyqqBA9a41Wnggw8=
|
github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f h1:7LYC+Yfkj3CTRcShK0KOL/w6iTiKyqqBA9a41Wnggw8=
|
||||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
|
|||||||
+1
-11
@@ -3,7 +3,6 @@ package route
|
|||||||
import (
|
import (
|
||||||
"GohCMS2/api"
|
"GohCMS2/api"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"github.com/MadAppGang/httplog"
|
"github.com/MadAppGang/httplog"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/go-chi/cors"
|
"github.com/go-chi/cors"
|
||||||
@@ -30,19 +29,10 @@ func HtmlContentTypeMiddleware(next http.Handler) http.Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func InitJwt() {
|
func InitJwt() {
|
||||||
api.TokenAuth = jwtauth.New("HS256", []byte("secret"), nil)
|
api.TokenAuth = jwtauth.New("HS256", []byte(os.Getenv("JWT_SECRET")), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetHelloWorld(w http.ResponseWriter, _ *http.Request) {
|
func GetHelloWorld(w http.ResponseWriter, _ *http.Request) {
|
||||||
err := api.Container.SendMailUseCase.SendMail(
|
|
||||||
"floriansylvainpro@gmail.com",
|
|
||||||
"mailValidation",
|
|
||||||
struct{}{},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
msg, _ := json.Marshal(map[string]string{"message": "Hello World"})
|
msg, _ := json.Marshal(map[string]string{"message": "Hello World"})
|
||||||
_, _ = w.Write(msg)
|
_, _ = w.Write(msg)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ import (
|
|||||||
|
|
||||||
var possibleEnvFileLocations = []string{".env", "../.env"}
|
var possibleEnvFileLocations = []string{".env", "../.env"}
|
||||||
var envVarsToLoad = []string{
|
var envVarsToLoad = []string{
|
||||||
|
"HOST",
|
||||||
"PORT",
|
"PORT",
|
||||||
|
"JWT_SECRET",
|
||||||
"ENVIRONMENT",
|
"ENVIRONMENT",
|
||||||
"CORS_ALLOWED_ORIGINS",
|
"CORS_ALLOWED_ORIGINS",
|
||||||
"DB_FILE",
|
"DB_FILE",
|
||||||
|
|||||||
@@ -11,9 +11,10 @@ type CreateUserUseCase struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CreateUserCommand struct {
|
type CreateUserCommand struct {
|
||||||
Username string
|
Username string
|
||||||
Password string
|
Password string
|
||||||
Email string
|
Email string
|
||||||
|
VerificationCode string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCreateUserUseCase(db *gorm.DB) *CreateUserUseCase {
|
func NewCreateUserUseCase(db *gorm.DB) *CreateUserUseCase {
|
||||||
@@ -27,5 +28,6 @@ func (g *CreateUserUseCase) CreateUser(createUser CreateUserCommand) (user.User,
|
|||||||
createUser.Username,
|
createUser.Username,
|
||||||
createUser.Password,
|
createUser.Password,
|
||||||
createUser.Email,
|
createUser.Email,
|
||||||
|
createUser.VerificationCode,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user