diff options
| author | Bobby <[email protected]> | 2025-12-15 14:28:13 +0530 |
|---|---|---|
| committer | Bobby <[email protected]> | 2025-12-15 14:28:13 +0530 |
| commit | e143ba0b4a0fff8448124d86bb94e07742aa0a9b (patch) | |
| tree | 0dce19e2122b60c6a9e1a338fa884ba206be561c | |
| parent | 5f6e383d3799d39036842e00ae3149be7fafe188 (diff) | |
| download | imageboard-main.tar.xz imageboard-main.zip | |
| -rw-r--r-- | config/constants.go | 49 | ||||
| -rw-r--r-- | controllers/account.go | 279 | ||||
| -rw-r--r-- | controllers/login.go | 81 | ||||
| -rw-r--r-- | controllers/logout.go | 28 | ||||
| -rw-r--r-- | controllers/register.go | 89 | ||||
| -rw-r--r-- | database/user.go | 6 | ||||
| -rw-r--r-- | router/routes.go | 25 | ||||
| -rw-r--r-- | static/images/833de6dd73cb113ff6ebc631cdb14ee3.webp | bin | 0 -> 26816 bytes | |||
| -rw-r--r-- | static/images/cdcea50ffd8313b9b5418907e327be65.webp | bin | 0 -> 26976 bytes | |||
| -rw-r--r-- | templates/account/forgot.django | 52 | ||||
| -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.html | 75 | ||||
| -rw-r--r-- | templates/partials/navbar.django | 4 | ||||
| -rw-r--r-- | utils/email/email.go | 34 |
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 Binary files differnew file mode 100644 index 0000000..0e7d55c --- /dev/null +++ b/static/images/833de6dd73cb113ff6ebc631cdb14ee3.webp diff --git a/static/images/cdcea50ffd8313b9b5418907e327be65.webp b/static/images/cdcea50ffd8313b9b5418907e327be65.webp Binary files differnew file mode 100644 index 0000000..b6f1f3f --- /dev/null +++ b/static/images/cdcea50ffd8313b9b5418907e327be65.webp 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 { |
