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
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -25,4 +25,6 @@ go.work.sum
|
|||
.env
|
||||
|
||||
# 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 {
|
||||
validate = validator.New()
|
||||
|
||||
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) {
|
||||
var request LoginAccountRequest
|
||||
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
|
||||
}
|
||||
|
||||
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"`
|
||||
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)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
type AccountWithToken struct {
|
||||
Account *Account
|
||||
Token string
|
||||
}
|
||||
|
||||
type AccountCreate struct {
|
||||
Username string
|
||||
Password string
|
||||
|
|
|
@ -3,4 +3,5 @@ package domain
|
|||
type AccountRepository interface {
|
||||
Insert(account Account) (int, 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"
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
var account domain.Account
|
||||
|
||||
|
|
|
@ -47,3 +47,23 @@ func TestRepository_FetchOneByUsername(t *testing.T) {
|
|||
assert.Equal(t, account.Password, "LOLPASSWORD")
|
||||
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 (
|
||||
SqlInsert = "INSERT INTO accounts (username, password, role_id) VALUES ($1, $2, $3) RETURNING id"
|
||||
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}
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, domain.ErrAccountNotFound
|
||||
return nil, core.NewInternalServerError(err)
|
||||
}
|
||||
|
||||
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 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))
|
||||
}
|
||||
|
||||
func VerifyJWT(token string) (JWTClaims, error) {
|
||||
func VerifyJWT(token string) (JWTClaims, *HTTPError) {
|
||||
parsedClaims := JWTClaims{}
|
||||
claims, err := jwt.ParseWithClaims(token, &parsedClaims, func(token *jwt.Token) (any, error) {
|
||||
return []byte(jwtSecret), nil
|
||||
|
|
|
@ -44,3 +44,21 @@ func ErrorMiddleware(next func(w *Response, r *http.Request)) func(w *Response,
|
|||
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