First commit
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gisle/stufflog/config"
|
||||
"github.com/gisle/stufflog/database"
|
||||
"github.com/gisle/stufflog/internal/generate"
|
||||
"github.com/gisle/stufflog/models"
|
||||
"github.com/gisle/stufflog/slerrors"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"time"
|
||||
)
|
||||
|
||||
var dummyPassword []byte
|
||||
|
||||
type ctxKey string
|
||||
|
||||
const sessionCookie = "stufflog_sessid"
|
||||
const sessionCtxKey = ctxKey("stufflog_session")
|
||||
const userCtxKey = ctxKey("stufflog_user")
|
||||
|
||||
type AuthService struct {
|
||||
db database.Database
|
||||
}
|
||||
|
||||
// UserFromContext gets the User object stored in the context.
|
||||
func (s *AuthService) UserFromContext(ctx context.Context) *models.User {
|
||||
if gctx, ok := ctx.(*gin.Context); ok {
|
||||
ctx = gctx.Request.Context()
|
||||
}
|
||||
|
||||
session := s.SessionFromContext(ctx)
|
||||
if session == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
user, _ := ctx.Value(userCtxKey).(*models.User)
|
||||
return user
|
||||
}
|
||||
|
||||
// SessionFromContext gets the Session object stored in the context.
|
||||
func (s *AuthService) SessionFromContext(ctx context.Context) *models.UserSession {
|
||||
if gctx, ok := ctx.(*gin.Context); ok {
|
||||
ctx = gctx.Request.Context()
|
||||
}
|
||||
|
||||
session, _ := ctx.Value(sessionCtxKey).(*models.UserSession)
|
||||
if session == nil || time.Now().After(session.Expires) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return session
|
||||
}
|
||||
|
||||
// Register registers a new user.
|
||||
func (s *AuthService) Register(ctx context.Context, username, password string) (*models.User, error) {
|
||||
if !config.Get().Users.AllowRegister {
|
||||
return nil, &slerrors.SLError{Code: 403, Text: "Registration is disabled"}
|
||||
}
|
||||
|
||||
if len(password) < 6 {
|
||||
return nil, &slerrors.SLError{Code: 400, Text: "Password is too short"}
|
||||
}
|
||||
if len(username) == 0 {
|
||||
return nil, &slerrors.SLError{Code: 400, Text: "No username provided"}
|
||||
}
|
||||
|
||||
if _, err := s.db.Users().FindID(ctx, username); !slerrors.IsNotFound(err) {
|
||||
return nil, &slerrors.SLError{Code: 409, Text: "Username is not available"}
|
||||
}
|
||||
|
||||
user := models.User{
|
||||
ID: username,
|
||||
Name: username,
|
||||
}
|
||||
err := user.SetPassword(password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = s.db.Users().Save(ctx, user)
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// Login logs in the user and creates the session. The session is not saved to the request. For that you need to use
|
||||
// GinSaveSession on the returned session.
|
||||
func (s *AuthService) Login(ctx context.Context, userID, password string) (*models.User, *models.UserSession, error) {
|
||||
user, err := s.db.Users().FindID(ctx, userID)
|
||||
if err != nil {
|
||||
_ = bcrypt.CompareHashAndPassword(dummyPassword, []byte("u r a dummy too"))
|
||||
return nil, nil, slerrors.LoginFailed()
|
||||
}
|
||||
|
||||
err = user.CheckPassword(password)
|
||||
if err != nil {
|
||||
return nil, nil, slerrors.LoginFailed()
|
||||
}
|
||||
|
||||
session := models.UserSession{
|
||||
ID: generate.ID("S", 64),
|
||||
Expires: time.Now().Add(time.Hour * 24 * 30),
|
||||
UserID: user.ID,
|
||||
}
|
||||
err = s.db.UserSessions().Save(ctx, session)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return user, &session, nil
|
||||
}
|
||||
|
||||
// Logout deletes the session associated with the context.
|
||||
func (s *AuthService) Logout(ctx context.Context) error {
|
||||
session := s.SessionFromContext(ctx)
|
||||
if session == nil {
|
||||
return slerrors.Unauthorized().WithText("You're already logged out.")
|
||||
}
|
||||
|
||||
session.Expires = time.Time{}
|
||||
|
||||
return s.db.UserSessions().Remove(ctx, *session)
|
||||
}
|
||||
|
||||
// SaveSession saves a session to gin.
|
||||
func (s *AuthService) GinSaveSession(c *gin.Context, session models.UserSession) {
|
||||
c.SetCookie(sessionCookie, session.ID, 336*3600, "", "", false, true)
|
||||
}
|
||||
|
||||
// SaveSession saves a session to gin.
|
||||
func (s *AuthService) GinClearSession(c *gin.Context) {
|
||||
c.SetCookie(sessionCookie, "", 0, "", "", false, true)
|
||||
}
|
||||
|
||||
// Middleware is a middleware that adds a session to the context, or
|
||||
// gives a 403 Unauthorized error if there is none.
|
||||
func (s *AuthService) GinSessionMiddleware(required bool) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
// Find cookie
|
||||
cookie, err := c.Cookie(sessionCookie)
|
||||
if err != nil || cookie == "" {
|
||||
if required {
|
||||
slerrors.GinRespond(c, slerrors.Unauthorized())
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// Check session existence
|
||||
session, err := s.db.UserSessions().FindID(ctx, cookie)
|
||||
if err != nil {
|
||||
c.SetCookie(sessionCookie, "", 0, "", "", false, true)
|
||||
|
||||
if required {
|
||||
slerrors.GinRespond(c, slerrors.Unauthorized())
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
ctx = context.WithValue(ctx, sessionCtxKey, session)
|
||||
|
||||
// Find the user associated with the session
|
||||
user, err := s.db.Users().FindID(ctx, session.UserID)
|
||||
if err != nil {
|
||||
c.SetCookie(sessionCookie, "", 0, "", "", false, true)
|
||||
_ = s.db.UserSessions().Remove(ctx, *session)
|
||||
|
||||
if required {
|
||||
slerrors.GinRespond(c, slerrors.Unauthorized())
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
ctx = context.WithValue(ctx, userCtxKey, user)
|
||||
|
||||
// Check if session has expired
|
||||
if time.Now().After(session.Expires) {
|
||||
c.SetCookie(sessionCookie, "", 0, "", "", false, true)
|
||||
_ = s.db.UserSessions().Remove(ctx, *session)
|
||||
|
||||
if required {
|
||||
slerrors.GinRespond(c, slerrors.Unauthorized())
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// If there's less than 27.75 days, reset expiry.
|
||||
if time.Until(session.Expires) < time.Hour*666 {
|
||||
session.Expires = time.Now().Add(time.Hour * 672)
|
||||
s.GinSaveSession(c, *session)
|
||||
_ = s.db.UserSessions().Save(c.Request.Context(), *session)
|
||||
}
|
||||
|
||||
// Proceed.
|
||||
c.Request = c.Request.WithContext(ctx)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func NewAuthService(db database.Database) *AuthService {
|
||||
return &AuthService{db: db}
|
||||
}
|
||||
|
||||
func init() {
|
||||
dummyPassword, _ = bcrypt.GenerateFromPassword([]byte("u r a dummy"), bcrypt.DefaultCost)
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/gisle/stufflog/database"
|
||||
"github.com/gisle/stufflog/models"
|
||||
"github.com/gisle/stufflog/slerrors"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ScoringService scores activities. It cares not for users.
|
||||
type ScoringService struct {
|
||||
db database.Database
|
||||
}
|
||||
|
||||
func (s *ScoringService) ScoreOne(ctx context.Context, period models.Period, log models.PeriodLog) (*models.Score, error) {
|
||||
return s.scoreOne(ctx, period, log)
|
||||
}
|
||||
|
||||
func (s *ScoringService) scoreOne(ctx context.Context, period models.Period, log models.PeriodLog) (*models.Score, error) {
|
||||
goal := period.Goal(log.GoalID)
|
||||
if goal == nil {
|
||||
return nil, slerrors.NotFound("Goal")
|
||||
}
|
||||
|
||||
activity, err := s.db.Activities().FindID(ctx, goal.ActivityID)
|
||||
if err != nil || activity.UserID != period.UserID {
|
||||
return nil, slerrors.NotFound("Goal's Activity")
|
||||
}
|
||||
subActivity := activity.SubActivity(log.SubActivityID)
|
||||
if subActivity == nil {
|
||||
return nil, slerrors.NotFound("Goal's Sub-Activity")
|
||||
}
|
||||
|
||||
score := models.Score{
|
||||
ActivityScore: subActivity.Value,
|
||||
Amount: log.Amount,
|
||||
}
|
||||
|
||||
if log.SubGoalID != "" {
|
||||
subGoal := goal.SubGoal(log.SubGoalID)
|
||||
if subGoal == nil {
|
||||
return nil, slerrors.NotFound("Sub-Goal")
|
||||
}
|
||||
|
||||
multiplier := subGoal.Multiplier
|
||||
score.SubGoalMultiplier = &multiplier
|
||||
}
|
||||
|
||||
if activity.DailyBonus > 0 {
|
||||
isFirst := true
|
||||
for _, log2 := range period.Logs {
|
||||
if log2.Date.Local().Truncate(time.Hour*24) != log.Date.Local().Truncate(time.Hour*24) {
|
||||
continue
|
||||
}
|
||||
|
||||
if log2.ID == log.ID {
|
||||
break
|
||||
}
|
||||
if log2.GoalID == log.GoalID {
|
||||
isFirst = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if isFirst {
|
||||
score.DailyBonus = activity.DailyBonus
|
||||
}
|
||||
}
|
||||
|
||||
score.Calc()
|
||||
|
||||
return &score, nil
|
||||
}
|
||||
|
||||
func (s *ScoringService) ScoreAll(ctx context.Context, period models.Period) (*models.Period, error) {
|
||||
period = *period.Copy()
|
||||
|
||||
for i, log := range period.Logs {
|
||||
score, err := s.scoreOne(ctx, period, log)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
period.Logs[i].Score = score
|
||||
}
|
||||
|
||||
err := s.db.Periods().Insert(ctx, period)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &period, nil
|
||||
}
|
||||
|
||||
func NewScoringService(db database.Database) *ScoringService {
|
||||
return &ScoringService{db: db}
|
||||
}
|
||||
Reference in New Issue
Block a user