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
|
||||
|
||||
# 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