starting accounts #2
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
|
.env
|
||||||
|
|
||||||
# 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