1
0
mirror of https://github.com/muety/wakapi.git synced 2023-08-10 21:12:56 +03:00

Merge branch 'master' into fork

This commit is contained in:
bdeshi
2022-04-24 03:56:18 +06:00
committed by GitHub
38 changed files with 1360 additions and 1135 deletions

View File

@ -5,7 +5,9 @@ import (
"github.com/gorilla/mux"
lru "github.com/hashicorp/golang-lru"
conf "github.com/muety/wakapi/config"
"github.com/muety/wakapi/utils"
"net/http"
"time"
)
type AvatarHandler struct {
@ -33,6 +35,10 @@ func (h *AvatarHandler) RegisterRoutes(router *mux.Router) {
func (h *AvatarHandler) Get(w http.ResponseWriter, r *http.Request) {
hash := mux.Vars(r)["hash"]
if utils.IsNoCache(r, 1*time.Hour) {
h.cache.Remove(hash)
}
if !h.cache.Contains(hash) {
h.cache.Add(hash, avatars.MakeMaleAvatar(hash))
}

98
routes/api/badge.go Normal file
View File

@ -0,0 +1,98 @@
package api
import (
"fmt"
"github.com/duke-git/lancet/v2/maputil"
"github.com/duke-git/lancet/v2/slice"
"github.com/gorilla/mux"
conf "github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
v1 "github.com/muety/wakapi/models/compat/shields/v1"
routeutils "github.com/muety/wakapi/routes/utils"
"github.com/muety/wakapi/services"
"github.com/muety/wakapi/utils"
"github.com/narqo/go-badge"
"github.com/patrickmn/go-cache"
"net/http"
"time"
)
type BadgeHandler struct {
config *conf.Config
cache *cache.Cache
userSrvc services.IUserService
summarySrvc services.ISummaryService
}
func NewBadgeHandler(userService services.IUserService, summaryService services.ISummaryService) *BadgeHandler {
return &BadgeHandler{
config: conf.Get(),
cache: cache.New(time.Hour, time.Hour),
userSrvc: userService,
summarySrvc: summaryService,
}
}
func (h *BadgeHandler) RegisterRoutes(router *mux.Router) {
r := router.PathPrefix("/badge/{user}").Subrouter()
r.Methods(http.MethodGet).HandlerFunc(h.Get)
}
func (h *BadgeHandler) Get(w http.ResponseWriter, r *http.Request) {
user, err := h.userSrvc.GetUserById(mux.Vars(r)["user"])
if err != nil {
w.WriteHeader(http.StatusNotFound)
return
}
interval, filters, err := routeutils.GetBadgeParams(r, user)
if err != nil {
w.WriteHeader(http.StatusForbidden)
w.Write([]byte(err.Error()))
return
}
cacheKey := fmt.Sprintf("%s_%v_%s", user.ID, *interval.Key, filters.Hash())
noCache := utils.IsNoCache(r, 1*time.Hour)
if cacheResult, ok := h.cache.Get(cacheKey); ok && !noCache {
respondSvg(w, cacheResult.([]byte))
return
}
params := &models.SummaryParams{
From: interval.Start,
To: interval.End,
User: user,
Filters: filters,
}
summary, err, status := routeutils.LoadUserSummaryByParams(h.summarySrvc, params)
if err != nil {
w.WriteHeader(status)
w.Write([]byte(err.Error()))
return
}
badgeData := v1.NewBadgeDataFrom(summary)
if customLabel := r.URL.Query().Get("label"); customLabel != "" {
badgeData.Label = customLabel
}
if customColor := r.URL.Query().Get("color"); customColor != "" {
badgeData.Color = customColor
}
if badgeData.Color[0:1] != "#" && !slice.Contain(maputil.Keys(badge.ColorScheme), badgeData.Color) {
badgeData.Color = "#" + badgeData.Color
}
badgeSvg, err := badge.RenderBytes(badgeData.Label, badgeData.Message, badge.Color(badgeData.Color))
h.cache.SetDefault(cacheKey, badgeSvg)
respondSvg(w, badgeSvg)
}
func respondSvg(w http.ResponseWriter, data []byte) {
w.Header().Set("Content-Type", "image/svg+xml")
w.Header().Set("Cache-Control", "max-age=3600")
w.WriteHeader(http.StatusOK)
w.Write(data)
}

View File

@ -6,7 +6,6 @@ import (
"github.com/gorilla/mux"
conf "github.com/muety/wakapi/config"
"github.com/muety/wakapi/middlewares"
"github.com/muety/wakapi/services"
"github.com/muety/wakapi/utils"
@ -29,9 +28,6 @@ func NewDiagnosticsApiHandler(userService services.IUserService, diagnosticsServ
func (h *DiagnosticsApiHandler) RegisterRoutes(router *mux.Router) {
r := router.PathPrefix("/plugins/errors").Subrouter()
r.Use(
middlewares.NewAuthenticateMiddleware(h.userSrvc).Handler,
)
r.Path("").Methods(http.MethodPost).HandlerFunc(h.Post)
}

View File

@ -2,8 +2,8 @@ package v1
import (
"fmt"
routeutils "github.com/muety/wakapi/routes/utils"
"net/http"
"regexp"
"time"
"github.com/gorilla/mux"
@ -15,11 +15,6 @@ import (
"github.com/patrickmn/go-cache"
)
const (
intervalPattern = `interval:([a-z0-9_]+)`
entityFilterPattern = `(project|os|editor|language|machine|label):([_a-zA-Z0-9-\s\.]+)`
)
type BadgeHandler struct {
config *conf.Config
userSrvc services.IUserService
@ -53,77 +48,33 @@ func (h *BadgeHandler) RegisterRoutes(router *mux.Router) {
// @Success 200 {object} v1.BadgeData
// @Router /compat/shields/v1/{user}/{interval}/{filter} [get]
func (h *BadgeHandler) Get(w http.ResponseWriter, r *http.Request) {
intervalReg := regexp.MustCompile(intervalPattern)
entityFilterReg := regexp.MustCompile(entityFilterPattern)
var filterEntity, filterKey string
if groups := entityFilterReg.FindStringSubmatch(r.URL.Path); len(groups) > 2 {
filterEntity, filterKey = groups[1], groups[2]
}
var interval = models.IntervalPast30Days
if groups := intervalReg.FindStringSubmatch(r.URL.Path); len(groups) > 1 {
if i, err := utils.ParseInterval(groups[1]); err == nil {
interval = i
}
}
requestedUserId := mux.Vars(r)["user"]
user, err := h.userSrvc.GetUserById(requestedUserId)
user, err := h.userSrvc.GetUserById(mux.Vars(r)["user"])
if err != nil {
w.WriteHeader(http.StatusNotFound)
return
}
_, rangeFrom, rangeTo := utils.ResolveIntervalTZ(interval, user.TZ())
minStart := rangeTo.Add(-24 * time.Hour * time.Duration(user.ShareDataMaxDays))
// negative value means no limit
if rangeFrom.Before(minStart) && user.ShareDataMaxDays >= 0 {
interval, filters, err := routeutils.GetBadgeParams(r, user)
if err != nil {
w.WriteHeader(http.StatusForbidden)
w.Write([]byte("requested time range too broad"))
w.Write([]byte(err.Error()))
return
}
var permitEntity bool
var filters *models.Filters
switch filterEntity {
case "project":
permitEntity = user.ShareProjects
filters = models.NewFiltersWith(models.SummaryProject, filterKey)
case "os":
permitEntity = user.ShareOSs
filters = models.NewFiltersWith(models.SummaryOS, filterKey)
case "editor":
permitEntity = user.ShareEditors
filters = models.NewFiltersWith(models.SummaryEditor, filterKey)
case "language":
permitEntity = user.ShareLanguages
filters = models.NewFiltersWith(models.SummaryLanguage, filterKey)
case "machine":
permitEntity = user.ShareMachines
filters = models.NewFiltersWith(models.SummaryMachine, filterKey)
case "label":
permitEntity = user.ShareLabels
filters = models.NewFiltersWith(models.SummaryLabel, filterKey)
// branches are intentionally omitted here, as only relevant in combination with a project filter
default:
permitEntity = true
filters = &models.Filters{}
}
if !permitEntity {
w.WriteHeader(http.StatusForbidden)
w.Write([]byte("user did not opt in to share entity-specific data"))
return
}
cacheKey := fmt.Sprintf("%s_%v_%s_%s", user.ID, *interval, filterEntity, filterKey)
cacheKey := fmt.Sprintf("%s_%v_%s", user.ID, *interval.Key, filters.Hash())
if cacheResult, ok := h.cache.Get(cacheKey); ok {
utils.RespondJSON(w, r, http.StatusOK, cacheResult.(*v1.BadgeData))
return
}
summary, err, status := h.loadUserSummary(user, interval, filters)
params := &models.SummaryParams{
From: interval.Start,
To: interval.End,
User: user,
Filters: filters,
}
summary, err, status := routeutils.LoadUserSummaryByParams(h.summarySrvc, params)
if err != nil {
w.WriteHeader(status)
w.Write([]byte(err.Error()))

View File

@ -30,7 +30,7 @@ func TestBadgeHandler_EntityPattern(t *testing.T) {
{test: pathPrefix + "project:Anchr-Android_v2.0", key: "project", val: "Anchr-Android_v2.0"}, // all the way
}
sut := regexp.MustCompile(entityFilterPattern)
sut := regexp.MustCompile(`(project|os|editor|language|machine|label):([^:?&/]+)`) // see entityFilterPattern in badge_utils.go
for _, tc := range tests {
var key, val string

View File

@ -91,6 +91,7 @@ func (h *LoginHandler) PostLogin(w http.ResponseWriter, r *http.Request) {
encoded, err := h.config.Security.SecureCookie.Encode(models.AuthCookieKey, login.Username)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
conf.Log().Request(r).Error("failed to encode secure cookie - %v", err)
templates[conf.LoginTemplate].Execute(w, h.buildViewModel(r).WithError("internal server error"))
return
}
@ -163,6 +164,7 @@ func (h *LoginHandler) PostSignup(w http.ResponseWriter, r *http.Request) {
_, created, err := h.userSrvc.CreateOrGet(&signup, numUsers == 0)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
conf.Log().Request(r).Error("failed to create new user - %v", err)
templates[conf.SignupTemplate].Execute(w, h.buildViewModel(r).WithError("failed to create new user"))
return
}
@ -237,6 +239,7 @@ func (h *LoginHandler) PostSetPassword(w http.ResponseWriter, r *http.Request) {
user.ResetToken = ""
if hash, err := utils.HashBcrypt(user.Password, h.config.Security.PasswordSalt); err != nil {
w.WriteHeader(http.StatusInternalServerError)
conf.Log().Request(r).Error("failed to set new password - %v", err)
templates[conf.SetPasswordTemplate].Execute(w, h.buildViewModel(r).WithError("failed to set new password"))
return
} else {
@ -245,6 +248,7 @@ func (h *LoginHandler) PostSetPassword(w http.ResponseWriter, r *http.Request) {
if _, err := h.userSrvc.Update(user); err != nil {
w.WriteHeader(http.StatusInternalServerError)
conf.Log().Request(r).Error("failed to save new password - %v", err)
templates[conf.SetPasswordTemplate].Execute(w, h.buildViewModel(r).WithError("failed to save new password"))
return
}
@ -278,6 +282,7 @@ func (h *LoginHandler) PostResetPassword(w http.ResponseWriter, r *http.Request)
if user, err := h.userSrvc.GetUserByEmail(resetRequest.Email); user != nil && err == nil {
if u, err := h.userSrvc.GenerateResetToken(user); err != nil {
w.WriteHeader(http.StatusInternalServerError)
conf.Log().Request(r).Error("failed to generate password reset token - %v", err)
templates[conf.ResetPasswordTemplate].Execute(w, h.buildViewModel(r).WithError("failed to generate password reset token"))
return
} else {

View File

@ -51,6 +51,7 @@ func (h *SummaryHandler) GetIndex(w http.ResponseWriter, r *http.Request) {
summary, err, status := su.LoadUserSummary(h.summarySrvc, r)
if err != nil {
w.WriteHeader(status)
conf.Log().Request(r).Error("failed to load summary - %v", err)
templates[conf.SummaryTemplate].Execute(w, h.buildViewModel(r).WithError(err.Error()))
return
}

View File

@ -0,0 +1,85 @@
package utils
import (
"errors"
"github.com/muety/wakapi/models"
"github.com/muety/wakapi/utils"
"net/http"
"regexp"
"time"
)
const (
intervalPattern = `interval:([a-z0-9_]+)`
entityFilterPattern = `(project|os|editor|language|machine|label):([^:?&/]+)`
)
var (
intervalReg *regexp.Regexp
entityFilterReg *regexp.Regexp
)
func init() {
intervalReg = regexp.MustCompile(intervalPattern)
entityFilterReg = regexp.MustCompile(entityFilterPattern)
}
func GetBadgeParams(r *http.Request, requestedUser *models.User) (*models.KeyedInterval, *models.Filters, error) {
var filterEntity, filterKey string
if groups := entityFilterReg.FindStringSubmatch(r.URL.Path); len(groups) > 2 {
filterEntity, filterKey = groups[1], groups[2]
}
var intervalKey = models.IntervalPast30Days
if groups := intervalReg.FindStringSubmatch(r.URL.Path); len(groups) > 1 {
if i, err := utils.ParseInterval(groups[1]); err == nil {
intervalKey = i
}
}
_, rangeFrom, rangeTo := utils.ResolveIntervalTZ(intervalKey, requestedUser.TZ())
interval := &models.KeyedInterval{
Interval: models.Interval{Start: rangeFrom, End: rangeTo},
Key: intervalKey,
}
minStart := rangeTo.Add(-24 * time.Hour * time.Duration(requestedUser.ShareDataMaxDays))
// negative value means no limit
if rangeFrom.Before(minStart) && requestedUser.ShareDataMaxDays >= 0 {
return nil, nil, errors.New("requested time range too broad")
}
var permitEntity bool
var filters *models.Filters
switch filterEntity {
case "project":
permitEntity = requestedUser.ShareProjects
filters = models.NewFiltersWith(models.SummaryProject, filterKey)
case "os":
permitEntity = requestedUser.ShareOSs
filters = models.NewFiltersWith(models.SummaryOS, filterKey)
case "editor":
permitEntity = requestedUser.ShareEditors
filters = models.NewFiltersWith(models.SummaryEditor, filterKey)
case "language":
permitEntity = requestedUser.ShareLanguages
filters = models.NewFiltersWith(models.SummaryLanguage, filterKey)
case "machine":
permitEntity = requestedUser.ShareMachines
filters = models.NewFiltersWith(models.SummaryMachine, filterKey)
case "label":
permitEntity = requestedUser.ShareLabels
filters = models.NewFiltersWith(models.SummaryLabel, filterKey)
// branches are intentionally omitted here, as only relevant in combination with a project filter
default:
// non-entity-specific request, just a general, in-total query
permitEntity = true
filters = &models.Filters{}
}
if !permitEntity {
return nil, nil, errors.New("user did not opt in to share entity-specific data")
}
return interval, filters, nil
}

View File

@ -1,7 +1,6 @@
package utils
import (
"github.com/muety/wakapi/middlewares"
"github.com/muety/wakapi/models"
"github.com/muety/wakapi/services"
"github.com/muety/wakapi/utils"
@ -9,24 +8,33 @@ import (
)
func LoadUserSummary(ss services.ISummaryService, r *http.Request) (*models.Summary, error, int) {
user := middlewares.GetPrincipal(r)
summaryParams, err := utils.ParseSummaryParams(r)
if err != nil {
return nil, err, http.StatusBadRequest
}
return LoadUserSummaryByParams(ss, summaryParams)
}
func LoadUserSummaryByParams(ss services.ISummaryService, params *models.SummaryParams) (*models.Summary, error, int) {
var retrieveSummary services.SummaryRetriever = ss.Retrieve
if summaryParams.Recompute {
if params.Recompute {
retrieveSummary = ss.Summarize
}
summary, err := ss.Aliased(summaryParams.From, summaryParams.To, summaryParams.User, retrieveSummary, summaryParams.Filters, summaryParams.Recompute)
summary, err := ss.Aliased(
params.From,
params.To,
params.User,
retrieveSummary,
params.Filters,
params.Recompute,
)
if err != nil {
return nil, err, http.StatusInternalServerError
}
summary.FromTime = models.CustomTime(summary.FromTime.T().In(user.TZ()))
summary.ToTime = models.CustomTime(summary.ToTime.T().In(user.TZ()))
summary.FromTime = models.CustomTime(summary.FromTime.T().In(params.User.TZ()))
summary.ToTime = models.CustomTime(summary.ToTime.T().In(params.User.TZ()))
return summary, nil, http.StatusOK
}