diff options
| author | Bobby <[email protected]> | 2026-03-07 14:25:54 +0530 |
|---|---|---|
| committer | Bobby <[email protected]> | 2026-03-07 14:25:54 +0530 |
| commit | 41926c10ea2e8496ce4b528262f5047ccbe6f155 (patch) | |
| tree | 67ff5a27bb50faec368cf7ef38a1b1f2866459dd | |
| parent | ed2a033d7c08e448f5c6fd035e2de8f51431b597 (diff) | |
| download | dove-41926c10ea2e8496ce4b528262f5047ccbe6f155.tar.xz dove-41926c10ea2e8496ce4b528262f5047ccbe6f155.zip | |
Implement authentication system with login/logout functionality and session management
33 files changed, 580 insertions, 13 deletions
diff --git a/config/config.go b/config/config.go index 78eb6a8..ed9dab3 100644 --- a/config/config.go +++ b/config/config.go @@ -14,8 +14,9 @@ var ( ) var ( - DataDir string - DevMode bool + AuthEnabled bool + DataDir string + DevMode bool ) func init() { @@ -34,5 +35,7 @@ func init() { } } + AuthEnabled = isAuthEnabled() + logger.Successf(LOG_PREFIX, messages.ConfigLoaded) } diff --git a/config/functions.go b/config/functions.go index 58ab717..1a49226 100644 --- a/config/functions.go +++ b/config/functions.go @@ -56,3 +56,7 @@ func loadConfigFile(configFilePath string) error { func parseSection(target any) error { return toml.Parse(target) } + +func isAuthEnabled() bool { + return Server.Username != "" && Server.Password != "" +} diff --git a/controllers/auth.go b/controllers/auth.go new file mode 100644 index 0000000..444026a --- /dev/null +++ b/controllers/auth.go @@ -0,0 +1,33 @@ +package controllers + +import ( + "dove/services" + "dove/types" + "dove/utils/meta" + "dove/utils/shortcuts" + + "github.com/gofiber/fiber/v2" +) + +func Login(context *fiber.Ctx) error { + body, parseError := meta.Body[types.LoginRequest](context) + if parseError != nil { + return shortcuts.BadRequest(context, parseError) + } + + _, serviceError := services.Authenticate(context, body) + if serviceError != nil { + return shortcuts.HandleError(context, serviceError) + } + + return shortcuts.Redirect(context, "dashboard.index") +} + +func Logout(context *fiber.Ctx) error { + _, serviceError := services.Deauthenticate(context) + if serviceError != nil { + return shortcuts.HandleError(context, serviceError) + } + + return shortcuts.Redirect(context, "home") +}
\ No newline at end of file diff --git a/enums/error.go b/enums/error.go new file mode 100644 index 0000000..b3dd10a --- /dev/null +++ b/enums/error.go @@ -0,0 +1,12 @@ +package enums + +type ErrorKind string + +const ( + BadRequest ErrorKind = "bad_request" + Forbidden ErrorKind = "forbidden" + Internal ErrorKind = "internal" + NotFound ErrorKind = "not_found" + Unauthorized ErrorKind = "unauthorized" + Unprocessable ErrorKind = "unprocessable" +) @@ -3,6 +3,7 @@ module dove go 1.25.0 require ( + github.com/gofiber/fiber/v2 v2.52.12 github.com/pelletier/go-toml/v2 v2.2.4 go.uber.org/zap v1.27.1 gorm.io/driver/sqlite v1.6.0 @@ -11,7 +12,6 @@ require ( require ( github.com/andybalholm/brotli v1.1.0 // indirect - github.com/gofiber/fiber/v2 v2.52.12 // indirect github.com/google/uuid v1.6.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect diff --git a/messages/auth.go b/messages/auth.go new file mode 100644 index 0000000..0cad598 --- /dev/null +++ b/messages/auth.go @@ -0,0 +1,7 @@ +package messages + +const ( + AuthAuthenticated = "Authenticated successfully." + AuthInvalidCredentials = "Invalid username or password." + AuthLoggedOut = "Logged out successfully." +) diff --git a/messages/config.go b/messages/config.go index fcb42c3..43c9d7f 100644 --- a/messages/config.go +++ b/messages/config.go @@ -1,9 +1,9 @@ package messages const ( - ConfigFileLoadFailed = "failed to load config: %v" - ConfigFileReadFailed = "failed to read config file %s: %s" - ConfigLoaded = "configuration loaded successfully" - ConfigSectionInvalid = "config section '%s' has invalid data" - ConfigSectionParseFailed = "failed to parse config section: %v" + ConfigFileLoadFailed = "Failed to load config: %v" + ConfigFileReadFailed = "Failed to read config file %s: %s." + ConfigLoaded = "Configuration loaded successfully." + ConfigSectionInvalid = "Config section '%s' has invalid data." + ConfigSectionParseFailed = "Failed to parse config section: %v" ) diff --git a/messages/database.go b/messages/database.go index 119bdef..57bc27f 100644 --- a/messages/database.go +++ b/messages/database.go @@ -1,7 +1,7 @@ package messages const ( - DatabaseConnected = "connected to %s" - DatabaseConnectionFailed = "failed to connect to database: %v" - DatabasePoolConfigFailed = "failed to configure connection pool: %v" + DatabaseConnected = "Connected to %s." + DatabaseConnectionFailed = "Failed to connect to database: %v" + DatabasePoolConfigFailed = "Failed to configure connection pool: %v" ) diff --git a/messages/logger.go b/messages/logger.go index 46c12e6..755e259 100644 --- a/messages/logger.go +++ b/messages/logger.go @@ -1,5 +1,5 @@ package messages const ( - LoggerNotInitialized = "logger.Init() was not called" + LoggerNotInitialized = "logger.Init() was not called." ) diff --git a/messages/session.go b/messages/session.go new file mode 100644 index 0000000..b35b85f --- /dev/null +++ b/messages/session.go @@ -0,0 +1,5 @@ +package messages + +const ( + SessionInitialized = "Session store initialized." +) diff --git a/messages/shortcuts.go b/messages/shortcuts.go new file mode 100644 index 0000000..b0c0d91 --- /dev/null +++ b/messages/shortcuts.go @@ -0,0 +1,5 @@ +package messages + +const ( + ShortcutUnsupportedBindType = "Bind data must be a struct, *struct, fiber.Map, or map[string]any." +)
\ No newline at end of file diff --git a/messages/toml.go b/messages/toml.go index ee4cbfd..9ecc069 100644 --- a/messages/toml.go +++ b/messages/toml.go @@ -1,5 +1,5 @@ package messages const ( - ParseTargetMustBeStructPointer = "parse target must be a pointer to a struct" + ParseTargetMustBeStructPointer = "Parse target must be a pointer to a struct." ) diff --git a/middleware/auth.go b/middleware/auth.go new file mode 100644 index 0000000..008aa2c --- /dev/null +++ b/middleware/auth.go @@ -0,0 +1,17 @@ +package middleware + +import ( + "dove/utils/auth" + "dove/utils/shortcuts" + + "github.com/gofiber/fiber/v2" +) + +func authentication(context *fiber.Ctx) error { + switch auth.IsAuthenticated(context) { + case true: + return context.Next() + default: + return shortcuts.Redirect(context, "auth.login") + } +} diff --git a/middleware/middleware.go b/middleware/middleware.go new file mode 100644 index 0000000..dffc109 --- /dev/null +++ b/middleware/middleware.go @@ -0,0 +1,14 @@ +package middleware + +import ( + "dove/config" + + "github.com/gofiber/fiber/v2" +) + +func Initialize(application *fiber.App) { + switch config.AuthEnabled { + case true: + application.Use(authentication) + } +} diff --git a/pages/dashboard.go b/pages/dashboard.go new file mode 100644 index 0000000..323cd1f --- /dev/null +++ b/pages/dashboard.go @@ -0,0 +1,11 @@ +package pages + +import ( + "dove/utils/shortcuts" + + "github.com/gofiber/fiber/v2" +) + +func Dashboard(context *fiber.Ctx) error { + return shortcuts.Render(context, "dashboard", nil) +} diff --git a/pages/home.go b/pages/home.go new file mode 100644 index 0000000..cbf5a5d --- /dev/null +++ b/pages/home.go @@ -0,0 +1,17 @@ +package pages + +import ( + "dove/config" + "dove/utils/shortcuts" + + "github.com/gofiber/fiber/v2" +) + +func Home(context *fiber.Ctx) error { + switch config.AuthEnabled { + case true: + return shortcuts.Render(context, "auth/login", nil) + default: + return shortcuts.Render(context, "dashboard", nil) + } +} diff --git a/router/auth.go b/router/auth.go new file mode 100644 index 0000000..f8f41fc --- /dev/null +++ b/router/auth.go @@ -0,0 +1,14 @@ +package router + +import ( + "dove/controllers" + "dove/enums" + "dove/utils/urls" +) + +func init() { + urls.SetNamespace("auth") + + urls.Path(enums.Post, "/login", controllers.Login, "login") + urls.Path(enums.Get, "/logout", controllers.Logout, "logout") +} diff --git a/router/base.go b/router/base.go new file mode 100644 index 0000000..3a5e7ee --- /dev/null +++ b/router/base.go @@ -0,0 +1,13 @@ +package router + +import ( + "dove/enums" + "dove/pages" + "dove/utils/urls" +) + +func init() { + urls.SetNamespace("") + + urls.Path(enums.Get, "/", pages.Home, "home") +} diff --git a/router/dashboard.go b/router/dashboard.go new file mode 100644 index 0000000..1ab4dda --- /dev/null +++ b/router/dashboard.go @@ -0,0 +1,14 @@ +package router + +import ( + "dove/enums" + "dove/pages" + "dove/utils/auth" + "dove/utils/urls" +) + +func init() { + urls.SetNamespace("dashboard") + + urls.Path(enums.Get, "/", auth.RequireAuthentication(pages.Dashboard), "index") +} diff --git a/router/router.go b/router/router.go new file mode 100644 index 0000000..68ecad9 --- /dev/null +++ b/router/router.go @@ -0,0 +1,33 @@ +package router + +import ( + "dove/utils/shortcuts" + "dove/utils/urls" + + "github.com/gofiber/fiber/v2" +) + +func Initialize(application *fiber.App) { + application.Static("/static", "./static") + urls.Attach(application) +} + +func ErrorHandler(context *fiber.Ctx, err error) error { + statusCode := fiber.StatusInternalServerError + if fiberError, ok := err.(*fiber.Error); ok { + statusCode = fiberError.Code + } + + switch statusCode { + case fiber.StatusBadRequest: + return shortcuts.BadRequest(context, err) + case fiber.StatusForbidden: + return shortcuts.Forbidden(context, err) + case fiber.StatusNotFound: + return shortcuts.NotFound(context, err) + case fiber.StatusUnauthorized: + return shortcuts.Unauthorized(context, err) + default: + return shortcuts.InternalServerError(context, err) + } +}
\ No newline at end of file diff --git a/services/auth.go b/services/auth.go new file mode 100644 index 0000000..ce62d10 --- /dev/null +++ b/services/auth.go @@ -0,0 +1,37 @@ +package services + +import ( + "dove/config" + "dove/enums" + "dove/messages" + "dove/types" + "dove/utils/auth" + "dove/utils/shortcuts" + + "github.com/gofiber/fiber/v2" +) + +func Authenticate(context *fiber.Ctx, request types.LoginRequest) (*types.MessageResponse, *types.ServiceError) { + switch request.Username == config.Server.Username && request.Password == config.Server.Password { + case true: + if sessionError := auth.Authenticate(context); sessionError != nil { + return nil, shortcuts.ServiceError(enums.Internal, sessionError.Error()) + } + + return &types.MessageResponse{ + Message: messages.AuthAuthenticated, + }, nil + default: + return nil, shortcuts.ServiceError(enums.Unauthorized, messages.AuthInvalidCredentials) + } +} + +func Deauthenticate(context *fiber.Ctx) (*types.MessageResponse, *types.ServiceError) { + if sessionError := auth.Deauthenticate(context); sessionError != nil { + return nil, shortcuts.ServiceError(enums.Internal, sessionError.Error()) + } + + return &types.MessageResponse{ + Message: messages.AuthLoggedOut, + }, nil +}
\ No newline at end of file diff --git a/session/constants.go b/session/constants.go new file mode 100644 index 0000000..d6a7f69 --- /dev/null +++ b/session/constants.go @@ -0,0 +1,10 @@ +package session + +import "time" + +const ( + LOG_PREFIX = "Session" + SESSION_COOKIE_NAME = "dove_session" + SESSION_COOKIE_SAME_SITE = "Lax" + SESSION_EXPIRATION = 24 * time.Hour +) diff --git a/session/session.go b/session/session.go new file mode 100644 index 0000000..89b7ef8 --- /dev/null +++ b/session/session.go @@ -0,0 +1,23 @@ +package session + +import ( + "fmt" + + "dove/messages" + "dove/utils/logger" + + "github.com/gofiber/fiber/v2/middleware/session" +) + +var Store *session.Store + +func init() { + Store = session.New(session.Config{ + KeyLookup: fmt.Sprintf("cookie:%s", SESSION_COOKIE_NAME), + CookieHTTPOnly: true, + CookieSameSite: SESSION_COOKIE_SAME_SITE, + Expiration: SESSION_EXPIRATION, + }) + + logger.Successf(LOG_PREFIX, messages.SessionInitialized) +} diff --git a/types/auth.go b/types/auth.go new file mode 100644 index 0000000..1792011 --- /dev/null +++ b/types/auth.go @@ -0,0 +1,6 @@ +package types + +type LoginRequest struct { + Username string `form:"username"` + Password string `form:"password"` +} diff --git a/types/errors.go b/types/errors.go new file mode 100644 index 0000000..1e5a562 --- /dev/null +++ b/types/errors.go @@ -0,0 +1,12 @@ +package types + +import "dove/enums" + +type ServiceError struct { + Kind enums.ErrorKind + Message string +} + +func (self *ServiceError) Error() string { + return self.Message +} diff --git a/types/response.go b/types/response.go new file mode 100644 index 0000000..d38051d --- /dev/null +++ b/types/response.go @@ -0,0 +1,5 @@ +package types + +type MessageResponse struct { + Message string +} diff --git a/utils/auth/auth.go b/utils/auth/auth.go new file mode 100644 index 0000000..5abb496 --- /dev/null +++ b/utils/auth/auth.go @@ -0,0 +1,46 @@ +package auth + +import ( + "dove/session" + + "github.com/gofiber/fiber/v2" +) + +func IsAuthenticated(context *fiber.Ctx) bool { + activeSession, sessionError := session.Store.Get(context) + if sessionError != nil { + return false + } + + return activeSession.Get(SESSION_AUTHENTICATED_KEY) != nil +} + +func RequireAuthentication(handler fiber.Handler) fiber.Handler { + return func(context *fiber.Ctx) error { + switch IsAuthenticated(context) { + case true: + return handler(context) + default: + return fiber.ErrUnauthorized + } + } +} + +func Authenticate(context *fiber.Ctx) error { + activeSession, sessionError := session.Store.Get(context) + if sessionError != nil { + return sessionError + } + + activeSession.Set(SESSION_AUTHENTICATED_KEY, true) + return activeSession.Save() +} + +func Deauthenticate(context *fiber.Ctx) error { + activeSession, sessionError := session.Store.Get(context) + if sessionError != nil { + return sessionError + } + + return activeSession.Destroy() +} diff --git a/utils/auth/constants.go b/utils/auth/constants.go new file mode 100644 index 0000000..99f5a85 --- /dev/null +++ b/utils/auth/constants.go @@ -0,0 +1,5 @@ +package auth + +const ( + SESSION_AUTHENTICATED_KEY = "authenticated" +) diff --git a/utils/meta/body.go b/utils/meta/body.go new file mode 100644 index 0000000..ce10bde --- /dev/null +++ b/utils/meta/body.go @@ -0,0 +1,12 @@ +package meta + +import "github.com/gofiber/fiber/v2" + +func Body[T any](context *fiber.Ctx) (T, error) { + var body T + if parseError := context.BodyParser(&body); parseError != nil { + return body, parseError + } + + return body, nil +} diff --git a/utils/shortcuts/error.go b/utils/shortcuts/error.go new file mode 100644 index 0000000..71fa4bd --- /dev/null +++ b/utils/shortcuts/error.go @@ -0,0 +1,65 @@ +package shortcuts + +import ( + "dove/enums" + "dove/types" + + "github.com/gofiber/fiber/v2" +) + +var statusMap = map[enums.ErrorKind]int{ + enums.BadRequest: fiber.StatusBadRequest, + enums.Forbidden: fiber.StatusForbidden, + enums.Internal: fiber.StatusInternalServerError, + enums.NotFound: fiber.StatusNotFound, + enums.Unauthorized: fiber.StatusUnauthorized, + enums.Unprocessable: fiber.StatusUnprocessableEntity, +} + +func ServiceError(kind enums.ErrorKind, message string) *types.ServiceError { + return &types.ServiceError{ + Kind: kind, + Message: message, + } +} + +func HandleError(context *fiber.Ctx, serviceError *types.ServiceError) error { + statusCode, exists := statusMap[serviceError.Kind] + if !exists { + statusCode = fiber.StatusInternalServerError + } + + return RenderWithStatus(context, "error", fiber.Map{ + "ErrorMessage": serviceError.Message, + }, statusCode) +} + +func BadRequest(context *fiber.Ctx, err error) error { + return RenderWithStatus(context, "error", fiber.Map{ + "ErrorMessage": err.Error(), + }, fiber.StatusBadRequest) +} + +func Forbidden(context *fiber.Ctx, err error) error { + return RenderWithStatus(context, "error", fiber.Map{ + "ErrorMessage": err.Error(), + }, fiber.StatusForbidden) +} + +func InternalServerError(context *fiber.Ctx, err error) error { + return RenderWithStatus(context, "error", fiber.Map{ + "ErrorMessage": err.Error(), + }, fiber.StatusInternalServerError) +} + +func NotFound(context *fiber.Ctx, err error) error { + return RenderWithStatus(context, "error", fiber.Map{ + "ErrorMessage": err.Error(), + }, fiber.StatusNotFound) +} + +func Unauthorized(context *fiber.Ctx, err error) error { + return RenderWithStatus(context, "error", fiber.Map{ + "ErrorMessage": err.Error(), + }, fiber.StatusUnauthorized) +} diff --git a/utils/shortcuts/functions.go b/utils/shortcuts/functions.go new file mode 100644 index 0000000..df00dcf --- /dev/null +++ b/utils/shortcuts/functions.go @@ -0,0 +1,97 @@ +package shortcuts + +import ( + "maps" + "reflect" + "strings" + + "dove/messages" + "dove/utils/errors" + + "github.com/gofiber/fiber/v2" +) + +func mergeContextValues(context *fiber.Ctx, targetMap fiber.Map) { + context.Context().VisitUserValues(func(key []byte, value any) { + targetMap[string(key)] = value + }) +} + +func mergeBindData(targetMap fiber.Map, data any) error { + normalizedData, normalizeError := normalizeToMap(data) + if normalizeError != nil { + return normalizeError + } + + maps.Copy(targetMap, normalizedData) + return nil +} + +func normalizeToMap(data any) (fiber.Map, error) { + switch typedData := data.(type) { + case fiber.Map: + return typedData, nil + case map[string]any: + return fiber.Map(typedData), nil + default: + return convertStructToMap(data) + } +} + +func convertStructToMap(data any) (fiber.Map, error) { + structValue := reflect.ValueOf(data) + + switch structValue.Kind() { + case reflect.Pointer: + structValue = structValue.Elem() + } + + switch structValue.Kind() { + case reflect.Struct: + return extractStructFields(structValue), nil + default: + return nil, errors.Error(messages.ShortcutUnsupportedBindType) + } +} + +func extractStructFields(structValue reflect.Value) fiber.Map { + structType := structValue.Type() + fieldMap := make(fiber.Map, structValue.NumField()) + + for fieldIndex := range structType.NumField() { + fieldDescriptor := structType.Field(fieldIndex) + + if !fieldDescriptor.IsExported() { + continue + } + + fieldKey := resolveFieldKey(fieldDescriptor) + fieldMap[fieldKey] = structValue.Field(fieldIndex).Interface() + } + + return fieldMap +} + +func resolveFieldKey(fieldDescriptor reflect.StructField) string { + jsonTag := fieldDescriptor.Tag.Get("json") + + switch { + case jsonTag == "" || jsonTag == "-": + return fieldDescriptor.Name + default: + return extractTagName(jsonTag, fieldDescriptor.Name) + } +} + +func extractTagName(jsonTag string, fallbackName string) string { + separatorIndex := strings.IndexByte(jsonTag, ',') + + switch { + case separatorIndex < 0: + return jsonTag + case separatorIndex > 0: + return jsonTag[:separatorIndex] + default: + return fallbackName + } +}
\ No newline at end of file diff --git a/utils/shortcuts/redirect.go b/utils/shortcuts/redirect.go new file mode 100644 index 0000000..627538d --- /dev/null +++ b/utils/shortcuts/redirect.go @@ -0,0 +1,25 @@ +package shortcuts + +import ( + "dove/utils/urls" + + "github.com/gofiber/fiber/v2" +) + +func Redirect(context *fiber.Ctx, routeName string) error { + fullPath, exists := urls.GetFullPath(routeName) + if !exists { + return fiber.ErrNotFound + } + + return context.Redirect(fullPath) +} + +func RedirectWithStatus(context *fiber.Ctx, routeName string, statusCode int) error { + fullPath, exists := urls.GetFullPath(routeName) + if !exists { + return fiber.ErrNotFound + } + + return context.Redirect(fullPath, statusCode) +}
\ No newline at end of file diff --git a/utils/shortcuts/render.go b/utils/shortcuts/render.go new file mode 100644 index 0000000..29c7a8f --- /dev/null +++ b/utils/shortcuts/render.go @@ -0,0 +1,22 @@ +package shortcuts + +import "github.com/gofiber/fiber/v2" + +func Render(context *fiber.Ctx, templateName string, data any) error { + templateData := make(fiber.Map) + + mergeContextValues(context, templateData) + + if data != nil { + if mergeError := mergeBindData(templateData, data); mergeError != nil { + return mergeError + } + } + + return context.Render(templateName, templateData) +} + +func RenderWithStatus(context *fiber.Ctx, templateName string, data any, statusCode int) error { + context.Status(statusCode) + return Render(context, templateName, data) +} |
