mirror of
				https://github.com/usememos/memos.git
				synced 2025-11-04 04:16:06 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			399 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			399 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package v1
 | 
						|
 | 
						|
import (
 | 
						|
	"context"
 | 
						|
	"encoding/json"
 | 
						|
	"fmt"
 | 
						|
	"net/http"
 | 
						|
	"regexp"
 | 
						|
	"strings"
 | 
						|
	"time"
 | 
						|
 | 
						|
	"github.com/labstack/echo/v4"
 | 
						|
	"github.com/pkg/errors"
 | 
						|
	"golang.org/x/crypto/bcrypt"
 | 
						|
 | 
						|
	"github.com/usememos/memos/api/auth"
 | 
						|
	"github.com/usememos/memos/internal/util"
 | 
						|
	"github.com/usememos/memos/plugin/idp"
 | 
						|
	"github.com/usememos/memos/plugin/idp/oauth2"
 | 
						|
	storepb "github.com/usememos/memos/proto/gen/store"
 | 
						|
	"github.com/usememos/memos/store"
 | 
						|
)
 | 
						|
 | 
						|
var (
 | 
						|
	usernameMatcher = regexp.MustCompile("^[a-z0-9]([a-z0-9-]{1,30}[a-z0-9])$")
 | 
						|
)
 | 
						|
 | 
						|
type SignIn struct {
 | 
						|
	Username string `json:"username"`
 | 
						|
	Password string `json:"password"`
 | 
						|
	Remember bool   `json:"remember"`
 | 
						|
}
 | 
						|
 | 
						|
type SSOSignIn struct {
 | 
						|
	IdentityProviderID int32  `json:"identityProviderId"`
 | 
						|
	Code               string `json:"code"`
 | 
						|
	RedirectURI        string `json:"redirectUri"`
 | 
						|
}
 | 
						|
 | 
						|
type SignUp struct {
 | 
						|
	Username string `json:"username"`
 | 
						|
	Password string `json:"password"`
 | 
						|
}
 | 
						|
 | 
						|
func (s *APIV1Service) registerAuthRoutes(g *echo.Group) {
 | 
						|
	g.POST("/auth/signin", s.SignIn)
 | 
						|
	g.POST("/auth/signin/sso", s.SignInSSO)
 | 
						|
	g.POST("/auth/signout", s.SignOut)
 | 
						|
	g.POST("/auth/signup", s.SignUp)
 | 
						|
}
 | 
						|
 | 
						|
// SignIn godoc
 | 
						|
//
 | 
						|
//	@Summary	Sign-in to memos.
 | 
						|
//	@Tags		auth
 | 
						|
//	@Accept		json
 | 
						|
//	@Produce	json
 | 
						|
//	@Param		body	body		SignIn		true	"Sign-in object"
 | 
						|
//	@Success	200		{object}	store.User	"User information"
 | 
						|
//	@Failure	400		{object}	nil			"Malformatted signin request"
 | 
						|
//	@Failure	401		{object}	nil			"Password login is deactivated | Incorrect login credentials, please try again"
 | 
						|
//	@Failure	403		{object}	nil			"User has been archived with username %s"
 | 
						|
//	@Failure	500		{object}	nil			"Failed to find system setting | Failed to unmarshal system setting | Incorrect login credentials, please try again | Failed to generate tokens | Failed to create activity"
 | 
						|
//	@Router		/api/v1/auth/signin [POST]
 | 
						|
