Compare commits
7 Commits
2c1ba0a8fb
...
6927ebf0d3
Author | SHA1 | Date | |
---|---|---|---|
6927ebf0d3
|
|||
00808c1c61
|
|||
0198d4c348
|
|||
c1b6143503
|
|||
94a873f10a
|
|||
703fd3174b
|
|||
584ee8865d
|
@ -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
1
go.mod
@ -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
2
go.sum
@ -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=
|
||||
|
@ -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(¶ms); 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
17
internal/web/error.go
Normal 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,
|
||||
})
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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
71
static/js/login.js
Normal 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
84
static/js/register.js
Normal 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")
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -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 }}
|
||||
|
@ -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 }}
|
||||
|
Reference in New Issue
Block a user