stufflog graphql server
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

227 lines
5.8 KiB

package services
import (
"context"
"errors"
"git.aiterp.net/stufflog/server/database/repositories"
"git.aiterp.net/stufflog/server/internal/generate"
"git.aiterp.net/stufflog/server/internal/xlerrors"
"git.aiterp.net/stufflog/server/models"
"github.com/gin-gonic/gin"
"math/rand"
"time"
)
var ErrLoginFailed = errors.New("login failed")
var ErrInternalLoginFailure = errors.New("login failed due to internal error")
var ErrInternalPermissionFailure = errors.New("permission check failed due to internal error")
var ErrWrongCurrentPassword = errors.New("current password is missing")
var ErrMissingCurrentPassword = errors.New("current password is missing")
var authCookieName = "stufflog_cookie"
var authCtxKey = "stufflog.auth"
var ginCtxKey = "stufflog.gin"
type Auth struct {
users repositories.UserRepository
session repositories.SessionRepository
projects repositories.ProjectRepository
}
func (auth *Auth) Login(ctx context.Context, username, password string) (*models.User, error) {
user, err := auth.users.Find(ctx, username)
if err != nil {
select {
case <-time.After(time.Millisecond * time.Duration(rand.Int63n(100)+100)):
case <-ctx.Done():
}
return nil, ErrLoginFailed
}
if !user.CheckPassword(password) {
select {
case <-time.After(time.Millisecond * time.Duration(rand.Int63n(50))):
case <-ctx.Done():
}
return nil, ErrLoginFailed
}
session := models.Session{
ID: generate.SessionID(),
UserID: user.ID,
ExpiryTime: time.Now().Add(time.Hour * 168),
}
err = auth.session.Save(ctx, session)
if err != nil {
return nil, ErrInternalLoginFailure
}
if c := ctx.Value(ginCtxKey).(*gin.Context); c != nil {
c.SetCookie(authCookieName, session.ID, 3600*168, "/", "", false, true)
} else {
return nil, ErrInternalLoginFailure
}
return user, nil
}
func (auth *Auth) Logout(ctx context.Context) (*models.User, error) {
user := auth.UserFromContext(ctx)
if user == nil {
return nil, xlerrors.PermissionDenied
}
c, ok := ctx.Value(ginCtxKey).(*gin.Context)
if !ok {
return nil, ErrInternalLoginFailure
}
c.SetCookie(authCookieName, "", 0, "/", "", false, true)
return user, nil
}
func (auth *Auth) CreateUser(ctx context.Context, username, password, name string, active, admin bool) (*models.User, error) {
loggedInUser := auth.UserFromContext(ctx)
if loggedInUser == nil || !loggedInUser.Admin {
return nil, xlerrors.PermissionDenied
}
user := &models.User{
ID: username,
Name: name,
Active: active,
Admin: admin,
}
err := user.SetPassword(password)
if err != nil {
return nil, err
}
user, err = auth.users.Insert(ctx, *user)
if err != nil {
return nil, err
}
return user, nil
}
func (auth *Auth) UserFromContext(ctx context.Context) *models.User {
user, _ := ctx.Value(authCtxKey).(*models.User)
return user
}
func (auth *Auth) ProjectPermission(ctx context.Context, project models.Project) (*models.ProjectPermission, error) {
user := auth.UserFromContext(ctx)
if user == nil || !user.Active {
return nil, xlerrors.PermissionDenied
}
permission, err := auth.projects.GetPermission(ctx, project, *user)
if err != nil {
return nil, ErrInternalPermissionFailure
}
if permission.Level == models.ProjectPermissionLevelNoAccess {
return nil, xlerrors.PermissionDenied
}
return permission, nil
}
func (auth *Auth) IssuePermission(ctx context.Context, issue models.Issue) (*models.ProjectPermission, error) {
user := auth.UserFromContext(ctx)
if user == nil || !user.Active {
return nil, xlerrors.PermissionDenied
}
isOwnedOrAssigned := issue.AssigneeID == user.ID || issue.OwnerID == user.ID
permission, err := auth.projects.GetIssuePermission(ctx, issue, *user)
if err != nil {
return nil, ErrInternalPermissionFailure
}
if permission.Level == models.ProjectPermissionLevelNoAccess {
return nil, xlerrors.PermissionDenied
}
if !(permission.CanViewAnyIssue() || (permission.CanViewOwnIssue() && isOwnedOrAssigned)) {
return nil, xlerrors.PermissionDenied
}
return permission, nil
}
func (auth *Auth) CheckGinSession(c *gin.Context) {
ctx := context.WithValue(c.Request.Context(), ginCtxKey, c)
cookie, err := c.Cookie(authCookieName)
if err != nil {
c.Request = c.Request.WithContext(ctx)
return
}
session, err := auth.session.Find(c.Request.Context(), cookie)
if err != nil {
c.Request = c.Request.WithContext(ctx)
return
}
if time.Until(session.ExpiryTime) < time.Hour*167 {
session.ExpiryTime = time.Now().Add(time.Hour * 168)
_ = auth.session.Save(c.Request.Context(), *session)
c.SetCookie(authCookieName, session.ID, 3600*168, "/", "", false, true)
}
user, err := auth.users.Find(c.Request.Context(), session.UserID)
if err != nil {
c.Request = c.Request.WithContext(ctx)
return
}
ctx = context.WithValue(ctx, authCtxKey, user)
c.Request = c.Request.WithContext(ctx)
}
func (auth *Auth) EditUser(ctx context.Context, username string, setName *string, currentPassword *string, newPassword *string) (*models.User, error) {
loggedInUser := auth.UserFromContext(ctx)
if loggedInUser == nil {
return nil, xlerrors.PermissionDenied
}
user, err := auth.users.Find(ctx, username)
if err != nil {
return nil, err
}
if user.ID != loggedInUser.ID && !loggedInUser.Admin {
return nil, xlerrors.PermissionDenied
}
if newPassword != nil {
// Only require current password if it's given, or if the user is NOT an admin changing
// another user's password
if currentPassword != nil || !(loggedInUser.Admin && loggedInUser.ID != user.ID) {
if currentPassword == nil {
return nil, ErrMissingCurrentPassword
}
if !user.CheckPassword(*currentPassword) {
return nil, ErrWrongCurrentPassword
}
}
err = user.SetPassword(*newPassword)
if err != nil {
return nil, err
}
}
if setName != nil && *setName != "" {
user.Name = *setName
}
err = auth.users.Save(ctx, *user)
if err != nil {
return nil, err
}
return user, nil
}