func (s *APIV1Service) SignIn(c echo.Context) error {
 | 
						|
	ctx := c.Request().Context()
 | 
						|
	signin := &SignIn{}
 | 
						|
 | 
						|
	disablePasswordLoginSystemSetting, err := s.Store.GetSystemSetting(ctx, &store.FindSystemSetting{
 | 
						|
		Name: SystemSettingDisablePasswordLoginName.String(),
 | 
						|
	})
 | 
						|
	if err != nil {
 | 
						|
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting").SetInternal(err)
 | 
						|
	}
 | 
						|
	if disablePasswordLoginSystemSetting != nil {
 | 
						|
		disablePasswordLogin := false
 | 
						|
		err = json.Unmarshal([]byte(disablePasswordLoginSystemSetting.Value), &disablePasswordLogin)
 | 
						|
		if err != nil {
 | 
						|
			return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting").SetInternal(err)
 | 
						|
		}
 | 
						|
		if disablePasswordLogin {
 | 
						|
			return echo.NewHTTPError(http.StatusUnauthorized, "Password login is deactivated")
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if err := json.NewDecoder(c.Request().Body).Decode(signin); err != nil {
 | 
						|
		return echo.NewHTTPError(http.StatusBadRequest, "Malformatted signin request").SetInternal(err)
 | 
						|
	}
 | 
						|
 | 
						|
	user, err := s.Store.GetUser(ctx, &store.FindUser{
 | 
						|
		Username: &signin.Username,
 | 
						|
	})
 | 
						|
	if err != nil {
 | 
						|
		return echo.NewHTTPError(http.StatusInternalServerError, "Incorrect login credentials, please try again")
 | 
						|
	}
 | 
						|
	if user == nil {
 | 
						|
		return echo.NewHTTPError(http.StatusUnauthorized, "Incorrect login credentials, please try again")
 | 
						|
	} else if user.RowStatus == store.Archived {
 | 
						|
		return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("User has been archived with username %s", signin.Username))
 | 
						|
	}
 | 
						|
 | 
						|
	// Compare the stored hashed password, with the hashed version of the password that was received.
 | 
						|
	if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(signin.Password)); err != nil {
 | 
						|
		// If the two passwords don't match, return a 401 status.
 | 
						|
		return echo.NewHTTPError(http.StatusUnauthorized, "Incorrect login credentials, please try again")
 | 
						|
	}
 | 
						|
 | 
						|
	var expireAt time.Time
 | 
						|
	// Set cookie expiration to 100 years to make it persistent.
 | 
						|
	cookieExp := time.Now().AddDate(100, 0, 0)
 | 
						|
	if !signin.Remember {
 | 
						|
		expireAt = time.Now().Add(auth.AccessTokenDuration)
 | 
						|
		cookieExp = time.Now().Add(auth.CookieExpDuration)
 | 
						|
	}
 | 
						|
 | 
						|
	accessToken, err := auth.GenerateAccessToken(user.Username, user.ID, expireAt, []byte(s.Secret))
 | 
						|
	if err != nil {
 | 
						|
		return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to generate tokens, err: %s", err)).SetInternal(err)
 | 
						|
	}
 | 
						|
	if err := s.UpsertAccessTokenToStore(ctx, user, accessToken); err != nil {
 | 
						|
		return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to upsert access token, err: %s", err)).SetInternal(err)
 | 
						|
	}
 | 
						|
	setTokenCookie(c, auth.AccessTokenCookieName, accessToken, cookieExp)
 | 
						|
	userMessage := convertUserFromStore(user)
 | 
						|
	return c.JSON(http.StatusOK, userMessage)
 | 
						|
}
 | 
						|
 | 
						|
// SignInSSO godoc
 | 
						|
//
 | 
						|
//	@Summary	Sign-in to memos using SSO.
 | 
						|
//	@Tags		auth
 | 
						|
//	@Accept		json
 | 
						|
//	@Produce	json
 | 
						|
//	@Param		body	body		SSOSignIn	true	"SSO sign-in object"
 | 
						|
//	@Success	200		{object}	store.User	"User information"
 | 
						|
//	@Failure	400		{object}	nil			"Malformatted signin request"
 | 
						|
//	@Failure	401		{object}	nil			"Access denied, identifier does not match the filter."
 | 
						|
//	@Failure	403		{object}	nil			"User has been archived with username {username}"
 | 
						|
//	@Failure	404		{object}	nil			"Identity provider not found"
 | 
						|
//	@Failure	500		{object}	nil			"Failed to find identity provider | Failed to create identity provider instance | Failed to exchange token | Failed to get user info | Failed to compile identifier filter | Incorrect login credentials, please try again | Failed to generate random password | Failed to generate password hash | Failed to create user | Failed to generate tokens | Failed to create activity"
 | 
						|
//	@Router		/api/v1/auth/signin/sso [POST]
 | 
						|
