Add account management features: implement GetAccount endpoint, update Login method to return token, enhance repository and service layers, and add JWT middleware for authentication. Update .gitignore to include tmp directory.
This commit is contained in:
parent
c2a78d820e
commit
a5e059a636
19 changed files with 260 additions and 6 deletions
51
.air.toml
Normal file
51
.air.toml
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
root = "."
|
||||||
|
testdata_dir = "testdata"
|
||||||
|
tmp_dir = "tmp"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
args_bin = []
|
||||||
|
bin = "./main"
|
||||||
|
cmd = "go build -o ./main ./cmd/api/main.go"
|
||||||
|
delay = 1000
|
||||||
|
exclude_dir = ["tmp", "vendor", "cmd/scripts"]
|
||||||
|
exclude_file = []
|
||||||
|
exclude_regex = ["_test.go"]
|
||||||
|
exclude_unchanged = false
|
||||||
|
follow_symlink = false
|
||||||
|
full_bin = ""
|
||||||
|
include_dir = []
|
||||||
|
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||||
|
include_file = []
|
||||||
|
kill_delay = "0s"
|
||||||
|
log = "build-errors.log"
|
||||||
|
poll = false
|
||||||
|
poll_interval = 0
|
||||||
|
post_cmd = []
|
||||||
|
pre_cmd = []
|
||||||
|
rerun = false
|
||||||
|
rerun_delay = 500
|
||||||
|
send_interrupt = false
|
||||||
|
stop_on_error = false
|
||||||
|
|
||||||
|
[color]
|
||||||
|
app = ""
|
||||||
|
build = "yellow"
|
||||||
|
main = "magenta"
|
||||||
|
runner = "green"
|
||||||
|
watcher = "cyan"
|
||||||
|
|
||||||
|
[log]
|
||||||
|
main_only = false
|
||||||
|
time = false
|
||||||
|
|
||||||
|
[misc]
|
||||||
|
clean_on_exit = false
|
||||||
|
|
||||||
|
[proxy]
|
||||||
|
app_port = 0
|
||||||
|
enabled = false
|
||||||
|
proxy_port = 0
|
||||||
|
|
||||||
|
[screen]
|
||||||
|
clear_on_rebuild = true
|
||||||
|
keep_scroll = true
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -26,3 +26,5 @@ go.work.sum
|
||||||
|
|
||||||
# dist directory
|
# dist directory
|
||||||
dist/
|
dist/
|
||||||
|
|
||||||
|
tmp/
|
3
bruno/accounts/folder.bru
Normal file
3
bruno/accounts/folder.bru
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
meta {
|
||||||
|
name: accounts
|
||||||
|
}
|
18
bruno/accounts/login.bru
Normal file
18
bruno/accounts/login.bru
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
meta {
|
||||||
|
name: login
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{base_url}}/accounts/login
|
||||||
|
body: json
|
||||||
|
auth: inherit
|
||||||
|
}
|
||||||
|
|
||||||
|
body:json {
|
||||||
|
{
|
||||||
|
"username": "lol",
|
||||||
|
"password": "lol"
|
||||||
|
}
|
||||||
|
}
|
9
bruno/bruno.json
Normal file
9
bruno/bruno.json
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"version": "1",
|
||||||
|
"name": "trepa",
|
||||||
|
"type": "collection",
|
||||||
|
"ignore": [
|
||||||
|
"node_modules",
|
||||||
|
".git"
|
||||||
|
]
|
||||||
|
}
|
3
bruno/environments/dev.bru
Normal file
3
bruno/environments/dev.bru
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
vars {
|
||||||
|
base_url: 127.0.0.1:3000
|
||||||
|
}
|
3
bruno/environments/test.bru
Normal file
3
bruno/environments/test.bru
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
vars {
|
||||||
|
base_url: {{process.env.BASE_URL}}
|
||||||
|
}
|
|
@ -16,9 +16,37 @@ type Controller struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewController(service domain.AccountService) Controller {
|
func NewController(service domain.AccountService) Controller {
|
||||||
|
validate = validator.New()
|
||||||
|
|
||||||
return Controller{service: service}
|
return Controller{service: service}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c Controller) GetAccount(w *core.Response, r *http.Request) {
|
||||||
|
claims := r.Context().Value("claims").(core.JWTClaims)
|
||||||
|
|
||||||
|
account, httpErr := c.service.GetAccount(claims.AccountId)
|
||||||
|
if httpErr != nil {
|
||||||
|
w.WriteError(httpErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := GetAccountResponse{
|
||||||
|
Id: account.Id,
|
||||||
|
Username: account.Username,
|
||||||
|
RoleId: account.RoleId,
|
||||||
|
CreatedAt: account.CreatedAt,
|
||||||
|
UpdatedAt: account.UpdatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
responseJson, err := json.Marshal(response)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteError(core.NewInternalServerError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Json(responseJson)
|
||||||
|
}
|
||||||
|
|
||||||
func (c Controller) Login(w *core.Response, r *http.Request) {
|
func (c Controller) Login(w *core.Response, r *http.Request) {
|
||||||
var request LoginAccountRequest
|
var request LoginAccountRequest
|
||||||
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
||||||
|
@ -31,4 +59,32 @@ func (c Controller) Login(w *core.Response, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result, httpErr := c.service.Login(domain.AccountLogin{
|
||||||
|
Username: request.Username,
|
||||||
|
Password: request.Password,
|
||||||
|
})
|
||||||
|
|
||||||
|
if httpErr != nil {
|
||||||
|
w.WriteError(httpErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := LoginAccountResponse{
|
||||||
|
Account: Account{
|
||||||
|
Id: result.Account.Id,
|
||||||
|
Username: result.Account.Username,
|
||||||
|
RoleId: result.Account.RoleId,
|
||||||
|
CreatedAt: result.Account.CreatedAt,
|
||||||
|
UpdatedAt: result.Account.UpdatedAt,
|
||||||
|
},
|
||||||
|
Token: result.Token,
|
||||||
|
}
|
||||||
|
|
||||||
|
responseJson, err := json.Marshal(response)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteError(core.NewInternalServerError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Json(responseJson)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,3 +4,24 @@ type LoginAccountRequest struct {
|
||||||
Username string `json:"username" validate:"required"`
|
Username string `json:"username" validate:"required"`
|
||||||
Password string `json:"password" validate:"required"`
|
Password string `json:"password" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LoginAccountResponse struct {
|
||||||
|
Account Account `json:"account"`
|
||||||
|
Token string `json:"token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Account struct {
|
||||||
|
Id int `json:"id"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
RoleId int `json:"role_id"`
|
||||||
|
CreatedAt string `json:"created_at"`
|
||||||
|
UpdatedAt string `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetAccountResponse struct {
|
||||||
|
Id int `json:"id"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
RoleId int `json:"role_id"`
|
||||||
|
CreatedAt string `json:"created_at"`
|
||||||
|
UpdatedAt string `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
|
@ -13,4 +13,5 @@ func BindRoutes(srv *core.ServerMux, db *sqlx.DB) {
|
||||||
controller := NewController(service)
|
controller := NewController(service)
|
||||||
|
|
||||||
srv.HandleFunc("POST /accounts/login", controller.Login)
|
srv.HandleFunc("POST /accounts/login", controller.Login)
|
||||||
|
srv.HandleFunc("GET /accounts/me", core.JwtMiddleware(controller.GetAccount))
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,11 @@ type AccountLogin struct {
|
||||||
Password string
|
Password string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AccountWithToken struct {
|
||||||
|
Account *Account
|
||||||
|
Token string
|
||||||
|
}
|
||||||
|
|
||||||
type AccountCreate struct {
|
type AccountCreate struct {
|
||||||
Username string
|
Username string
|
||||||
Password string
|
Password string
|
||||||
|
|
|
@ -3,4 +3,5 @@ package domain
|
||||||
type AccountRepository interface {
|
type AccountRepository interface {
|
||||||
Insert(account Account) (int, error)
|
Insert(account Account) (int, error)
|
||||||
FetchOneByUsername(username string) (*Account, error)
|
FetchOneByUsername(username string) (*Account, error)
|
||||||
|
FetchOneById(id int) (*Account, error)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,5 +3,6 @@ package domain
|
||||||
import "gitea.qpismont.fr/qpismont/trepa/internal/core"
|
import "gitea.qpismont.fr/qpismont/trepa/internal/core"
|
||||||
|
|
||||||
type AccountService interface {
|
type AccountService interface {
|
||||||
Login(login AccountLogin) (*Account, *core.HTTPError)
|
Login(login AccountLogin) (*AccountWithToken, *core.HTTPError)
|
||||||
|
GetAccount(id int) (*Account, *core.HTTPError)
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,21 @@ func (r *Repository) Insert(account domain.Account) (int, error) {
|
||||||
return id, nil
|
return id, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Repository) FetchOneById(id int) (*domain.Account, error) {
|
||||||
|
var account domain.Account
|
||||||
|
|
||||||
|
err := r.db.Get(&account, SqlFetchOneById, id)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil
|
||||||
|
} else {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &account, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Repository) FetchOneByUsername(username string) (*domain.Account, error) {
|
func (r *Repository) FetchOneByUsername(username string) (*domain.Account, error) {
|
||||||
var account domain.Account
|
var account domain.Account
|
||||||
|
|
||||||
|
|
|
@ -47,3 +47,23 @@ func TestRepository_FetchOneByUsername(t *testing.T) {
|
||||||
assert.Equal(t, account.Password, "LOLPASSWORD")
|
assert.Equal(t, account.Password, "LOLPASSWORD")
|
||||||
assert.Equal(t, account.RoleId, 1)
|
assert.Equal(t, account.RoleId, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRepository_FetchOneById(t *testing.T) {
|
||||||
|
db := test.SetupTestDB(t, "../../..")
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
repo := NewRepository(db)
|
||||||
|
|
||||||
|
account, err := repo.FetchOneById(1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to fetch account: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if account == nil {
|
||||||
|
t.Fatalf("Account not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, account.Username, "admin")
|
||||||
|
assert.Equal(t, account.Password, "LOLPASSWORD")
|
||||||
|
assert.Equal(t, account.RoleId, 1)
|
||||||
|
}
|
||||||
|
|
|
@ -3,4 +3,5 @@ package repository
|
||||||
const (
|
const (
|
||||||
SqlInsert = "INSERT INTO accounts (username, password, role_id) VALUES ($1, $2, $3) RETURNING id"
|
SqlInsert = "INSERT INTO accounts (username, password, role_id) VALUES ($1, $2, $3) RETURNING id"
|
||||||
SqlFetchOneByUsername = "SELECT * FROM accounts WHERE username = $1"
|
SqlFetchOneByUsername = "SELECT * FROM accounts WHERE username = $1"
|
||||||
|
SqlFetchOneById = "SELECT * FROM accounts WHERE id = $1"
|
||||||
)
|
)
|
||||||
|
|
|
@ -13,10 +13,23 @@ func NewService(repository domain.AccountRepository) domain.AccountService {
|
||||||
return &Service{repository: repository}
|
return &Service{repository: repository}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) Login(login domain.AccountLogin) (*domain.Account, *core.HTTPError) {
|
func (s *Service) GetAccount(id int) (*domain.Account, *core.HTTPError) {
|
||||||
|
account, err := s.repository.FetchOneById(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, core.NewInternalServerError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if account == nil {
|
||||||
|
return nil, domain.ErrAccountNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return account, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Login(login domain.AccountLogin) (*domain.AccountWithToken, *core.HTTPError) {
|
||||||
account, err := s.repository.FetchOneByUsername(login.Username)
|
account, err := s.repository.FetchOneByUsername(login.Username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, domain.ErrAccountNotFound
|
return nil, core.NewInternalServerError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ok, err := core.ComparePassword(login.Password, account.Password)
|
ok, err := core.ComparePassword(login.Password, account.Password)
|
||||||
|
@ -28,5 +41,18 @@ func (s *Service) Login(login domain.AccountLogin) (*domain.Account, *core.HTTPE
|
||||||
return nil, domain.ErrBadPassword
|
return nil, domain.ErrBadPassword
|
||||||
}
|
}
|
||||||
|
|
||||||
return account, nil
|
claims := core.JWTClaims{
|
||||||
|
AccountId: account.Id,
|
||||||
|
RoleId: account.RoleId,
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := core.SignJWT(claims)
|
||||||
|
if err != nil {
|
||||||
|
return nil, core.NewInternalServerError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &domain.AccountWithToken{
|
||||||
|
Account: account,
|
||||||
|
Token: token,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ func SignJWT(claims JWTClaims) (string, error) {
|
||||||
return token.SignedString([]byte(jwtSecret))
|
return token.SignedString([]byte(jwtSecret))
|
||||||
}
|
}
|
||||||
|
|
||||||
func VerifyJWT(token string) (JWTClaims, error) {
|
func VerifyJWT(token string) (JWTClaims, *HTTPError) {
|
||||||
parsedClaims := JWTClaims{}
|
parsedClaims := JWTClaims{}
|
||||||
claims, err := jwt.ParseWithClaims(token, &parsedClaims, func(token *jwt.Token) (any, error) {
|
claims, err := jwt.ParseWithClaims(token, &parsedClaims, func(token *jwt.Token) (any, error) {
|
||||||
return []byte(jwtSecret), nil
|
return []byte(jwtSecret), nil
|
||||||
|
|
|
@ -44,3 +44,21 @@ func ErrorMiddleware(next func(w *Response, r *http.Request)) func(w *Response,
|
||||||
next(w, r)
|
next(w, r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func JwtMiddleware(next func(w *Response, r *http.Request)) func(w *Response, r *http.Request) {
|
||||||
|
return func(w *Response, r *http.Request) {
|
||||||
|
token := r.Header.Get("Authorization")
|
||||||
|
if token == "" {
|
||||||
|
w.WriteError(ErrInvalidToken)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := VerifyJWT(token)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
next(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue