246 lines
7 KiB
Go
246 lines
7 KiB
Go
|
/*
|
||
|
Copyright 2017-2022 CAcert Inc.
|
||
|
SPDX-License-Identifier: Apache-2.0
|
||
|
|
||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
you may not use this file except in compliance with the License.
|
||
|
You may obtain a copy of the License at
|
||
|
|
||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||
|
|
||
|
Unless required by applicable law or agreed to in writing, software
|
||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
See the License for the specific language governing permissions and
|
||
|
limitations under the License.
|
||
|
*/
|
||
|
|
||
|
package u
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"fmt"
|
||
|
"io/fs"
|
||
|
"log"
|
||
|
"net/http"
|
||
|
"strconv"
|
||
|
|
||
|
"github.com/alexedwards/scs/v2"
|
||
|
"github.com/go-chi/chi/v5"
|
||
|
"github.com/go-chi/chi/v5/middleware"
|
||
|
"github.com/go-playground/form/v4"
|
||
|
"github.com/jmoiron/sqlx"
|
||
|
"github.com/vearutop/statigz"
|
||
|
"github.com/vearutop/statigz/brotli"
|
||
|
|
||
|
"git.cacert.org/cacert-boardvoting/internal/handlers"
|
||
|
"git.cacert.org/cacert-boardvoting/internal/jobs"
|
||
|
"git.cacert.org/cacert-boardvoting/internal/models"
|
||
|
"git.cacert.org/cacert-boardvoting/internal/notifications"
|
||
|
"git.cacert.org/cacert-boardvoting/ui"
|
||
|
)
|
||
|
|
||
|
type Application struct {
|
||
|
errorLog, infoLog *log.Logger
|
||
|
users *models.UserModel
|
||
|
motions *models.MotionModel
|
||
|
jobScheduler *jobs.JobScheduler
|
||
|
mailNotifier *notifications.MailNotifier
|
||
|
mailConfig *notifications.MailConfig
|
||
|
templateCache *handlers.TemplateCache
|
||
|
sessionManager *scs.SessionManager
|
||
|
formDecoder *form.Decoder
|
||
|
}
|
||
|
|
||
|
func New(
|
||
|
errorLog, infoLog *log.Logger,
|
||
|
db *sqlx.DB,
|
||
|
mailConfig *notifications.MailConfig,
|
||
|
sessionManager *scs.SessionManager,
|
||
|
) (*Application, error) {
|
||
|
app := &Application{
|
||
|
errorLog: errorLog,
|
||
|
infoLog: infoLog,
|
||
|
mailConfig: mailConfig,
|
||
|
motions: &models.MotionModel{DB: db},
|
||
|
users: &models.UserModel{DB: db},
|
||
|
sessionManager: sessionManager,
|
||
|
}
|
||
|
|
||
|
var err error
|
||
|
|
||
|
app.templateCache, err = handlers.NewTemplateCache()
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("could not initialize template cache: %w", err)
|
||
|
}
|
||
|
|
||
|
app.setupFormDecoder()
|
||
|
|
||
|
app.mailNotifier = notifications.NewMailNotifier(
|
||
|
app.mailConfig,
|
||
|
notifications.NotifierLog(app.infoLog, app.errorLog),
|
||
|
)
|
||
|
|
||
|
go app.mailNotifier.Start()
|
||
|
|
||
|
app.jobScheduler = jobs.NewJobScheduler(jobs.SchedulerLog(app.infoLog, app.errorLog))
|
||
|
|
||
|
app.jobScheduler.AddJob(jobs.NewCloseDecisionsJob(
|
||
|
app.motions,
|
||
|
app.mailNotifier,
|
||
|
jobs.CloseDecisionsLog(app.infoLog, app.errorLog),
|
||
|
))
|
||
|
|
||
|
app.jobScheduler.AddJob(jobs.NewRemindVoters(
|
||
|
app.users, app.motions, app.mailNotifier,
|
||
|
jobs.RemindVotersLog(app.infoLog, app.errorLog),
|
||
|
))
|
||
|
|
||
|
go app.jobScheduler.Schedule()
|
||
|
|
||
|
return app, nil
|
||
|
}
|
||
|
|
||
|
func (app *Application) Close() error {
|
||
|
app.jobScheduler.Quit()
|
||
|
app.mailNotifier.Quit()
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (app *Application) setupFormDecoder() {
|
||
|
decoder := form.NewDecoder()
|
||
|
|
||
|
decoder.RegisterCustomTypeFunc(func(values []string) (interface{}, error) {
|
||
|
v, err := models.VoteTypeFromString(values[0])
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("could not convert value %s: %w", values[0], err)
|
||
|
}
|
||
|
|
||
|
return v, nil
|
||
|
}, new(models.VoteType))
|
||
|
decoder.RegisterCustomTypeFunc(func(values []string) (interface{}, error) {
|
||
|
v, err := models.VoteChoiceFromString(values[0])
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("could not convert value %s: %w", values[0], err)
|
||
|
}
|
||
|
|
||
|
return v, nil
|
||
|
}, new(models.VoteChoice))
|
||
|
decoder.RegisterCustomTypeFunc(func(values []string) (interface{}, error) {
|
||
|
userID, err := strconv.Atoi(values[0])
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("could not convert value %s to user ID: %w", values[0], err)
|
||
|
}
|
||
|
|
||
|
u, err := app.users.ByID(context.Background(), int64(userID))
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("could not convert value %s to user: %w", values[0], err)
|
||
|
}
|
||
|
|
||
|
return u, nil
|
||
|
}, new(models.User))
|
||
|
|
||
|
app.formDecoder = decoder
|
||
|
}
|
||
|
|
||
|
func (app *Application) Routes() http.Handler {
|
||
|
staticDir, _ := fs.Sub(ui.Files, "static")
|
||
|
|
||
|
staticData, ok := staticDir.(fs.ReadDirFS)
|
||
|
if !ok {
|
||
|
app.errorLog.Fatal("could not use uiStaticDir as fs.ReadDirFS")
|
||
|
}
|
||
|
|
||
|
fileServer := statigz.FileServer(staticData, brotli.AddEncoding, statigz.EncodeOnInit)
|
||
|
|
||
|
router := chi.NewRouter()
|
||
|
|
||
|
router.Use(middleware.RealIP)
|
||
|
router.Use(middleware.RequestLogger(&middleware.DefaultLogFormatter{Logger: app.infoLog}))
|
||
|
router.Use(middleware.Recoverer)
|
||
|
router.Use(handlers.SecureHeaders)
|
||
|
|
||
|
router.NotFound(func(w http.ResponseWriter, _ *http.Request) { handlers.NotFound(w) })
|
||
|
|
||
|
router.Get(
|
||
|
"/",
|
||
|
http.RedirectHandler("/motions/", http.StatusMovedPermanently).ServeHTTP,
|
||
|
)
|
||
|
router.Get(
|
||
|
"/favicon.ico",
|
||
|
http.RedirectHandler("/static/images/favicon.ico", http.StatusMovedPermanently).ServeHTTP,
|
||
|
)
|
||
|
router.Get("/static/*", http.StripPrefix("/static", fileServer).ServeHTTP)
|
||
|
|
||
|
userMiddleware := handlers.NewUserMiddleware(app.users, app.errorLog)
|
||
|
|
||
|
flashes := handlers.NewFlashes(app.sessionManager)
|
||
|
|
||
|
handlerParams := handlers.CommonParams{
|
||
|
Flashes: flashes,
|
||
|
TemplateCache: app.templateCache,
|
||
|
FormDecoder: app.formDecoder,
|
||
|
}
|
||
|
|
||
|
motionHandler := handlers.NewMotionHandler(app.motions, app.mailNotifier, app.jobScheduler, handlerParams)
|
||
|
voteHandler := handlers.NewVoteHandler(app.motions, app.users, app.mailNotifier, handlerParams)
|
||
|
userHandler := handlers.NewUserHandler(app.users, handlerParams)
|
||
|
|
||
|
router.Group(func(r chi.Router) {
|
||
|
r.Use(app.sessionManager.LoadAndSave, userMiddleware.TryAuthenticate)
|
||
|
|
||
|
r.Get("/motions/", motionHandler.List)
|
||
|
r.Get("/motions/{tag}", motionHandler.Details)
|
||
|
|
||
|
r.Group(func(r chi.Router) {
|
||
|
r.Use(userMiddleware.UserCanEditVote, handlers.NoSurf)
|
||
|
|
||
|
r.Get("/newmotion/", motionHandler.NewForm)
|
||
|
r.Post("/newmotion/", motionHandler.New)
|
||
|
|
||
|
r.Route("/motions/{tag}", func(r chi.Router) {
|
||
|
r.Get("/edit", motionHandler.EditForm)
|
||
|
r.Post("/edit", motionHandler.Edit)
|
||
|
r.Get("/withdraw", motionHandler.WithdrawForm)
|
||
|
r.Post("/withdraw", motionHandler.Withdraw)
|
||
|
})
|
||
|
})
|
||
|
|
||
|
r.Group(func(r chi.Router) {
|
||
|
r.Use(userMiddleware.UserCanVote, handlers.NoSurf)
|
||
|
|
||
|
r.Get("/vote/{tag}/{choice}", voteHandler.VoteForm)
|
||
|
r.Post("/vote/{tag}/{choice}", voteHandler.Vote)
|
||
|
r.Get("/proxy/{tag}", voteHandler.ProxyVoteForm)
|
||
|
r.Post("/proxy/{tag}", voteHandler.ProxyVote)
|
||
|
})
|
||
|
|
||
|
r.Group(func(r chi.Router) {
|
||
|
r.Use(userMiddleware.CanManageUsers, handlers.NoSurf)
|
||
|
|
||
|
r.Get("/users/", userHandler.List)
|
||
|
r.Get("/new-user/", userHandler.CreateForm)
|
||
|
r.Post("/new-user/", userHandler.Create)
|
||
|
|
||
|
r.Route("/users/{id}", func(r chi.Router) {
|
||
|
r.Get("/", userHandler.EditForm)
|
||
|
r.Post("/", userHandler.Edit)
|
||
|
r.Get("/add-mail", userHandler.AddEmailForm)
|
||
|
r.Post("/add-mail", userHandler.AddEmail)
|
||
|
r.Get("/mail/{address}/delete", userHandler.DeleteEmailForm)
|
||
|
r.Post("/mail/{address}/delete", userHandler.DeleteEmail)
|
||
|
r.Get("/delete", userHandler.DeleteForm)
|
||
|
r.Post("/delete", userHandler.Delete)
|
||
|
})
|
||
|
|
||
|
r.Get("/voters/", userHandler.ChangeVotersForm)
|
||
|
r.Post("/voters/", userHandler.ChangeVoters)
|
||
|
})
|
||
|
})
|
||
|
|
||
|
router.Method(http.MethodGet, "/health", handlers.NewHealthCheck(app.mailNotifier, app.motions))
|
||
|
|
||
|
return router
|
||
|
}
|