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)
|
FileServer(router, "/static", staticDir)
|
||||||
|
|
||||||
router.Route("/api/v1", func(r chi.Router) {
|
router.Route("/api/v1", func(r chi.Router) {
|
||||||
userHandler := user.NewUserHandler()
|
userHandler := user.NewUserHandler(s.repo)
|
||||||
userHandler.RegisterRoutes(router, r)
|
userHandler.RegisterRoutes(router, r)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
1
go.mod
1
go.mod
@ -4,6 +4,7 @@ go 1.24.3
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/go-chi/chi/v5 v5.2.1 // indirect
|
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/google/uuid v1.6.0 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // 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/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 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
|
||||||
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
|
@ -1,21 +1,29 @@
|
|||||||
package user
|
package user
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"gitea.local/admin/hspguard/internal/repository"
|
||||||
"gitea.local/admin/hspguard/internal/web"
|
"gitea.local/admin/hspguard/internal/web"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserHandler struct{}
|
type UserHandler struct {
|
||||||
|
repo *repository.Queries
|
||||||
|
}
|
||||||
|
|
||||||
func NewUserHandler() *UserHandler {
|
func NewUserHandler(repo *repository.Queries) *UserHandler {
|
||||||
return &UserHandler{}
|
return &UserHandler{
|
||||||
|
repo: repo,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *UserHandler) RegisterRoutes(router chi.Router, api chi.Router) {
|
func (h *UserHandler) RegisterRoutes(router chi.Router, api chi.Router) {
|
||||||
router.Get("/login", h.loginPage)
|
router.Get("/login", h.loginPage)
|
||||||
router.Get("/register", h.registerPage)
|
router.Get("/register", h.registerPage)
|
||||||
|
api.Post("/register", h.register)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *UserHandler) loginPage(w http.ResponseWriter, r *http.Request) {
|
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)
|
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;
|
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) {
|
@media only screen and (max-width: 450px) {
|
||||||
.container {
|
.container {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
@ -42,6 +42,49 @@
|
|||||||
margin-top: 25px;
|
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) {
|
@media only screen and (max-width: 450px) {
|
||||||
.container {
|
.container {
|
||||||
padding: 0;
|
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 class="input-icon">
|
||||||
@
|
@
|
||||||
</div>
|
</div>
|
||||||
<input class="input-field" type="email" name="email" placeholder="Email" required>
|
<input class="input-field" type="email" name="email" placeholder="Email">
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<div class="input-icon">
|
<div class="input-icon">
|
||||||
<img src="/static/icons/padlock.png" alt="user">
|
<img src="/static/icons/padlock.png" alt="user">
|
||||||
</div>
|
</div>
|
||||||
<input class="input-field" type="password" name="password" placeholder="Password" required>
|
<input class="input-field" type="password" name="password" placeholder="Password">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="checkbox-group">
|
<div class="validation_box" id="validationBox">
|
||||||
<input type="checkbox" name="terms_and_conditions">
|
<p class="validation_box__msg" id="validationMsg">
|
||||||
<div>
|
</p>
|
||||||
<p>By checking this checkbox I submit, that read and accepted terms and conditions of this service and home lab.</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="success_box" id="successBox">
|
||||||
|
<p class="success_box__msg" id="successMsg">
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="button primary login-btn" type="submit">
|
<button class="button primary login-btn" type="submit">
|
||||||
Login
|
Login
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<div class="login_link">
|
||||||
|
Don't have an account? <a href="/register">Register</a>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
<script src="/static/js/login.js"></script>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
@ -14,31 +14,31 @@
|
|||||||
<div class="input-icon">
|
<div class="input-icon">
|
||||||
<img src="/static/icons/user.png" alt="user">
|
<img src="/static/icons/user.png" alt="user">
|
||||||
</div>
|
</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>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<div class="input-icon">
|
<div class="input-icon">
|
||||||
@
|
@
|
||||||
</div>
|
</div>
|
||||||
<input class="input-field" type="email" name="email" placeholder="Email" required>
|
<input class="input-field" type="email" name="email" placeholder="Email*">
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<div class="input-icon">
|
<div class="input-icon">
|
||||||
<img src="/static/icons/telephone.png" alt="user">
|
<img src="/static/icons/telephone.png" alt="user">
|
||||||
</div>
|
</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>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<div class="input-icon">
|
<div class="input-icon">
|
||||||
<img src="/static/icons/padlock.png" alt="user">
|
<img src="/static/icons/padlock.png" alt="user">
|
||||||
</div>
|
</div>
|
||||||
<input class="input-field" type="password" name="password" placeholder="Password" required>
|
<input class="input-field" type="password" name="password" placeholder="Password*">
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<div class="input-icon">
|
<div class="input-icon">
|
||||||
<img src="/static/icons/padlock.png" alt="user">
|
<img src="/static/icons/padlock.png" alt="user">
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div class="checkbox-group">
|
<div class="checkbox-group">
|
||||||
@ -48,8 +48,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<button class="button primary register-btn" type="submit">
|
||||||
Register
|
Register
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<div class="login_link">
|
||||||
|
Already have an account? <a href="/login">Login</a>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
<script src="/static/js/register.js"></script>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
Reference in New Issue
Block a user