Compare commits

...

7 Commits

11 changed files with 359 additions and 16 deletions

View File

@ -34,7 +34,7 @@ func (s *APIServer) Run() error {
FileServer(router, "/static", staticDir)
router.Route("/api/v1", func(r chi.Router) {
userHandler := user.NewUserHandler()
userHandler := user.NewUserHandler(s.repo)
userHandler.RegisterRoutes(router, r)
})

1
go.mod
View File

@ -4,6 +4,7 @@ go 1.24.3
require (
github.com/go-chi/chi/v5 v5.2.1 // indirect
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect

2
go.sum
View File

@ -1,6 +1,8 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
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/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=

View File

@ -1,21 +1,29 @@
package user
import (
"context"
"encoding/json"
"net/http"
"gitea.local/admin/hspguard/internal/repository"
"gitea.local/admin/hspguard/internal/web"
"github.com/go-chi/chi/v5"
)
type UserHandler struct{}
type UserHandler struct {
repo *repository.Queries
}
func NewUserHandler() *UserHandler {
return &UserHandler{}
func NewUserHandler(repo *repository.Queries) *UserHandler {
return &UserHandler{
repo: repo,
}
}
func (h *UserHandler) RegisterRoutes(router chi.Router, api chi.Router) {
router.Get("/login", h.loginPage)
router.Get("/register", h.registerPage)
api.Post("/register", h.register)
}
func (h *UserHandler) loginPage(w http.ResponseWriter, r *http.Request) {
@ -33,3 +41,54 @@ func (h *UserHandler) registerPage(w http.ResponseWriter, r *http.Request) {
web.RenderTemplate(w, "register", data)
}
type RegisterParams struct {
FullName string `json:"full_name"`
Email string `json:"email"`
PhoneNumber string `json:"phone"`
Password string `json:"password"`
}
func (h *UserHandler) register(w http.ResponseWriter, r *http.Request) {
var params RegisterParams
decoder := json.NewDecoder(r.Body)
if err := decoder.Decode(&params); err != nil {
web.Error(w, "failed to parse request body", http.StatusBadRequest)
return
}
if params.Email == "" || params.FullName == "" || params.Password == "" {
web.Error(w, "missing required fields", http.StatusBadRequest)
return
}
_, err := h.repo.FindUserEmail(context.Background(), params.Email)
if err == nil {
web.Error(w, "user with provided email already exists", http.StatusBadRequest)
return
}
id, err := h.repo.InsertUser(context.Background(), repository.InsertUserParams{
FullName: params.FullName,
Email: params.Email,
PasswordHash: params.Password,
IsAdmin: false,
})
if err != nil {
web.Error(w, "failed to create new user", http.StatusInternalServerError)
return
}
encoder := json.NewEncoder(w)
type Response struct {
Id string `json:"id"`
}
if err := encoder.Encode(Response{
Id: id.String(),
}); err != nil {
web.Error(w, "failed to encode response", http.StatusInternalServerError)
}
}

17
internal/web/error.go Normal file
View File

@ -0,0 +1,17 @@
package web
import (
"encoding/json"
"net/http"
)
func Error(w http.ResponseWriter, err string, code int) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.WriteHeader(code)
_ = json.NewEncoder(w).Encode(map[string]any{
"error": err,
"status": code,
})
}

View File

@ -42,6 +42,49 @@
margin-top: 25px;
}
.validation_box {
display: none;
width: 100%;
padding: 5px;
background: #D0000011;
border: 1px solid #D00000aa;
border-radius: 5px;
margin: 10px 0;
}
.validation_box__msg {
color: #111;
font-size: 0.85rem;
}
.success_box {
display: none;
width: 100%;
padding: 5px;
background: #6AB54711;
border: 1px solid #6AB547aa;
border-radius: 5px;
margin: 10px 0;
}
.success_box__msg {
color: #111;
font-size: 0.85rem;
}
.login_link {
font-size: 0.8rem;
display: flex;
align-items: center;
flex-wrap: wrap;
justify-content: center;
margin: 20px 0 15px;
}
.login_link > a {
padding-left: 4px;
}
@media only screen and (max-width: 450px) {
.container {
padding: 0;

View File

@ -42,6 +42,49 @@
margin-top: 25px;
}
.validation_box {
display: none;
width: 100%;
padding: 5px;
background: #D0000011;
border: 1px solid #D00000aa;
border-radius: 5px;
margin: 10px 0;
}
.validation_box__msg {
color: #111;
font-size: 0.85rem;
}
.success_box {
display: none;
width: 100%;
padding: 5px;
background: #6AB54711;
border: 1px solid #6AB547aa;
border-radius: 5px;
margin: 10px 0;
}
.success_box__msg {
color: #111;
font-size: 0.85rem;
}
.login_link {
font-size: 0.8rem;
display: flex;
align-items: center;
flex-wrap: wrap;
justify-content: center;
margin: 20px 0 15px;
}
.login_link > a {
padding-left: 4px;
}
@media only screen and (max-width: 450px) {
.container {
padding: 0;

71
static/js/login.js Normal file
View File

@ -0,0 +1,71 @@
const $form = document.querySelector(".form")
const $validationBox = document.querySelector("#validationBox")
const $validationMsg = document.querySelector("#validationMsg")
const $successBox = document.querySelector("#successBox")
const $successMsg = document.querySelector("#successMsg")
const showError = (message) => {
$validationBox.style.display = "block"
$validationMsg.innerText = message
}
const clearError = () => {
$validationMsg.innerText = ""
$validationBox.style.display = "none"
}
const showSuccess = (message) => {
$successBox.style.display = "block"
$successMsg.innerText = message
}
const clearSuccess = () => {
$successMsg.innerText = ""
$successBox.style.display = "none"
}
document.addEventListener("DOMContentLoaded", (e) => {
$form.addEventListener("submit", async (e) => {
e.preventDefault()
clearError()
clearSuccess()
const formData = new FormData($form)
const data = Object.fromEntries(formData)
if ([{key: "email", name: "Email"}, {key: "password", name: "Password"}].some(({key, name}) => {
if (!data[key]) {
showError(`${name} is required`)
return true
}
return false;
})) {
return
}
try {
const response = await fetch("/api/v1/login", {
method: "POST",
body: JSON.stringify(data),
headers: {
"Content-Type": "application/json",
},
})
if (response.status != 200) {
const json = await response.json()
const text = json.error || "Unexpected error happened"
showError(`Failed to log into account. ${text[0].toUpperCase() + text.slice(1)}`)
} else {
showSuccess("Successfully logged into your account")
$form.reset()
}
} catch(err) {
showError("Failed to log into account. Unexpected error happened")
}
})
})

84
static/js/register.js Normal file
View File

@ -0,0 +1,84 @@
const $form = document.querySelector(".form")
const $validationBox = document.querySelector("#validationBox")
const $validationMsg = document.querySelector("#validationMsg")
const $successBox = document.querySelector("#successBox")
const $successMsg = document.querySelector("#successMsg")
const showError = (message) => {
$validationBox.style.display = "block"
$validationMsg.innerText = message
}
const clearError = () => {
$validationMsg.innerText = ""
$validationBox.style.display = "none"
}
const showSuccess = (message) => {
$successBox.style.display = "block"
$successMsg.innerText = message
}
const clearSuccess = () => {
$successMsg.innerText = ""
$successBox.style.display = "none"
}
document.addEventListener("DOMContentLoaded", (e) => {
$form.addEventListener("submit", async (e) => {
e.preventDefault()
clearError()
clearSuccess()
const formData = new FormData($form)
const data = Object.fromEntries(formData)
if ([{key: "full_name", name: "Full Name"}, {key: "email", name: "Email"}, {key: "password", name: "Password"}, {key: "repeat_password", name: "Password"}].some(({key, name}) => {
if (!data[key]) {
showError(`${name} is required`)
return true
}
return false;
})) {
return
}
if (data.repeat_password != data.password) {
console.log("passwords do not match")
showError("Password does not match")
return
}
if (data.terms_and_conditions !== "on") {
console.log("terms and conditions are not accepted")
showError("Terms and Conditions are not accepted. Cannot proceed without agreement")
return
}
try {
const response = await fetch("/api/v1/register", {
method: "POST",
body: JSON.stringify(data),
headers: {
"Content-Type": "application/json",
},
})
if (response.status != 200) {
const json = await response.json()
const text = json.error || "Unexpected error happened"
showError(`Failed to create an account. ${text[0].toUpperCase() + text.slice(1)}`)
} else {
showSuccess("Account has been created. You can now log into your new account")
$form.reset()
}
} catch(err) {
showError("Failed to create account. Unexpected error happened")
}
})
})

View File

@ -14,24 +14,32 @@
<div class="input-icon">
@
</div>
<input class="input-field" type="email" name="email" placeholder="Email" required>
<input class="input-field" type="email" name="email" placeholder="Email">
</div>
<div class="input-group">
<div class="input-icon">
<img src="/static/icons/padlock.png" alt="user">
</div>
<input class="input-field" type="password" name="password" placeholder="Password" required>
<input class="input-field" type="password" name="password" placeholder="Password">
</div>
<div class="checkbox-group">
<input type="checkbox" name="terms_and_conditions">
<div>
<p>By checking this checkbox I submit, that read and accepted terms and conditions of this service and home lab.</p>
</div>
<div class="validation_box" id="validationBox">
<p class="validation_box__msg" id="validationMsg">
</p>
</div>
<div class="success_box" id="successBox">
<p class="success_box__msg" id="successMsg">
</p>
</div>
<button class="button primary login-btn" type="submit">
Login
</button>
<div class="login_link">
Don't have an account? <a href="/register">Register</a>
</div>
</form>
<script src="/static/js/login.js"></script>
{{ end }}

View File

@ -14,31 +14,31 @@
<div class="input-icon">
<img src="/static/icons/user.png" alt="user">
</div>
<input class="input-field" type="text" name="full_name" placeholder="Full Name" required>
<input class="input-field" type="text" name="full_name" placeholder="Full Name*">
</div>
<div class="input-group">
<div class="input-icon">
@
</div>
<input class="input-field" type="email" name="email" placeholder="Email" required>
<input class="input-field" type="email" name="email" placeholder="Email*">
</div>
<div class="input-group">
<div class="input-icon">
<img src="/static/icons/telephone.png" alt="user">
</div>
<input class="input-field" type="tel" name="phone" placeholder="Phone Number" required>
<input class="input-field" type="tel" name="phone" placeholder="Phone Number">
</div>
<div class="input-group">
<div class="input-icon">
<img src="/static/icons/padlock.png" alt="user">
</div>
<input class="input-field" type="password" name="password" placeholder="Password" required>
<input class="input-field" type="password" name="password" placeholder="Password*">
</div>
<div class="input-group">
<div class="input-icon">
<img src="/static/icons/padlock.png" alt="user">
</div>
<input class="input-field" type="password" name="password" placeholder="Repeat Password" required>
<input class="input-field" type="password" name="repeat_password" placeholder="Repeat Password*">
</div>
<div class="checkbox-group">
@ -48,8 +48,23 @@
</div>
</div>
<div class="validation_box" id="validationBox">
<p class="validation_box__msg" id="validationMsg">
</p>
</div>
<div class="success_box" id="successBox">
<p class="success_box__msg" id="successMsg">
</p>
</div>
<button class="button primary register-btn" type="submit">
Register
</button>
<div class="login_link">
Already have an account? <a href="/login">Login</a>
</div>
</form>
<script src="/static/js/register.js"></script>
{{ end }}