From 18661f82f86cacafbdfe9ec43a120b39bbaf8082 Mon Sep 17 00:00:00 2001 From: rmanach Date: Fri, 3 Jan 2025 14:17:37 +0100 Subject: [PATCH] rename auth service --- handlers/home/handler.go | 5 +- handlers/login/handler.go | 86 ++++++++++----------- handlers/upload/handler.go | 15 ++-- main.go | 8 +- services/{sessions.go => authentication.go} | 60 ++++++++------ 5 files changed, 90 insertions(+), 84 deletions(-) rename services/{sessions.go => authentication.go} (64%) diff --git a/handlers/home/handler.go b/handlers/home/handler.go index 5698ad9..4a2d782 100644 --- a/handlers/home/handler.go +++ b/handlers/home/handler.go @@ -30,8 +30,5 @@ func getHome(w http.ResponseWriter, _ *http.Request) { return } - if _, err := fmt.Fprint(w, buf); err != nil { - log.Err(err).Msg("unable to write to response") - http.Error(w, "unexpected error occurred", http.StatusInternalServerError) - } + fmt.Fprint(w, buf) } diff --git a/handlers/login/handler.go b/handlers/login/handler.go index e9a4a07..d7e964f 100644 --- a/handlers/login/handler.go +++ b/handlers/login/handler.go @@ -5,8 +5,6 @@ import ( "errors" "fmt" "net/http" - "os" - "sync" "github.com/rs/zerolog/log" @@ -14,16 +12,6 @@ import ( "librapi/templates" ) -var ( - adminUsername = sync.OnceValue[string](func() string { - return os.Getenv("API_ADMIN_USERNAME") - }) - - adminPassword = sync.OnceValue[string](func() string { - return os.Getenv("API_ADMIN_PASSWORD") - }) -) - var ( ErrInvalidUsername = errors.New("username must not be empty") ErrInvalidPassword = errors.New("password must not be empty") @@ -63,17 +51,13 @@ func (lf *LoginForm) IsSuccess() bool { return lf.Method == http.MethodPost && lf.Error != nil && !lf.HasErrors() } -func (lf *LoginForm) ValidCredentials() bool { - return lf.Username.Value == adminUsername() && lf.Password.Value == adminPassword() -} - -func Handler(s *services.SessionStore) func(http.ResponseWriter, *http.Request) { +func Handler(a services.IAuthenticate) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: - getLogin(w, r, s) + getLogin(w, r, a) case http.MethodPost: - postLogin(w, r, s) + postLogin(w, r, a) default: http.Error(w, "method not allowed", http.StatusMethodNotAllowed) } @@ -98,8 +82,20 @@ func extractLoginForm(r *http.Request) LoginForm { return lf } -func postLogin(w http.ResponseWriter, r *http.Request, s *services.SessionStore) { +func postLogin(w http.ResponseWriter, r *http.Request, a services.IAuthenticate) { loginForm := templates.GetLoginForm() + loginSuccess := templates.GetLoginSuccess() + + if a.IsLogged(r) { + buf := bytes.NewBufferString("") + if err := loginSuccess.Execute(buf, nil); err != nil { + log.Err(err).Msg("unable to generate template") + http.Error(w, "unexpected error occurred", http.StatusInternalServerError) + return + } + fmt.Fprint(w, buf) + return + } lf := extractLoginForm(r) if lf.HasErrors() { @@ -114,25 +110,25 @@ func postLogin(w http.ResponseWriter, r *http.Request, s *services.SessionStore) return } - if ok := lf.ValidCredentials(); !ok { - log.Warn().Str("username", lf.Username.Value).Msg("bad credentials") - lf.Error = ErrInvalidCredentials + session, err := a.Authenticate(lf.Username.Value, lf.Password.Value) + if err != nil { + if errors.Is(err, services.ErrUnauthorized) { + log.Warn().Str("username", lf.Username.Value).Msg("bad credentials") - buf := bytes.NewBufferString("") - if err := loginForm.Execute(buf, &lf); err != nil { - log.Err(err).Msg("unable to generate template") - http.Error(w, "unexpected error occurred", http.StatusInternalServerError) + lf.Error = ErrInvalidCredentials + + buf := bytes.NewBufferString("") + if err := loginForm.Execute(buf, &lf); err != nil { + log.Err(err).Msg("unable to generate template") + http.Error(w, "unexpected error occurred", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusUnauthorized) + fmt.Fprint(w, buf.String()) return } - w.WriteHeader(http.StatusUnauthorized) - fmt.Fprint(w, buf.String()) - return - } - - session, err := s.NewSession() - if err != nil { - log.Err(err).Msg("unable to create a new session") http.Error(w, "unexpected error occurred", http.StatusInternalServerError) return } @@ -140,7 +136,6 @@ func postLogin(w http.ResponseWriter, r *http.Request, s *services.SessionStore) cookie := session.GenerateCookie() http.SetCookie(w, cookie) - loginSuccess := templates.GetLoginSuccess() buf := bytes.NewBufferString("") if err := loginSuccess.Execute(buf, nil); err != nil { log.Err(err).Msg("unable to generate template") @@ -151,12 +146,20 @@ func postLogin(w http.ResponseWriter, r *http.Request, s *services.SessionStore) fmt.Fprint(w, buf) } -func getLogin(w http.ResponseWriter, r *http.Request, s *services.SessionStore) { +func getLogin(w http.ResponseWriter, r *http.Request, a services.IAuthenticate) { loginForm := templates.GetLoginForm() - if s.IsLogged(r) { + if a.IsLogged(r) { loginSuccess := templates.GetLoginSuccess() - fmt.Fprint(w, loginSuccess.Tree.Root.String()) + + buf := bytes.NewBufferString("") + if err := loginSuccess.Execute(buf, nil); err != nil { + log.Err(err).Msg("unable to generate template") + http.Error(w, "unexpected error occurred", http.StatusInternalServerError) + return + } + + fmt.Fprint(w, buf) return } @@ -167,8 +170,5 @@ func getLogin(w http.ResponseWriter, r *http.Request, s *services.SessionStore) return } - if _, err := fmt.Fprint(w, buf); err != nil { - log.Err(err).Msg("unable to write to response") - http.Error(w, "unexpected error occurred", http.StatusInternalServerError) - } + fmt.Fprint(w, buf) } diff --git a/handlers/upload/handler.go b/handlers/upload/handler.go index bcea623..f76b787 100644 --- a/handlers/upload/handler.go +++ b/handlers/upload/handler.go @@ -100,13 +100,13 @@ func (bf *BookForm) IsSuccess() bool { return bf.Method == http.MethodPost && bf.Error == "" && !bf.HasErrors() } -func Handler(s *services.SessionStore) func(http.ResponseWriter, *http.Request) { +func Handler(a services.IAuthenticate) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: getUploadFile(w, r) case http.MethodPost: - postUploadFile(w, r, s) + postUploadFile(w, r, a) default: http.Error(w, "method not allowed", http.StatusMethodNotAllowed) } @@ -164,10 +164,10 @@ func extractBookForm(r *http.Request) BookForm { return bf } -func postUploadFile(w http.ResponseWriter, r *http.Request, s *services.SessionStore) { +func postUploadFile(w http.ResponseWriter, r *http.Request, a services.IAuthenticate) { uploadForm := templates.GetUploadForm() - if !s.IsLogged(r) { + if !a.IsLogged(r) { buf := bytes.NewBufferString("") if err := uploadForm.Execute(buf, &BookForm{Error: services.ErrUnauthorized.Error()}); err != nil { log.Err(err).Msg("unable to generate template") @@ -179,8 +179,8 @@ func postUploadFile(w http.ResponseWriter, r *http.Request, s *services.SessionS return } - buf := bytes.NewBufferString("") bf := extractBookForm(r) + buf := bytes.NewBufferString("") if err := uploadForm.Execute(buf, &bf); err != nil { log.Err(err).Msg("unable to generate template") http.Error(w, "unexpected error occurred", http.StatusInternalServerError) @@ -241,8 +241,5 @@ func getUploadFile(w http.ResponseWriter, _ *http.Request) { return } - if _, err := fmt.Fprint(w, buf); err != nil { - log.Err(err).Msg("unable to write to response") - http.Error(w, "unexpected error occurred", http.StatusInternalServerError) - } + fmt.Fprint(w, buf) } diff --git a/main.go b/main.go index da2f851..a5970b3 100644 --- a/main.go +++ b/main.go @@ -26,17 +26,17 @@ func main() { ctx, fnCancel := signal.NotifyContext(context.Background(), os.Kill, os.Interrupt) defer fnCancel() - sessionStore := services.NewSessionStore(ctx) + auth := services.NewAuthentication(ctx) srv := server.NewServer( ctx, services.GetEnv().GetPort(), server.NewHandler("/", home.Handler()), - server.NewHandler("/upload", upload.Handler(sessionStore)), - server.NewHandler("/login", login.Handler(sessionStore)), + server.NewHandler("/upload", upload.Handler(auth)), + server.NewHandler("/login", login.Handler(auth)), ) srv.Serve() <-srv.Done() - <-sessionStore.Done() + <-auth.Done() } diff --git a/services/sessions.go b/services/authentication.go similarity index 64% rename from services/sessions.go rename to services/authentication.go index aa55a08..2dfaeeb 100644 --- a/services/sessions.go +++ b/services/authentication.go @@ -45,7 +45,14 @@ func (s *Session) GenerateCookie() *http.Cookie { } } -type SessionStore struct { +type IAuthenticate interface { + IsLogged(r *http.Request) bool + Authenticate(username, password string) (*Session, error) +} + +var _ IAuthenticate = (*Authentication)(nil) + +type Authentication struct { l sync.RWMutex ctx context.Context @@ -54,10 +61,10 @@ type SessionStore struct { sessions map[string]*Session } -func NewSessionStore(ctx context.Context) *SessionStore { +func NewAuthentication(ctx context.Context) *Authentication { ctxChild, fnCancel := context.WithCancel(ctx) - s := &SessionStore{ + s := &Authentication{ ctx: ctxChild, fnCancel: fnCancel, sessions: map[string]*Session{}, @@ -67,13 +74,13 @@ func NewSessionStore(ctx context.Context) *SessionStore { return s } -func (s *SessionStore) purge() { - s.l.Lock() - defer s.l.Unlock() +func (a *Authentication) purge() { + a.l.Lock() + defer a.l.Unlock() now := time.Now() toDelete := []*Session{} - for _, session := range s.sessions { + for _, session := range a.sessions { if now.After(session.expirationTime) { toDelete = append(toDelete, session) } @@ -81,18 +88,18 @@ func (s *SessionStore) purge() { for _, session := range toDelete { log.Debug().Str("sessionId", session.sessionID).Msg("purge expired session") - delete(s.sessions, session.sessionID) + delete(a.sessions, session.sessionID) } } -func (s *SessionStore) purgeWorker() { +func (a *Authentication) purgeWorker() { ticker := time.NewTicker(10 * time.Second) //nolint go func() { for { select { case <-ticker.C: - s.purge() - case <-s.ctx.Done(): + a.purge() + case <-a.ctx.Done(): log.Info().Msg("purge worker stopped") ticker.Stop() return @@ -101,45 +108,50 @@ func (s *SessionStore) purgeWorker() { }() } -func (s *SessionStore) Stop() { - s.fnCancel() +func (a *Authentication) Stop() { + a.fnCancel() } -func (s *SessionStore) Done() <-chan struct{} { - return s.ctx.Done() +func (a *Authentication) Done() <-chan struct{} { + return a.ctx.Done() } -func (s *SessionStore) NewSession() (*Session, error) { +func (a *Authentication) Authenticate(username, password string) (*Session, error) { + adminUsername, adminPassword := GetEnv().GetCredentials() + if username != adminUsername || password != adminPassword { + return nil, ErrUnauthorized + } + sessionID, err := generateSessionID() if err != nil { log.Err(err).Msg("unable to generate sessionId") return nil, err } - s.l.Lock() - defer s.l.Unlock() + a.l.Lock() + defer a.l.Unlock() - if _, ok := s.sessions[sessionID]; ok { + if _, ok := a.sessions[sessionID]; ok { log.Error().Str("sessionId", sessionID).Msg("sessionId collision") return nil, ErrSessionIDCollision } now := time.Now().Add(GetEnv().GetSessionExpirationDuration()) session := Session{expirationTime: now, sessionID: sessionID} - s.sessions[sessionID] = &session + a.sessions[sessionID] = &session return &session, nil } -func (s *SessionStore) IsLogged(r *http.Request) bool { +func (a *Authentication) IsLogged(r *http.Request) bool { cookie, err := r.Cookie("session_id") if err != nil { return false } - s.l.RLock() - defer s.l.RUnlock() + a.l.RLock() + defer a.l.RUnlock() - _, ok := s.sessions[cookie.Value] + _, ok := a.sessions[cookie.Value] return ok }