aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--config/constants.go49
-rw-r--r--controllers/account.go279
-rw-r--r--controllers/login.go81
-rw-r--r--controllers/logout.go28
-rw-r--r--controllers/register.go89
-rw-r--r--database/user.go6
-rw-r--r--router/routes.go25
-rw-r--r--static/images/833de6dd73cb113ff6ebc631cdb14ee3.webpbin0 -> 26816 bytes
-rw-r--r--static/images/cdcea50ffd8313b9b5418907e327be65.webpbin0 -> 26976 bytes
-rw-r--r--templates/account/forgot.django52
-rw-r--r--templates/account/login.django (renamed from templates/login.django)5
-rw-r--r--templates/account/register.django (renamed from templates/register.django)4
-rw-r--r--templates/email/forgot_username.html75
-rw-r--r--templates/partials/navbar.django4
-rw-r--r--utils/email/email.go34
15 files changed, 497 insertions, 234 deletions
diff --git a/config/constants.go b/config/constants.go
index 96c5851..34c2ba1 100644
--- a/config/constants.go
+++ b/config/constants.go
@@ -2,40 +2,43 @@ package config
const (
// Page titles
- PT_HOME = "Home Page"
- PT_LOGIN = "Login"
- PT_POST_LIST = "Posts"
- PT_POST_NEW = "Upload New Post"
- PT_POST_SINGLE = "Post"
- PT_POST_EDIT = "Edit Post"
- PT_PREFERENCES = "Preferences"
- PT_REGISTER = "Register"
- PT_404 = "Page Not Found"
- PT_VERIFY_EMAIL = "Verify Email"
+ PT_HOME = "Home Page"
+ PT_LOGIN = "Login"
+ PT_FORGOT_PASSWORD = "Forgot Password"
+ PT_FORGOT_USERNAME = "Forgot Username"
+ PT_POST_LIST = "Posts"
+ PT_POST_NEW = "Upload New Post"
+ PT_POST_SINGLE = "Post"
+ PT_POST_EDIT = "Edit Post"
+ PT_PREFERENCES = "Preferences"
+ PT_REGISTER = "Register"
+ PT_404 = "Page Not Found"
+ PT_VERIFY_EMAIL = "Verify Email"
// Template names
TEMPLATE_HOME = "home"
- TEMPLATE_LOGIN = "login"
+ TEMPLATE_LOGIN = "account/login"
+ TEMPLATE_REGISTER = "account/register"
+ TEMPLATE_FORGOT = "account/forgot"
+ TEMPLATE_VERIFY_EMAIL = "account/verify_email"
TEMPLATE_POST_LIST = "posts/list"
TEMPLATE_POST_NEW = "posts/new"
TEMPLATE_POST_SINGLE = "posts/single"
TEMPLATE_POST_EDIT = "posts/edit"
TEMPLATE_PREFERENCES = "preferences"
- TEMPLATE_REGISTER = "register"
TEMPLATE_ERROR = "error"
- TEMPLATE_VERIFY_EMAIL = "account/verify_email"
// URL constants for various routes
URL_HOME = "/"
- URL_LOGIN = "/login"
- URL_LOGOUT = "/logout"
- URL_POST_LIST = "/posts"
- URL_POST_NEW = "/posts/new"
- URL_PREFERENCES = "/preferences"
- URL_REGISTER = "/register"
+ URL_LOGIN = "/account/login"
+ URL_LOGOUT = "/account/logout"
+ URL_REGISTER = "/account/register"
URL_VERIFY_EMAIL = "/account/verify"
URL_FORGOT_PASSWORD = "/account/forgot-password"
URL_RESEND_VERIFICATION = "/account/resend-verification"
+ URL_POST_LIST = "/posts"
+ URL_POST_NEW = "/posts/new"
+ URL_PREFERENCES = "/preferences"
// Error messages
ERR_INVALID_FORM_DATA = "The submitted form data is invalid. Check your input and try again."
@@ -54,6 +57,10 @@ const (
ERR_VERIFY_EMAIL_ACTIVATION_FAILED = `Failed to activate your account. If this issue persists, contact support.`
// Success messages
- SUCCESS_USER_REGISTERED = "Your account has been created successfully. A verification email has been sent to your email address. You will only be able to log in after verifying your email. If you did not receive the email, you can <a href=\"" + URL_RESEND_VERIFICATION + "\">request a new one</a>."
- SUCCESS_VERIFY_EMAIL = `Your email has been successfully verified. You can now <a href="` + URL_LOGIN + `">log in</a> to your account.`
+ SUCCESS_USER_REGISTERED = "Your account has been created successfully. A verification email has been sent to your email address. You will only be able to log in after verifying your email. If you did not receive the email, you can <a href=\"" + URL_RESEND_VERIFICATION + "\">request a new one</a>."
+ SUCCESS_VERIFY_EMAIL = `Your email has been successfully verified. You can now <a href="` + URL_LOGIN + `">log in</a> to your account.`
+ SUCCESS_FORGOT_USERNAME_EMAIL_SENT = "An email has been sent to your email address with all your associated usernames."
+
+ // Non Existent User
+ ERR_NO_ACCOUNT_ASSOCIATED_WITH_EMAIL = "No account is associated with the provided email address. Check for typos or consider registering for a new account."
)
diff --git a/controllers/account.go b/controllers/account.go
index d59da8e..df0ccdb 100644
--- a/controllers/account.go
+++ b/controllers/account.go
@@ -3,12 +3,291 @@ package controllers
import (
"imageboard/config"
"imageboard/database"
+ "imageboard/models"
+ "imageboard/session"
"imageboard/utils/auth"
+ "imageboard/utils/email"
"imageboard/utils/shortcuts"
+ "log"
+ "strings"
"github.com/gofiber/fiber/v2"
)
+type LoginForm struct {
+ Username string `json:"username" form:"username"`
+ Password string `json:"password" form:"password"`
+}
+
+func LoginPageController(ctx *fiber.Ctx) error {
+ ctx.Locals("Title", config.PT_LOGIN)
+
+ if auth.IsAuthenticated(ctx) {
+ return ctx.Redirect(auth.GetRedirectURL(ctx), fiber.StatusSeeOther)
+ }
+
+ next := ctx.Query("next")
+ return shortcuts.Render(ctx, config.TEMPLATE_LOGIN, fiber.Map{
+ "Next": next,
+ })
+}
+
+func LoginPostController(ctx *fiber.Ctx) error {
+ ctx.Locals("Title", config.PT_LOGIN)
+
+ var form LoginForm
+ var err error
+ handleLoginError := func(errorMessage string, statusCode int) error {
+ return TemplateErrorController(ctx, TemplateError{
+ Template: config.TEMPLATE_LOGIN,
+ ErrorMessage: errorMessage,
+ StatusCode: statusCode,
+ }, fiber.Map{
+ "Username": form.Username,
+ })
+ }
+
+ if err = ctx.BodyParser(&form); err != nil {
+ return handleLoginError(config.ERR_INVALID_FORM_DATA, fiber.StatusBadRequest)
+ }
+
+ user, err := database.GetUserByUsername(form.Username)
+ if err != nil {
+ return handleLoginError(config.ERR_USER_NOT_FOUND, fiber.StatusUnauthorized)
+ }
+
+ if !user.CheckPassword(form.Password) {
+ return handleLoginError(config.ERR_LOGIN_INVALID_CREDENTIALS, fiber.StatusUnauthorized)
+ }
+
+ if !user.IsActive() {
+ return handleLoginError(config.ERR_ACCOUNT_DISABLED, fiber.StatusForbidden)
+ }
+
+ if !user.CanLogin() {
+ return handleLoginError(config.ERR_ACCOUNT_UNABLE_TO_LOGIN, fiber.StatusForbidden)
+ }
+
+ sess, err := session.Store.Get(ctx)
+ if err != nil {
+ return handleLoginError(config.ERR_SESSION_FAILED_TO_CREATE, fiber.StatusInternalServerError)
+ }
+ sess.Set("user_id", user.ID)
+ sess.Set("username", user.Username)
+ if err := sess.Save(); err != nil {
+ return handleLoginError(config.ERR_SESSION_FAILED_TO_SAVE, fiber.StatusInternalServerError)
+ }
+
+ user.UpdateLastUserLogin(database.DB)
+ user.UpdateLastUserActivity(database.DB)
+
+ return ctx.Redirect(auth.GetRedirectURL(ctx), fiber.StatusSeeOther)
+}
+
+func LogoutController(ctx *fiber.Ctx) error {
+ sess, err := session.Store.Get(ctx)
+ if err != nil {
+ return ctx.Redirect(config.URL_HOME, fiber.StatusSeeOther)
+ }
+
+ if err := sess.Destroy(); err != nil {
+ sess.Delete("user_id")
+ sess.Delete("username")
+ sess.Save()
+ }
+
+ next := ctx.Query("next")
+ if next != "" {
+ return ctx.Redirect(next, fiber.StatusSeeOther)
+ }
+
+ return ctx.Redirect(config.URL_HOME, fiber.StatusSeeOther)
+}
+
+type RegisterForm struct {
+ Username string `json:"username" form:"username"`
+ Email string `json:"email" form:"email"`
+ Password string `json:"password" form:"password"`
+ ConfirmPassword string `json:"confirm_password" form:"confirm_password"`
+}
+
+func RegisterPageController(ctx *fiber.Ctx) error {
+ ctx.Locals("Title", config.PT_REGISTER)
+
+ if auth.IsAuthenticated(ctx) {
+ return ctx.Redirect(auth.GetRedirectURL(ctx), fiber.StatusSeeOther)
+ }
+
+ return shortcuts.Render(ctx, config.TEMPLATE_REGISTER, nil)
+}
+
+func RegisterPostController(ctx *fiber.Ctx) error {
+ ctx.Locals("Title", config.PT_REGISTER)
+
+ if auth.IsAuthenticated(ctx) {
+ return ctx.Redirect(auth.GetRedirectURL(ctx), fiber.StatusSeeOther)
+ }
+
+ var form RegisterForm
+ handleRegisterError := func(errorMessage string, statusCode int) error {
+ return TemplateErrorController(ctx, TemplateError{
+ Template: config.TEMPLATE_REGISTER,
+ ErrorMessage: errorMessage,
+ StatusCode: statusCode,
+ }, fiber.Map{
+ "Username": form.Username,
+ "Email": form.Email,
+ })
+ }
+
+ if err := ctx.BodyParser(&form); err != nil {
+ return handleRegisterError(config.ERR_INVALID_FORM_DATA, fiber.StatusBadRequest)
+ }
+
+ if form.Password != form.ConfirmPassword {
+ return handleRegisterError(config.ERR_PASSWORD_MISMATCH, fiber.StatusBadRequest)
+ }
+
+ user := &models.User{
+ Username: form.Username,
+ Email: form.Email,
+ Password: form.Password,
+ PostsRequireApproval: true,
+ Level: config.UserLevelMember,
+ }
+
+ if err := database.CreateUser(user); err != nil {
+ var statusCode int
+ if strings.Contains(err.Error(), "username") {
+ statusCode = fiber.StatusConflict
+ } else if strings.Contains(err.Error(), "email") {
+ statusCode = fiber.StatusBadRequest
+ } else {
+ statusCode = fiber.StatusInternalServerError
+ }
+
+ return handleRegisterError(config.ERR_REGISTER_FAILED_TO_CREATE_USER+err.Error(), statusCode)
+ }
+
+ if err := email.SendVerificationEmail(user); err != nil {
+ log.Printf("Failed to send verification email: %v", err)
+ return handleRegisterError(config.ERR_REGISTER_USER_CREATED_EMAIL_FAILED, fiber.StatusInternalServerError)
+ }
+
+ return shortcuts.Render(ctx, config.TEMPLATE_REGISTER, fiber.Map{
+ "Success": config.SUCCESS_USER_REGISTERED,
+ })
+}
+
+func ForgotPasswordPageController(ctx *fiber.Ctx) error {
+ mode := ctx.Query("mode", "username")
+ switch mode {
+ case "username":
+ ctx.Locals("Title", config.PT_FORGOT_USERNAME)
+ case "password":
+ ctx.Locals("Title", config.PT_FORGOT_PASSWORD)
+ default:
+ ctx.Locals("Title", config.PT_FORGOT_USERNAME)
+ mode = "username"
+ }
+
+ if auth.IsAuthenticated(ctx) {
+ return ctx.Redirect(auth.GetRedirectURL(ctx), fiber.StatusSeeOther)
+ }
+
+ return shortcuts.Render(ctx, config.TEMPLATE_FORGOT, fiber.Map{
+ "Mode": mode,
+ })
+}
+
+type ForgotPasswordInput struct {
+ Email string `json:"email" form:"email"`
+ Mode string `json:"mode" form:"mode"`
+}
+
+func ForgotPasswordPostController(ctx *fiber.Ctx) error {
+ ctx.Locals("Title", config.PT_FORGOT_PASSWORD)
+
+ if auth.IsAuthenticated(ctx) {
+ return ctx.Redirect(auth.GetRedirectURL(ctx), fiber.StatusSeeOther)
+ }
+
+ var input ForgotPasswordInput
+ if err := ctx.BodyParser(&input); err != nil {
+ return TemplateErrorController(ctx, TemplateError{
+ Template: config.TEMPLATE_FORGOT,
+ ErrorMessage: config.ERR_INVALID_FORM_DATA,
+ StatusCode: fiber.StatusBadRequest,
+ }, fiber.Map{
+ "Mode": input.Mode,
+ })
+ }
+
+ switch input.Mode {
+ case "password":
+ ctx.Locals("Title", config.PT_FORGOT_PASSWORD)
+ case "username":
+ ctx.Locals("Title", config.PT_FORGOT_USERNAME)
+ default:
+ ctx.Locals("Title", config.PT_FORGOT_USERNAME)
+ input.Mode = "username"
+ }
+
+ users, err := database.GetUsersByEmail(input.Email)
+ if err != nil || len(users) == 0 {
+ return TemplateErrorController(ctx, TemplateError{
+ Template: config.TEMPLATE_FORGOT,
+ ErrorMessage: config.ERR_NO_ACCOUNT_ASSOCIATED_WITH_EMAIL,
+ StatusCode: fiber.StatusNotFound,
+ }, fiber.Map{
+ "Mode": input.Mode,
+ })
+ }
+
+ switch mode := input.Mode; mode {
+ case "username":
+ if err := email.SendForgotUsernameEmail(&users); err != nil {
+ log.Printf("Failed to send forgot username email: %v", err)
+ return TemplateErrorController(ctx, TemplateError{
+ Template: config.TEMPLATE_FORGOT,
+ ErrorMessage: "Failed to send username email. Please try again later.",
+ StatusCode: fiber.StatusInternalServerError,
+ }, fiber.Map{
+ "Mode": input.Mode,
+ })
+ }
+ case "password":
+ // TODO
+ default:
+ return TemplateErrorController(ctx, TemplateError{
+ Template: config.TEMPLATE_FORGOT,
+ ErrorMessage: config.ERR_INVALID_FORM_DATA,
+ StatusCode: fiber.StatusBadRequest,
+ }, fiber.Map{
+ "Mode": input.Mode,
+ })
+ }
+
+ switch input.Mode {
+ case "username":
+ return shortcuts.Render(ctx, config.TEMPLATE_FORGOT, fiber.Map{
+ "Success": config.SUCCESS_FORGOT_USERNAME_EMAIL_SENT,
+ "Mode": input.Mode,
+ })
+ case "password":
+ // TODO
+ return shortcuts.Render(ctx, config.TEMPLATE_FORGOT, fiber.Map{
+ "Success": "If an account with that email exists, a password reset email has been sent.",
+ "Mode": input.Mode,
+ })
+ default:
+ return shortcuts.Render(ctx, config.TEMPLATE_FORGOT, fiber.Map{
+ "Success": config.SUCCESS_FORGOT_USERNAME_EMAIL_SENT,
+ "Mode": input.Mode,
+ })
+ }
+}
+
func VerifyEmailController(ctx *fiber.Ctx) error {
ctx.Locals("Title", config.PT_VERIFY_EMAIL)
if auth.IsAuthenticated(ctx) {
diff --git a/controllers/login.go b/controllers/login.go
deleted file mode 100644
index 64ad047..0000000
--- a/controllers/login.go
+++ /dev/null
@@ -1,81 +0,0 @@
-package controllers
-
-import (
- "imageboard/config"
- "imageboard/database"
- "imageboard/session"
- "imageboard/utils/auth"
- "imageboard/utils/shortcuts"
-
- "github.com/gofiber/fiber/v2"
-)
-
-type LoginForm struct {
- Username string `json:"username" form:"username"`
- Password string `json:"password" form:"password"`
-}
-
-func LoginPageController(ctx *fiber.Ctx) error {
- ctx.Locals("Title", config.PT_LOGIN)
-
- if auth.IsAuthenticated(ctx) {
- return ctx.Redirect(auth.GetRedirectURL(ctx), fiber.StatusSeeOther)
- }
-
- next := ctx.Query("next")
- return shortcuts.Render(ctx, config.TEMPLATE_LOGIN, fiber.Map{
- "Next": next,
- })
-}
-
-func LoginPostController(ctx *fiber.Ctx) error {
- ctx.Locals("Title", config.PT_LOGIN)
-
- var form LoginForm
- var err error
- handleLoginError := func(errorMessage string, statusCode int) error {
- return TemplateErrorController(ctx, TemplateError{
- Template: config.TEMPLATE_LOGIN,
- ErrorMessage: errorMessage,
- StatusCode: statusCode,
- }, fiber.Map{
- "Username": form.Username,
- })
- }
-
- if err = ctx.BodyParser(&form); err != nil {
- return handleLoginError(config.ERR_INVALID_FORM_DATA, fiber.StatusBadRequest)
- }
-
- user, err := database.GetUserByUsername(form.Username)
- if err != nil {
- return handleLoginError(config.ERR_USER_NOT_FOUND, fiber.StatusUnauthorized)
- }
-
- if !user.CheckPassword(form.Password) {
- return handleLoginError(config.ERR_LOGIN_INVALID_CREDENTIALS, fiber.StatusUnauthorized)
- }
-
- if !user.IsActive() {
- return handleLoginError(config.ERR_ACCOUNT_DISABLED, fiber.StatusForbidden)
- }
-
- if !user.CanLogin() {
- return handleLoginError(config.ERR_ACCOUNT_UNABLE_TO_LOGIN, fiber.StatusForbidden)
- }
-
- sess, err := session.Store.Get(ctx)
- if err != nil {
- return handleLoginError(config.ERR_SESSION_FAILED_TO_CREATE, fiber.StatusInternalServerError)
- }
- sess.Set("user_id", user.ID)
- sess.Set("username", user.Username)
- if err := sess.Save(); err != nil {
- return handleLoginError(config.ERR_SESSION_FAILED_TO_SAVE, fiber.StatusInternalServerError)
- }
-
- user.UpdateLastUserLogin(database.DB)
- user.UpdateLastUserActivity(database.DB)
-
- return ctx.Redirect(auth.GetRedirectURL(ctx), fiber.StatusSeeOther)
-}
diff --git a/controllers/logout.go b/controllers/logout.go
deleted file mode 100644
index 58ff545..0000000
--- a/controllers/logout.go
+++ /dev/null
@@ -1,28 +0,0 @@
-package controllers
-
-import (
- "imageboard/config"
- "imageboard/session"
-
- "github.com/gofiber/fiber/v2"
-)
-
-func LogoutController(ctx *fiber.Ctx) error {
- sess, err := session.Store.Get(ctx)
- if err != nil {
- return ctx.Redirect(config.URL_HOME, fiber.StatusSeeOther)
- }
-
- if err := sess.Destroy(); err != nil {
- sess.Delete("user_id")
- sess.Delete("username")
- sess.Save()
- }
-
- next := ctx.Query("next")
- if next != "" {
- return ctx.Redirect(next, fiber.StatusSeeOther)
- }
-
- return ctx.Redirect(config.URL_HOME, fiber.StatusSeeOther)
-}
diff --git a/controllers/register.go b/controllers/register.go
deleted file mode 100644
index 6d1383f..0000000
--- a/controllers/register.go
+++ /dev/null
@@ -1,89 +0,0 @@
-package controllers
-
-import (
- "imageboard/config"
- "imageboard/database"
- "imageboard/models"
- "imageboard/utils/auth"
- "imageboard/utils/email"
- "imageboard/utils/shortcuts"
- "log"
- "strings"
-
- "github.com/gofiber/fiber/v2"
-)
-
-type RegisterForm struct {
- Username string `json:"username" form:"username"`
- Email string `json:"email" form:"email"`
- Password string `json:"password" form:"password"`
- ConfirmPassword string `json:"confirm_password" form:"confirm_password"`
-}
-
-func RegisterPageController(ctx *fiber.Ctx) error {
- ctx.Locals("Title", config.PT_REGISTER)
-
- if auth.IsAuthenticated(ctx) {
- return ctx.Redirect(auth.GetRedirectURL(ctx), fiber.StatusSeeOther)
- }
-
- return shortcuts.Render(ctx, config.TEMPLATE_REGISTER, nil)
-}
-
-func RegisterPostController(ctx *fiber.Ctx) error {
- ctx.Locals("Title", config.PT_REGISTER)
-
- if auth.IsAuthenticated(ctx) {
- return ctx.Redirect(auth.GetRedirectURL(ctx), fiber.StatusSeeOther)
- }
-
- var form RegisterForm
- handleRegisterError := func(errorMessage string, statusCode int) error {
- return TemplateErrorController(ctx, TemplateError{
- Template: config.TEMPLATE_REGISTER,
- ErrorMessage: errorMessage,
- StatusCode: statusCode,
- }, fiber.Map{
- "Username": form.Username,
- "Email": form.Email,
- })
- }
-
- if err := ctx.BodyParser(&form); err != nil {
- return handleRegisterError(config.ERR_INVALID_FORM_DATA, fiber.StatusBadRequest)
- }
-
- if form.Password != form.ConfirmPassword {
- return handleRegisterError(config.ERR_PASSWORD_MISMATCH, fiber.StatusBadRequest)
- }
-
- user := &models.User{
- Username: form.Username,
- Email: form.Email,
- Password: form.Password,
- PostsRequireApproval: true,
- Level: config.UserLevelMember,
- }
-
- if err := database.CreateUser(user); err != nil {
- var statusCode int
- if strings.Contains(err.Error(), "username") {
- statusCode = fiber.StatusConflict
- } else if strings.Contains(err.Error(), "email") {
- statusCode = fiber.StatusBadRequest
- } else {
- statusCode = fiber.StatusInternalServerError
- }
-
- return handleRegisterError(config.ERR_REGISTER_FAILED_TO_CREATE_USER+err.Error(), statusCode)
- }
-
- if err := email.SendVerificationEmail(user); err != nil {
- log.Printf("Failed to send verification email: %v", err)
- return handleRegisterError(config.ERR_REGISTER_USER_CREATED_EMAIL_FAILED, fiber.StatusInternalServerError)
- }
-
- return shortcuts.Render(ctx, config.TEMPLATE_REGISTER, fiber.Map{
- "Success": config.SUCCESS_USER_REGISTERED,
- })
-}
diff --git a/database/user.go b/database/user.go
index cae46ae..f2ba5cb 100644
--- a/database/user.go
+++ b/database/user.go
@@ -13,6 +13,12 @@ func GetUserByUsername(username string) (*models.User, error) {
return &user, nil
}
+func GetUsersByEmail(email string) ([]models.User, error) {
+ var users []models.User
+ err := DB.Where("email = ?", email).Find(&users).Error
+ return users, err
+}
+
func ListAllUsers() ([]models.User, error) {
var users []models.User
err := DB.Where("is_deleted = ?", false).Order("LOWER(username) ASC").Find(&users).Error
diff --git a/router/routes.go b/router/routes.go
index f94e060..9842ece 100644
--- a/router/routes.go
+++ b/router/routes.go
@@ -24,24 +24,31 @@ func Initialize(router *fiber.App) {
tags := router.Group("/tags")
tags.Get("/search.json", controllers.TagsSearchJSONController)
- tags.Post("/create.json", controllers.FindOrCreateTagJSONController)
tags.Get("/search_for_image.json", controllers.TagsSearchForImageJSONController)
+ tags.Post("/create.json", controllers.FindOrCreateTagJSONController)
tags.Post("/add_to_image.json", controllers.TagsAddToImageJSONController)
tags.Post("/remove_from_image.json", controllers.TagsRemoveFromImageJSONController)
- login := router.Group("/login")
- login.Get("/", controllers.LoginPageController)
- login.Post("/", controllers.LoginPostController)
+ // login := router.Group("/login")
+ // login.Get("/", controllers.LoginPageController)
+ // login.Post("/", controllers.LoginPostController)
- logout := router.Group("/logout")
- logout.Get("/", controllers.LogoutController)
+ // logout := router.Group("/logout")
+ // logout.Get("/", controllers.LogoutController)
- register := router.Group("/register")
- register.Get("/", controllers.RegisterPageController)
- register.Post("/", controllers.RegisterPostController)
+ // register := router.Group("/register")
+ // register.Get("/", controllers.RegisterPageController)
+ // register.Post("/", controllers.RegisterPostController)
account := router.Group("/account")
+ account.Get("/login", controllers.LoginPageController)
+ account.Get("/forgot", controllers.ForgotPasswordPageController)
+ account.Get("/logout", controllers.LogoutController)
+ account.Get("/register", controllers.RegisterPageController)
account.Get("/verify", controllers.VerifyEmailController)
+ account.Post("/register", controllers.RegisterPostController)
+ account.Post("/login", controllers.LoginPostController)
+ account.Post("/forgot", controllers.ForgotPasswordPostController)
preferences := router.Group("/preferences")
preferences.Get("/", controllers.PreferencesPageController)
diff --git a/static/images/833de6dd73cb113ff6ebc631cdb14ee3.webp b/static/images/833de6dd73cb113ff6ebc631cdb14ee3.webp
new file mode 100644
index 0000000..0e7d55c
--- /dev/null
+++ b/static/images/833de6dd73cb113ff6ebc631cdb14ee3.webp
Binary files differ
diff --git a/static/images/cdcea50ffd8313b9b5418907e327be65.webp b/static/images/cdcea50ffd8313b9b5418907e327be65.webp
new file mode 100644
index 0000000..b6f1f3f
--- /dev/null
+++ b/static/images/cdcea50ffd8313b9b5418907e327be65.webp
Binary files differ
diff --git a/templates/account/forgot.django b/templates/account/forgot.django
new file mode 100644
index 0000000..deb9d16
--- /dev/null
+++ b/templates/account/forgot.django
@@ -0,0 +1,52 @@
+{% extends 'layouts/main.django' %}
+
+{% block content %}
+ <div class="centered-main">
+ <div class="bordered-box">
+ {% if Mode == "username" %}
+ <img src="/static/images/cdcea50ffd8313b9b5418907e327be65.webp" alt="Forgot Username" class="q-img">
+ {% else %}
+ <img src="/static/images/833de6dd73cb113ff6ebc631cdb14ee3.webp" alt="Forgot Password" class="q-img">
+ {% endif %}
+ <h1>Forgot your {{ Mode|capfirst }}?</h1>
+ <p>No worries! Just enter your email address below and we'll send you an email with {% if Mode == "username" %}your username{% else %}a link to reset your password{% endif %}.</p>
+ <form method="post" action="/account/forgot" class="ibform">
+ {% if Error %}
+ <div class="error">{{ Error|safe }}</div>
+ {% endif %}
+ {% if Success %}
+ <div class="success">{{ Success|safe }}</div>
+ {% endif %}
+ {% if Mode == "password" %}
+ <div class="fgroup">
+ <div class="fg-main">
+ <label for="username">Username</label>
+ </div>
+ <div class="fg-sub">
+ <input type="text" class="itext" id="username" name="username" required value="{{ Username }}" maxlength="72" autocomplete="username" pattern="[a-zA-Z0-9_-]+" />
+ <small>Enter username for which you want to reset the password for. 3-72 characters, letters, numbers, underscores, and hyphens only</small>
+ </div>
+ </div>
+ {% endif %}
+ <div class="fgroup">
+ <div class="fg-main">
+ <label for="email">Email Address</label>
+ </div>
+ <div class="fg-sub">
+ <input type="email" id="email" name="email" required value="{{ Email }}" />
+ </div>
+ </div>
+ <input type="hidden" name="mode" value="{{ Mode }}" />
+ <div class="fbtngrp">
+ <input type="submit" value="Send Email" />
+ <input type="button" value="Clear" onclick="this.form.reset();" />
+ {% if Mode == "username" %}
+ <input type="button" value="Forgot Password?" onclick="window.location.href='/account/forgot?mode=password';" />
+ {% else %}
+ <input type="button" value="Forgot Username?" onclick="window.location.href='/account/forgot?mode=username';" />
+ {% endif %}
+ </div>
+ </form>
+ </div>
+ </div>
+{% endblock %}
diff --git a/templates/login.django b/templates/account/login.django
index 428c260..2627012 100644
--- a/templates/login.django
+++ b/templates/account/login.django
@@ -6,7 +6,7 @@
<img src="/static/images/25631a9833b39de4053f9eed8b2d3ae6.webp" alt="Login Image" class="q-img" />
<h1>Login to {{ Appname }}</h1>
<p>Welcome back! Please enter your credentials to continue.</p>
- <form action="/login" method="POST" class="ibform">
+ <form action="/account/login" method="POST" class="ibform">
{% if Next %}
<input type="hidden" name="next" value="{{ Next }}" />
{% endif %}
@@ -33,7 +33,8 @@
<div class="fbtngrp">
<input type="submit" value="Login" />
<input type="button" value="Clear" onclick="this.form.reset();" />
- <input type="button" value="Forgot Password?" onclick="window.location.href='/account/forgot-password';" />
+ <input type="button" value="Forgot Password?" onclick="window.location.href='/account/forgot?mode=password';" />
+ <input type="button" value="Forgot Username?" onclick="window.location.href='/account/forgot?mode=username';" />
</div>
</form>
<p class="text-center">
diff --git a/templates/register.django b/templates/account/register.django
index b1c344f..592ffa5 100644
--- a/templates/register.django
+++ b/templates/account/register.django
@@ -6,7 +6,7 @@
<img src="/static/images/1c8fcc330ea1e971440cd3bdb8993a81.webp" alt="Register Image" class="q-img" />
<h1>Join {{ Appname }}</h1>
<p>Create your account to start sharing and exploring images!</p>
- <form action="/register" method="POST" class="ibform">
+ <form action="/account/register" method="POST" class="ibform">
{% if Error %}
<div class="error">{{ Error|safe }}</div>
{% endif %}
@@ -53,7 +53,7 @@
</div>
</form>
<p>
- Already have an account? <a href="/login">Login here</a>
+ Already have an account? <a href="/account/login">Login here</a>
</p>
</div>
</div>
diff --git a/templates/email/forgot_username.html b/templates/email/forgot_username.html
new file mode 100644
index 0000000..89fa982
--- /dev/null
+++ b/templates/email/forgot_username.html
@@ -0,0 +1,75 @@
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>Verify your email</title>
+ <style>
+ body {
+ background: #000033;
+ color: #ccccff;
+ font-family: monospace;
+ margin: 0;
+ padding: 0;
+ font-size: 13px;
+ text-align: justify;
+ }
+ .container {
+ background: #0d001a;
+ border: 1px solid #ff99cc;
+ max-width: 768px;
+ margin: 40px auto;
+ padding: 16px;
+ }
+ h1 {
+ color: #ffccff;
+ margin-bottom: 8px;
+ font-size: 16px;
+ }
+ p {
+ color: #99ffcc;
+ line-height: 18px;
+ }
+ .verify-link {
+ display: inline-block;
+ background: #330066;
+ color: #ccffcc;
+ border: 1px solid #99ffcc;
+ padding: 10px 24px;
+ text-decoration: none;
+ font-weight: bold;
+ margin: 12px auto;
+ }
+ .verify-link:hover {
+ background: #ff99cc;
+ color: #1a001a;
+ border-color: #ff99cc;
+ }
+ .footer {
+ color: #ffccff;
+ margin-top: 32px;
+ font-size: 11px;
+ }
+ </style>
+ </head>
+ <body>
+ <div class="container">
+ <h1>We heard you forgot your username?</h1>
+ <p>
+ No worries! The username(s) associated with your email on {{ .Appname }} are: <strong>{{ .Username }}</strong>
+ </p>
+ <p>
+ In case you forgot your password as well, you can reset it by clicking the link below:
+ </p>
+ <a class="verify-link" href="{{ .Link }}">Reset Password</a>
+ <p>
+ If you are unable to click the link, copy and paste it into your browser:
+ </p>
+ <p>
+ <a href="{{ .Link }}">{{ .Link }}</a>
+ </p>
+ <div class="footer">
+ If you did not request this email, you can safely ignore it.
+ </div>
+ </div>
+ </body>
+</html>
diff --git a/templates/partials/navbar.django b/templates/partials/navbar.django
index 2287302..8a30a19 100644
--- a/templates/partials/navbar.django
+++ b/templates/partials/navbar.django
@@ -16,8 +16,8 @@
<a href="{{ LogoutURL }}">Logout</a>
{% else %}
<span class="user-status">Guest</span>
- <a href="/login">Login</a>
- <a href="/register">Register</a>
+ <a href="/account/login">Login</a>
+ <a href="/account/register">Register</a>
{% endif %}
<a href="/preferences">Preferences</a>
<a href="/help">Help</a>
diff --git a/utils/email/email.go b/utils/email/email.go
index 168da25..4877cd5 100644
--- a/utils/email/email.go
+++ b/utils/email/email.go
@@ -65,6 +65,40 @@ func SendVerificationEmail(user *models.User) error {
return SendMail(user.Email, subject, body.String())
}
+func SendForgotUsernameEmail(users *[]models.User) error {
+ tmpl, err := template.ParseFiles("templates/email/forgot_username.html")
+ if err != nil {
+ return fmt.Errorf("failed to parse email template: %w", err)
+ }
+ resetLink := fmt.Sprintf("%s%s?mode=password", config.Server.AppBaseURL, config.URL_FORGOT_PASSWORD)
+ var usernames string
+
+ for i, user := range *users {
+ usernames += user.Username
+ if i < len(*users)-1 {
+ usernames += ", "
+ }
+ }
+
+ data := struct {
+ Username string
+ Appname string
+ Link string
+ }{
+ Username: usernames,
+ Appname: config.Server.AppName,
+ Link: resetLink,
+ }
+
+ var body bytes.Buffer
+ if err := tmpl.Execute(&body, data); err != nil {
+ return fmt.Errorf("failed to execute email template: %w", err)
+ }
+
+ subject := fmt.Sprintf("Your username for %s", config.Server.AppName)
+ return SendMail((*users)[0].Email, subject, body.String())
+}
+
// func SendPasswordResetEmail(user *models.User) error {
// token, err := user.GenerateToken(database.DB, models.EmailTokenTypePasswordReset)
// if err != nil {