Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ require (
github.com/gofiber/schema v1.5.0 // indirect
github.com/gofiber/swagger v1.1.1 // indirect
github.com/gofiber/utils/v2 v2.0.0-beta.9 // indirect
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
Expand Down Expand Up @@ -59,11 +60,11 @@ require (
go.opentelemetry.io/otel/trace v1.37.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/crypto v0.39.0 // indirect
golang.org/x/crypto v0.40.0 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.26.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/text v0.27.0 // indirect
golang.org/x/tools v0.34.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
Expand Down
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ github.com/gofiber/swagger v1.1.1 h1:FZVhVQQ9s1ZKLHL/O0loLh49bYB5l1HEAgxDlcTtkRA
github.com/gofiber/swagger v1.1.1/go.mod h1:vtvY/sQAMc/lGTUCg0lqmBL7Ht9O7uzChpbvJeJQINw=
github.com/gofiber/utils/v2 v2.0.0-beta.9 h1:IMb2TpF2bb1spuB63GuiOZJXFfq9VJe98ofFJoy0EAY=
github.com/gofiber/utils/v2 v2.0.0-beta.9/go.mod h1:XjKLrtxE77EyWzzWGWAepv3NLclRSZkAG+Y+GfPcKeQ=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
Expand Down Expand Up @@ -122,15 +124,23 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
Expand Down
12 changes: 12 additions & 0 deletions internal/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,37 @@ import (
func App(db *gorm.DB) *fiber.App {
fmt.Println("Initializing App...")

// Task
var tr repository.ITaskRepository = repository.New(db)
var ts service.ITaskService = service.New(tr)
var th handler.TaskHandler = *handler.New(ts)

// User & Auth
var ur repository.IUserRepository = repository.NewUserRepo(db)
var us service.IUserService = service.NewUserService(ur)
var ah handler.AuthHandler = *handler.NewAuthHandler(us)

app := fiber.New()
app.Get("/health", func(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusOK)
})

// Monitoring
prometheus := fiberprometheus.New("devtasker")
prometheus.RegisterAt(app, "/metrics")
app.Use(prometheus.Middleware)

// Middleware
app.Use(middleware.Logger)
app.Use(middleware.Authorization)

// API Doc
app.Get("/doc/*", swagger.HandlerDefault)

// API
api := app.Group("/api")
handler.TaskRouter(api, th)
handler.AuthRouter(api, ah)

fmt.Println("App initiated successfully!")

Expand Down
14 changes: 14 additions & 0 deletions internal/dto/task.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package dto

import "devtasker/internal/model"

type CreateTaskRequest struct {
Title string `json:"title"`
Description string `json:"description"`
}

type UpdateTaskRequest struct {
Title string `json:"title"`
Description string `json:"description"`
Status model.TaskStatus `json:"status"`
}
12 changes: 12 additions & 0 deletions internal/dto/user.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package dto

type RegisterUserRequest struct {
Name string `json:"name"`
Username string `json:"username"`
Passwrod string `json:"password"`
}

type LoginUserRequest struct {
Username string `json:"username"`
Passwrod string `json:"password"`
}
57 changes: 57 additions & 0 deletions internal/handler/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package handler

import (
"devtasker/internal/dto"
"devtasker/internal/service"
"devtasker/internal/utils"

"github.com/gofiber/fiber/v2"
)

// ===== Router =====
func AuthRouter(api fiber.Router, ah AuthHandler) {
api.Route("/auth", func(authRouter fiber.Router) {
authRouter.Post("/register", ah.Register)
authRouter.Post("/login", ah.Login)
})
}

// ===== Handler =====
type AuthHandler struct {
s service.IUserService
}

func NewAuthHandler(s service.IUserService) *AuthHandler {
return &AuthHandler{
s: s,
}
}

func (ah *AuthHandler) Register(c *fiber.Ctx) error {
rur := new(dto.RegisterUserRequest)
if err := c.BodyParser(rur); err != nil {
utils.ErrorLogger.Println("Failed to parse the body:\n", c.Body())
return c.Status(fiber.StatusInternalServerError).JSON(err)
}
u, err := ah.s.Register(rur.Name, rur.Username, rur.Passwrod)
if err != nil {
utils.ErrorLogger.Println("Failed to register the user:\n", err)
return c.Status(fiber.StatusInternalServerError).JSON(err)
}
return c.JSON(u)
}

func (ah *AuthHandler) Login(c *fiber.Ctx) error {
lur := new(dto.LoginUserRequest)
if err := c.BodyParser(lur); err != nil {
utils.ErrorLogger.Println("Failed to parse the body:\n", c.Body())
return c.Status(fiber.StatusInternalServerError).JSON(err)

}
token, err := ah.s.Login(lur.Username, lur.Passwrod)
if err != nil {
utils.ErrorLogger.Println("Failed to login the user:\n", err)
return c.Status(fiber.StatusInternalServerError).JSON(err)
}
return c.JSON(token)
}
6 changes: 3 additions & 3 deletions internal/handler/task.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package handler

import (
"devtasker/internal/model"
"devtasker/internal/dto"
"devtasker/internal/service"
"devtasker/internal/utils"

Expand Down Expand Up @@ -41,7 +41,7 @@ func New(s service.ITaskService) *TaskHandler {
// @Failure 500 {object} error
// @Router /api/task [post]
func (th *TaskHandler) CreateTask(c *fiber.Ctx) error {
ctr := new(model.CreateTaskRequest)
ctr := new(dto.CreateTaskRequest)
if err := c.BodyParser(ctr); err != nil {
utils.ErrorLogger.Println("Failed to parse the body:\n", c.Body())
return err
Expand Down Expand Up @@ -103,7 +103,7 @@ func (th *TaskHandler) GetTaskByID(c *fiber.Ctx) error {
// @Router /api/task/{id} [patch]
func (th *TaskHandler) UpdateTask(c *fiber.Ctx) error {
id := c.Params("id")
b := new(model.UpdateTaskRequest)
b := new(dto.UpdateTaskRequest)
if err := c.BodyParser(b); err != nil {
utils.ErrorLogger.Println("Failed to parse the body:\n", c.Body())
return err
Expand Down
44 changes: 44 additions & 0 deletions internal/middleware/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package middleware

import (
"devtasker/internal/utils"
"strings"
"time"

"github.com/gofiber/fiber/v2"
)

func Authorization(c *fiber.Ctx) error {
if strings.Contains(c.Path(), "auth") {
return c.Next()
}

bearerToken := c.Get("Authorization")
if !strings.HasPrefix(bearerToken, "Bearer ") {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "Missing or malformed token",
})
}

token := strings.Split(bearerToken, " ")[1]
claims, ok := utils.ExtractClaims(token)
if !ok {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "Token invalid",
})
}

if expRaw, ok := claims["exp"].(float64); ok {
expTime := time.Unix(int64(expRaw), 0)
if time.Now().After(expTime) {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "Token expired",
})
}
}

c.Locals("username", claims["username"])
c.Locals("name", claims["name"])

return c.Next()
}
12 changes: 0 additions & 12 deletions internal/model/task.dto.go

This file was deleted.

7 changes: 5 additions & 2 deletions internal/model/task.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package model

import "time"

type TaskStatus string

const (
Expand All @@ -11,9 +13,10 @@ const (
)

type Task struct {
ID string `json:"id"`
ID string `json:"id" gorm:"type:uuid;default:gen_random_uuid();primaryKey"`
Title string `json:"title"`
Description string `json:"description"`
Status TaskStatus `json:"status"`
CreatedAt string `json:"created_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
12 changes: 12 additions & 0 deletions internal/model/user.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package model

import "time"

type User struct {
ID string `json:"id" gorm:"type:uuid;default:gen_random_uuid();primaryKey"`
Name string `json:"name"`
Username string `json:"username" gorm:"unique"`
PasswordHash string `json:"password"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
7 changes: 1 addition & 6 deletions internal/repository/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ package repository

import (
"devtasker/internal/model"
"time"

"github.com/google/uuid"
"gorm.io/gorm"
)

Expand All @@ -27,15 +25,12 @@ func New(db *gorm.DB) *TaskRepository {
}

func (tr *TaskRepository) CreateTask(title, description string) (model.Task, error) {
id := uuid.NewString()
t := model.Task{
ID: id,
Title: title,
Description: description,
Status: model.Pending,
CreatedAt: time.Now().String(),
}
tr.db.Save(t)
tr.db.Create(&t)
return t, nil
}

Expand Down
41 changes: 41 additions & 0 deletions internal/repository/user.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package repository

import (
"devtasker/internal/model"

"gorm.io/gorm"
)

type IUserRepository interface {
CreateUser(name, username, hashedPass string) (model.User, error)
GetUserByUsername(username string) (model.User, error)
}

type UserRepository struct {
db *gorm.DB
}

func NewUserRepo(db *gorm.DB) *UserRepository {
return &UserRepository{
db: db,
}
}

func (ur *UserRepository) CreateUser(name, username, hashedPass string) (model.User, error) {
u := model.User{
Name: name,
Username: username,
PasswordHash: hashedPass,
}
ur.db.Create(&u)
return u, nil
}

func (ur *UserRepository) GetUserByUsername(username string) (model.User, error) {
var user model.User
result := ur.db.First(&user, "username = ?", username)
if result.Error != nil {
return model.User{}, result.Error
}
return user, nil
}
Loading