func (s *APIV1Service) SignInSSO(c echo.Context) error {
 | 
						|
	ctx := c.Request().Context()
 | 
						|
	signin := &SSOSignIn{}
 | 
						|
	if err := json.NewDecoder(c.Request().Body).Decode(signin); err != nil {
 | 
						|
		return echo.NewHTTPError(http.StatusBadRequest, "Malformatted signin request").SetInternal(err)
 | 
						|
	}
 | 
						|
 | 
						|
	identityProvider, err := s.Store.GetIdentityProvider(ctx, &store.FindIdentityProvider{
 | 
						|
		ID: &signin.IdentityProviderID,
 | 
						|
	})
 | 
						|
	if err != nil {
 | 
						|
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find identity provider").SetInternal(err)
 | 
						|
	}
 | 
						|
	if identityProvider == nil {
 | 
						|
		return echo.NewHTTPError(http.StatusNotFound, "Identity provider not found")
 | 
						|
	}
 | 
						|
 | 
						|
	var userInfo *idp.IdentityProviderUserInfo
 | 
						|
	if identityProvider.Type == store.IdentityProviderOAuth2Type {
 | 
						|
		oauth2IdentityProvider, err := oauth2.NewIdentityProvider(identityProvider.Config.OAuth2Config)
 | 
						|
		if err != nil {
 | 
						|
			return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create identity provider instance").SetInternal(err)
 | 
						|
		}
 | 
						|
		token, err := oauth2IdentityProvider.ExchangeToken(ctx, signin.RedirectURI, signin.Code)
 | 
						|
		if err != nil {
 | 
						|
			return echo.NewHTTPError(http.StatusInternalServerError, "Failed to exchange token").SetInternal(err)
 | 
						|
		}
 | 
						|
		userInfo, err = oauth2IdentityProvider.UserInfo(token)
 | 
						|
		if err != nil {
 | 
						|
			return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get user info").SetInternal(err)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	identifierFilter := identityProvider.IdentifierFilter
 | 
						|
	if identifierFilter != "" {
 | 
						|
		identifierFilterRegex, err := regexp.Compile(identifierFilter)
 | 
						|
		if err != nil {
 | 
						|
			return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compile identifier filter").SetInternal(err)
 | 
						|
		}
 | 
						|
		if !identifierFilterRegex.MatchString(userInfo.Identifier) {
 | 
						|
			return echo.NewHTTPError(http.StatusUnauthorized, "Access denied, identifier does not match the filter.").SetInternal(err)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	user, err := s.Store.GetUser(ctx, &store.FindUser{
 | 
						|
		Username: &userInfo.Identifier,
 | 
						|
	})
 | 
						|
	if err != nil {
 | 
						|
		return echo.NewHTTPError(http.StatusInternalServerError, "Incorrect login credentials, please try again")
 | 
						|
	}
 | 
						|
	if user == nil {
 | 
						|
		allowSignUpSetting, err := s.Store.GetSystemSetting(ctx, &store.FindSystemSetting{
 | 
						|
			Name: SystemSettingAllowSignUpName.String(),
 | 
						|
		})
 | 
						|
		if err != nil {
 | 
						|
			return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting").SetInternal(err)
 | 
						|
		}
 | 
						|
 | 
						|
		allowSignUpSettingValue := true
 | 
						|
		if allowSignUpSetting != nil {
 | 
						|
			err = json.Unmarshal([]byte(allowSignUpSetting.Value), &allowSignUpSettingValue)
 | 
						|
			if err != nil {
 | 
						|
				return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting allow signup").SetInternal(err)
 | 
						|
			}
 | 
						|
		}
 | 
						|
		if !allowSignUpSettingValue {
 | 
						|
			return echo.NewHTTPError(http.StatusUnauthorized, "signup is disabled").SetInternal(err)
 | 
						|
		}
 | 
						|
 | 
						|
		userCreate := &store.User{
 | 
						|
			Username: userInfo.Identifier,
 | 
						|
			// The new signup user should be normal user by default.
 | 
						|
			Role:     store.RoleUser,
 | 
						|
			Nickname: userInfo.DisplayName,
 | 
						|
			Email:    userInfo.Email,
 | 
						|
		}
 | 
						|
		password, err := util.RandomString(20)
 | 
						|
		if err != nil {
 | 
						|
			return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate random password").SetInternal(err)
 | 
						|
		}
 | 
						|
		passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
 | 
						|
		if err != nil {
 | 
						|
			return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate password hash").SetInternal(err)
 | 
						|
		}
 | 
						|
		userCreate.PasswordHash = string(passwordHash)
 | 
						|
		user, err = s.Store.CreateUser(ctx, userCreate)
 | 
						|
		if err != nil {
 | 
						|
			return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create user").SetInternal(err)
 | 
						|
		}
 | 
						|
	}
 | 
						|
	if user.RowStatus == store.Archived {
 | 
						|
		return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("User has been archived with username %s", userInfo.Identifier))
 | 
						|
	}
 | 
						|
 | 
						|
	accessToken, err := auth.GenerateAccessToken(user.Username, user.ID, time.Now().Add(auth.AccessTokenDuration), []byte(s.Secret))
 | 
						|
	if err != nil {
 | 
						|
		return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to generate tokens, err: %s", err)).SetInternal(err)
 | 
						|
	}
 | 
						|
	if err := s.UpsertAccessTokenToStore(ctx, user, accessToken); err != nil {
 | 
						|
		return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to upsert access token, err: %s", err)).SetInternal(err)
 | 
						|
	}
 | 
						|
	cookieExp := time.Now().Add(auth.CookieExpDuration)
 | 
						|
	setTokenCookie(c, auth.AccessTokenCookieName, accessToken, cookieExp)
 | 
						|
	userMessage := convertUserFromStore(user)
 | 
						|
	return c.JSON(http.StatusOK, userMessage)
 | 
						|
}
 | 
						|
 | 
						|
// SignOut godoc
 | 
						|
//
 | 
						|
//	@Summary	Sign-out from memos.
 | 
						|
//	@Tags		auth
 | 
						|
//	@Produce	json
 | 
						|
//	@Success	200	{boolean}	true	"Sign-out success"
 | 
						|
//	@Router		/api/v1/auth/signout [POST]
 | 
						|
func (s *APIV1Service) SignOut(c echo.Context) error {
 | 
						|
	accessToken := findAccessToken(c)
 | 
						|
	userID, _ := getUserIDFromAccessToken(accessToken, s.Secret)
 | 
						|
 | 
						|
	err := removeAccessTokenAndCookies(c, s.Store, userID, accessToken)
 | 
						|
	if err != nil {
 | 
						|
		return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to remove access token, err: %s", err)).SetInternal(err)
 | 
						|
	}
 | 
						|
 | 
						|
	return c.JSON(http.StatusOK, true)
 | 
						|
}
 | 
						|
 | 
						|
// SignUp godoc
 | 
						|
//
 | 
						|
//	@Summary	Sign-up to memos.
 | 
						|
//	@Tags		auth
 | 
						|
//	@Accept		json
 | 
						|
//	@Produce	json
 | 
						|
//	@Param		body	body		SignUp		true	"Sign-up object"
 | 
						|
//	@Success	200		{object}	store.User	"User information"
 | 
						|
//	@Failure	400		{object}	nil			"Malformatted signup request | Failed to find users"
 | 
						|
//	@Failure	401		{object}	nil			"signup is disabled"
 | 
						|
//	@Failure	403		{object}	nil			"Forbidden"
 | 
						|
//	@Failure	404		{object}	nil			"Not found"
 | 
						|
//	@Failure	500		{object}	nil			"Failed to find system setting | Failed to unmarshal system setting allow signup | Failed to generate password hash | Failed to create user | Failed to generate tokens | Failed to create activity"
 | 
						|
//	@Router		/api/v1/auth/signup [POST]
 | 
						|
func (s *APIV1Service) SignUp(c echo.Context) error {
 | 
						|
	ctx := c.Request().Context()
 | 
						|
	signup := &SignUp{}
 | 
						|
	if err := json.NewDecoder(c.Request().Body).Decode(signup); err != nil {
 | 
						|
		return echo.NewHTTPError(http.StatusBadRequest, "Malformatted signup request").SetInternal(err)
 | 
						|
	}
 | 
						|
 | 
						|
	hostUserType := store.RoleHost
 | 
						|
	existedHostUsers, err := s.Store.ListUsers(ctx, &store.FindUser{
 | 
						|
		Role: &hostUserType,
 | 
						|
	})
 | 
						|
	if err != nil {
 | 
						|
		return echo.NewHTTPError(http.StatusBadRequest, "Failed to find users").SetInternal(err)
 | 
						|
	}
 | 
						|
	if !usernameMatcher.MatchString(strings.ToLower(signup.Username)) {
 | 
						|
		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid username %s", signup.Username)).SetInternal(err)
 | 
						|
	}
 | 
						|
 | 
						|
	userCreate := &store.User{
 | 
						|
		Username: signup.Username,
 | 
						|
		// The new signup user should be normal user by default.
 | 
						|
		Role:     store.RoleUser,
 | 
						|
		Nickname: signup.Username,
 | 
						|
	}
 | 
						|
	if len(existedHostUsers) == 0 {
 | 
						|
		// Change the default role to host if there is no host user.
 | 
						|
		userCreate.Role = store.RoleHost
 | 
						|
	} else {
 | 
						|
		allowSignUpSetting, err := s.Store.GetSystemSetting(ctx, &store.FindSystemSetting{
 | 
						|
			Name: SystemSettingAllowSignUpName.String(),
 | 
						|
		})
 | 
						|
		if err != nil {
 | 
						|
			return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting").SetInternal(err)
 | 
						|
		}
 | 
						|
 | 
						|
		allowSignUpSettingValue := true
 | 
						|
		if allowSignUpSetting != nil {
 | 
						|
			err = json.Unmarshal([]byte(allowSignUpSetting.Value), &allowSignUpSettingValue)
 | 
						|
			if err != nil {
 | 
						|
				return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting allow signup").SetInternal(err)
 | 
						|
			}
 | 
						|
		}
 | 
						|
		if !allowSignUpSettingValue {
 | 
						|
			return echo.NewHTTPError(http.StatusUnauthorized, "signup is disabled").SetInternal(err)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	passwordHash, err := bcrypt.GenerateFromPassword([]byte(signup.Password), bcrypt.DefaultCost)
 | 
						|
	if err != nil {
 | 
						|
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate password hash").SetInternal(err)
 | 
						|
	}
 | 
						|
 | 
						|
	userCreate.PasswordHash = string(passwordHash)
 | 
						|
	user, err := s.Store.CreateUser(ctx, userCreate)
 | 
						|
	if err != nil {
 | 
						|
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create user").SetInternal(err)
 | 
						|
	}
 | 
						|
	accessToken, err := auth.GenerateAccessToken(user.Username, user.ID, time.Now().Add(auth.AccessTokenDuration), []byte(s.Secret))
 | 
						|
	if err != nil {
 | 
						|
		return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to generate tokens, err: %s", err)).SetInternal(err)
 | 
						|
	}
 | 
						|
	if err := s.UpsertAccessTokenToStore(ctx, user, accessToken); err != nil {
 | 
						|
		return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to upsert access token, err: %s", err)).SetInternal(err)
 | 
						|
	}
 | 
						|
	cookieExp := time.Now().Add(auth.CookieExpDuration)
 | 
						|
	setTokenCookie(c, auth.AccessTokenCookieName, accessToken, cookieExp)
 | 
						|
	userMessage := convertUserFromStore(user)
 | 
						|
	return c.JSON(http.StatusOK, userMessage)
 | 
						|
}
 | 
						|
 | 
						|
func (s *APIV1Service) UpsertAccessTokenToStore(ctx context.Context, user *store.User, accessToken string) error {
 | 
						|
	userAccessTokens, err := s.Store.GetUserAccessTokens(ctx, user.ID)
 | 
						|
	if err != nil {
 | 
						|
		return errors.Wrap(err, "failed to get user access tokens")
 | 
						|
	}
 | 
						|
	userAccessToken := storepb.AccessTokensUserSetting_AccessToken{
 | 
						|
		AccessToken: accessToken,
 | 
						|
		Description: "Account sign in",
 | 
						|
	}
 | 
						|
	userAccessTokens = append(userAccessTokens, &userAccessToken)
 | 
						|
	if _, err := s.Store.UpsertUserSettingV1(ctx, &storepb.UserSetting{
 | 
						|
		UserId: user.ID,
 | 
						|
		Key:    storepb.UserSettingKey_USER_SETTING_ACCESS_TOKENS,
 | 
						|
		Value: &storepb.UserSetting_AccessTokens{
 | 
						|
			AccessTokens: &storepb.AccessTokensUserSetting{
 | 
						|
				AccessTokens: userAccessTokens,
 | 
						|
			},
 | 
						|
		},
 | 
						|
	}); err != nil {
 | 
						|
		return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to upsert user setting, err: %s", err)).SetInternal(err)
 | 
						|
	}
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
// removeAccessTokenAndCookies removes the jwt token from the cookies.
 | 
						|
func removeAccessTokenAndCookies(c echo.Context, s *store.Store, userID int32, token string) error {
 | 
						|
	err := s.RemoveUserAccessToken(c.Request().Context(), userID, token)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	cookieExp := time.Now().Add(-1 * time.Hour)
 | 
						|
	setTokenCookie(c, auth.AccessTokenCookieName, "", cookieExp)
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
// setTokenCookie sets the token to the cookie.
 | 
						|
func setTokenCookie(c echo.Context, name, token string, expiration time.Time) {
 | 
						|
	cookie := new(http.Cookie)
 | 
						|
	cookie.Name = name
 | 
						|
	cookie.Value = token
 | 
						|
	cookie.Expires = expiration
 | 
						|
	cookie.Path = "/"
 | 
						|
	// Http-only helps mitigate the risk of client side script accessing the protected cookie.
 | 
						|
	cookie.HttpOnly = true
 | 
						|
	cookie.SameSite = http.SameSiteStrictMode
 | 
						|
	c.SetCookie(cookie)
 | 
						|
}
 |