gtsocial-umbx

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit 742f985d5b0620ad14015f9a2df9940edc254bf4
parent dc338dc881ead40723f0540aac7fe894f58b174d
Author: Tobi Smethurst <31960611+tsmethurst@users.noreply.github.com>
Date:   Mon, 10 May 2021 16:29:05 +0200

Mediahandler (#21)

Media GET and media PUT handlers
Diffstat:
Minternal/api/client/auth/middleware.go | 3++-
Minternal/api/client/fileserver/servefile.go | 11++++++++++-
Minternal/api/client/media/media.go | 6++++++
Minternal/api/client/media/mediacreate.go | 10+++++-----
Ainternal/api/client/media/mediaget.go | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/api/client/media/mediaupdate.go | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Minternal/api/client/status/statuscreate.go | 3++-
Minternal/api/model/attachment.go | 16+++++++++++-----
Minternal/api/model/status.go | 2+-
Minternal/gtsmodel/account.go | 6+++---
Minternal/media/media.go | 5++++-
Minternal/message/mediaprocess.go | 137++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Minternal/message/processor.go | 9+++++++--
Minternal/oauth/server.go | 5++++-
Minternal/oauth/tokenstore.go | 27+++++++++++++++++++++------
15 files changed, 322 insertions(+), 56 deletions(-)

diff --git a/internal/api/client/auth/middleware.go b/internal/api/client/auth/middleware.go @@ -35,9 +35,10 @@ func (m *Module) OauthTokenMiddleware(c *gin.Context) { ti, err := m.server.ValidationBearerToken(c.Request) if err != nil { - l.Trace("no valid token presented: continuing with unauthenticated request") + l.Tracef("could not validate token: %s", err) return } + l.Trace("continuing with unauthenticated request") c.Set(oauth.SessionAuthorizedToken, ti) l.Tracef("set gin context %s to %+v", oauth.SessionAuthorizedToken, ti) diff --git a/internal/api/client/fileserver/servefile.go b/internal/api/client/fileserver/servefile.go @@ -78,7 +78,7 @@ func (m *FileServer) ServeFile(c *gin.Context) { return } - content, err := m.processor.MediaGet(authed, &model.GetContentRequestForm{ + content, err := m.processor.FileGet(authed, &model.GetContentRequestForm{ AccountID: accountID, MediaType: mediaType, MediaSize: mediaSize, @@ -90,5 +90,14 @@ func (m *FileServer) ServeFile(c *gin.Context) { return } + // TODO: do proper content negotiation here -- if the requester only accepts text/html we should try to serve them *something* + // This is mostly needed because when sharing a link to a gts-hosted file on something like mastodon, the masto servers will + // attempt to look up the content to provide a preview of the link, and they ask for text/html. + if c.NegotiateFormat(content.ContentType) == "" { + l.Debugf("couldn't negotiate content for Accept headers %+v: we have content type %s", c.Request.Header.Get("Accepted"), content.ContentType) + c.AbortWithStatus(http.StatusNotAcceptable) + return + } + c.DataFromReader(http.StatusOK, content.ContentLength, content.ContentType, bytes.NewReader(content.Content), nil) } diff --git a/internal/api/client/media/media.go b/internal/api/client/media/media.go @@ -33,6 +33,10 @@ import ( // BasePath is the base API path for making media requests const BasePath = "/api/v1/media" +// IDKey is the key for media attachment IDs +const IDKey = "id" +// BasePathWithID corresponds to a media attachment with the given ID +const BasePathWithID = BasePath + "/:" + IDKey // Module implements the ClientAPIModule interface for media type Module struct { @@ -53,6 +57,8 @@ func New(config *config.Config, processor message.Processor, log *logrus.Logger) // Route satisfies the RESTAPIModule interface func (m *Module) Route(s router.Router) error { s.AttachHandler(http.MethodPost, BasePath, m.MediaCreatePOSTHandler) + s.AttachHandler(http.MethodGet, BasePathWithID, m.MediaGETHandler) + s.AttachHandler(http.MethodPut, BasePathWithID, m.MediaPUTHandler) return nil } diff --git a/internal/api/client/media/mediacreate.go b/internal/api/client/media/mediacreate.go @@ -41,8 +41,8 @@ func (m *Module) MediaCreatePOSTHandler(c *gin.Context) { // extract the media create form from the request context l.Tracef("parsing request form: %s", c.Request.Form) - form := &model.AttachmentRequest{} - if err := c.ShouldBind(form); err != nil || form == nil { + var form model.AttachmentRequest + if err := c.ShouldBind(&form); err != nil { l.Debugf("could not parse form from request: %s", err) c.JSON(http.StatusBadRequest, gin.H{"error": "missing one or more required form values"}) return @@ -50,19 +50,19 @@ func (m *Module) MediaCreatePOSTHandler(c *gin.Context) { // Give the fields on the request form a first pass to make sure the request is superficially valid. l.Tracef("validating form %+v", form) - if err := validateCreateMedia(form, m.config.MediaConfig); err != nil { + if err := validateCreateMedia(&form, m.config.MediaConfig); err != nil { l.Debugf("error validating form: %s", err) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - mastoAttachment, err := m.processor.MediaCreate(authed, form) + mastoAttachment, err := m.processor.MediaCreate(authed, &form) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - c.JSON(http.StatusAccepted, mastoAttachment) + c.JSON(http.StatusOK, mastoAttachment) } func validateCreateMedia(form *model.AttachmentRequest, config *config.MediaConfig) error { diff --git a/internal/api/client/media/mediaget.go b/internal/api/client/media/mediaget.go @@ -0,0 +1,51 @@ +package media + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/oauth" +) + +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +// MediaGETHandler allows the owner of an attachment to get information about that attachment before it's used in a status. +func (m *Module) MediaGETHandler(c *gin.Context) { + l := m.log.WithField("func", "MediaGETHandler") + authed, err := oauth.Authed(c, true, true, true, true) + if err != nil { + l.Debugf("couldn't auth: %s", err) + c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) + return + } + + attachmentID := c.Param(IDKey) + if attachmentID == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "no attachment ID given in request"}) + return + } + + attachment, errWithCode := m.processor.MediaGet(authed, attachmentID) + if errWithCode != nil { + c.JSON(errWithCode.Code(),gin.H{"error": errWithCode.Safe()}) + return + } + + c.JSON(http.StatusOK, attachment) +} diff --git a/internal/api/client/media/mediaupdate.go b/internal/api/client/media/mediaupdate.go @@ -0,0 +1,87 @@ +package media + +import ( + "errors" + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/oauth" +) + +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +// MediaPUTHandler allows the owner of an attachment to update information about that attachment before it's used in a status. +func (m *Module) MediaPUTHandler(c *gin.Context) { + l := m.log.WithField("func", "MediaGETHandler") + authed, err := oauth.Authed(c, true, true, true, true) + if err != nil { + l.Debugf("couldn't auth: %s", err) + c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) + return + } + + attachmentID := c.Param(IDKey) + if attachmentID == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "no attachment ID given in request"}) + return + } + + // extract the media update form from the request context + l.Tracef("parsing request form: %s", c.Request.Form) + var form model.AttachmentUpdateRequest + if err := c.ShouldBind(&form); err != nil { + l.Debugf("could not parse form from request: %s", err) + c.JSON(http.StatusBadRequest, gin.H{"error": "missing one or more required form values"}) + return + } + + // Give the fields on the request form a first pass to make sure the request is superficially valid. + l.Tracef("validating form %+v", form) + if err := validateUpdateMedia(&form, m.config.MediaConfig); err != nil { + l.Debugf("error validating form: %s", err) + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + attachment, errWithCode := m.processor.MediaUpdate(authed, attachmentID, &form) + if errWithCode != nil { + c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + return + } + + c.JSON(http.StatusOK, attachment) +} + +func validateUpdateMedia(form *model.AttachmentUpdateRequest, config *config.MediaConfig) error { + + if form.Description != nil { + if len(*form.Description) < config.MinDescriptionChars || len(*form.Description) > config.MaxDescriptionChars { + return fmt.Errorf("image description length must be between %d and %d characters (inclusive), but provided image description was %d chars", config.MinDescriptionChars, config.MaxDescriptionChars, len(*form.Description)) + } + } + + if form.Focus == nil && form.Description == nil { + return errors.New("focus and description were both nil, there's nothing to update") + } + + return nil +} diff --git a/internal/api/client/status/statuscreate.go b/internal/api/client/status/statuscreate.go @@ -49,13 +49,14 @@ func (m *Module) StatusCreatePOSTHandler(c *gin.Context) { } // extract the status create form from the request context - l.Tracef("parsing request form: %s", c.Request.Form) + l.Debugf("parsing request form: %s", c.Request.Form) form := &model.AdvancedStatusCreateForm{} if err := c.ShouldBind(form); err != nil || form == nil { l.Debugf("could not parse form from request: %s", err) c.JSON(http.StatusBadRequest, gin.H{"error": "missing one or more required form values"}) return } + l.Debugf("handling status request form: %+v", form) // Give the fields on the request form a first pass to make sure the request is superficially valid. l.Tracef("validating form %+v", form) diff --git a/internal/api/model/attachment.go b/internal/api/model/attachment.go @@ -23,10 +23,16 @@ import "mime/multipart" // AttachmentRequest represents the form data parameters submitted by a client during a media upload request. // See: https://docs.joinmastodon.org/methods/statuses/media/ type AttachmentRequest struct { - File *multipart.FileHeader `form:"file"` - Thumbnail *multipart.FileHeader `form:"thumbnail"` - Description string `form:"description"` - Focus string `form:"focus"` + File *multipart.FileHeader `form:"file" binding:"required"` + Description string `form:"description" json:"description" xml:"description"` + Focus string `form:"focus" json:"focus" xml:"focus"` +} + +// AttachmentRequest represents the form data parameters submitted by a client during a media update/PUT request. +// See: https://docs.joinmastodon.org/methods/statuses/media/ +type AttachmentUpdateRequest struct { + Description *string `form:"description" json:"description" xml:"description"` + Focus *string `form:"focus" json:"focus" xml:"focus"` } // Attachment represents the object returned to a client after a successful media upload request. @@ -57,7 +63,7 @@ type Attachment struct { // See https://docs.joinmastodon.org/methods/statuses/media/#focal-points points for more. Meta MediaMeta `json:"meta,omitempty"` // Alternate text that describes what is in the media attachment, to be used for the visually impaired or when media attachments do not load. - Description string `json:"description,omitempty"` + Description string `json:"description"` // A hash computed by the BlurHash algorithm, for generating colorful preview thumbnails when media has not been downloaded yet. // See https://github.com/woltapp/blurhash Blurhash string `json:"blurhash,omitempty"` diff --git a/internal/api/model/status.go b/internal/api/model/status.go @@ -88,7 +88,7 @@ type StatusCreateRequest struct { // Text content of the status. If media_ids is provided, this becomes optional. Attaching a poll is optional while status is provided. Status string `form:"status"` // Array of Attachment ids to be attached as media. If provided, status becomes optional, and poll cannot be used. - MediaIDs []string `form:"media_ids"` + MediaIDs []string `form:"media_ids" json:"media_ids" xml:"media_ids"` // Poll to include with this status. Poll *PollRequest `form:"poll"` // ID of the status being replied to, if status is a reply diff --git a/internal/gtsmodel/account.go b/internal/gtsmodel/account.go @@ -76,15 +76,15 @@ type Account struct { */ // Does this account need an approval for new followers? - Locked bool + Locked bool `pg:",default:true"` // Should this account be shown in the instance's profile directory? Discoverable bool // Default post privacy for this account Privacy Visibility // Set posts from this account to sensitive by default? - Sensitive bool + Sensitive bool `pg:",default:false"` // What language does this account post in? - Language string + Language string `pg:",default:en"` /* ACTIVITYPUB THINGS diff --git a/internal/media/media.go b/internal/media/media.go @@ -410,22 +410,25 @@ func (mh *mediaHandler) processHeaderOrAvi(imageBytes []byte, contentType string var clean []byte var err error + var original *imageAndMeta switch contentType { case MIMEJpeg: if clean, err = purgeExif(imageBytes); err != nil { return nil, fmt.Errorf("error cleaning exif data: %s", err) } + original, err = deriveImage(clean, contentType) case MIMEPng: if clean, err = purgeExif(imageBytes); err != nil { return nil, fmt.Errorf("error cleaning exif data: %s", err) } + original, err = deriveImage(clean, contentType) case MIMEGif: clean = imageBytes + original, err = deriveGif(clean, contentType) default: return nil, errors.New("media type unrecognized") } - original, err := deriveImage(clean, contentType) if err != nil { return nil, fmt.Errorf("error parsing image: %s", err) } diff --git a/internal/message/mediaprocess.go b/internal/message/mediaprocess.go @@ -9,6 +9,7 @@ import ( "strings" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/media" "github.com/superseriousbusiness/gotosocial/internal/oauth" @@ -17,6 +18,8 @@ import ( func (p *processor) MediaCreate(authed *oauth.Auth, form *apimodel.AttachmentRequest) (*apimodel.Attachment, error) { // First check this user/account is permitted to create media // There's no point continuing otherwise. + // + // TODO: move this check to the oauth.Authed function and do it for all accounts if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() { return nil, errors.New("not authorized to post new media") } @@ -49,34 +52,9 @@ func (p *processor) MediaCreate(authed *oauth.Auth, form *apimodel.AttachmentReq attachment.Description = form.Description // now parse the focus parameter - // TODO: tidy this up into a separate function and just return an error so all the c.JSON and return calls are obviated - var focusx, focusy float32 - if form.Focus != "" { - spl := strings.Split(form.Focus, ",") - if len(spl) != 2 { - return nil, fmt.Errorf("improperly formatted focus %s", form.Focus) - } - xStr := spl[0] - yStr := spl[1] - if xStr == "" || yStr == "" { - return nil, fmt.Errorf("improperly formatted focus %s", form.Focus) - } - fx, err := strconv.ParseFloat(xStr, 32) - if err != nil { - return nil, fmt.Errorf("improperly formatted focus %s: %s", form.Focus, err) - } - if fx > 1 || fx < -1 { - return nil, fmt.Errorf("improperly formatted focus %s", form.Focus) - } - focusx = float32(fx) - fy, err := strconv.ParseFloat(yStr, 32) - if err != nil { - return nil, fmt.Errorf("improperly formatted focus %s: %s", form.Focus, err) - } - if fy > 1 || fy < -1 { - return nil, fmt.Errorf("improperly formatted focus %s", form.Focus) - } - focusy = float32(fy) + focusx, focusy, err := parseFocus(form.Focus) + if err != nil { + return nil, err } attachment.FileMeta.Focus.X = focusx attachment.FileMeta.Focus.Y = focusy @@ -96,7 +74,70 @@ func (p *processor) MediaCreate(authed *oauth.Auth, form *apimodel.AttachmentReq return &mastoAttachment, nil } -func (p *processor) MediaGet(authed *oauth.Auth, form *apimodel.GetContentRequestForm) (*apimodel.Content, error) { +func (p *processor) MediaGet(authed *oauth.Auth, mediaAttachmentID string) (*apimodel.Attachment, ErrorWithCode) { + attachment := &gtsmodel.MediaAttachment{} + if err := p.db.GetByID(mediaAttachmentID, attachment); err != nil { + if _, ok := err.(db.ErrNoEntries); ok { + // attachment doesn't exist + return nil, NewErrorNotFound(errors.New("attachment doesn't exist in the db")) + } + return nil, NewErrorNotFound(fmt.Errorf("db error getting attachment: %s", err)) + } + + if attachment.AccountID != authed.Account.ID { + return nil, NewErrorNotFound(errors.New("attachment not owned by requesting account")) + } + + a, err := p.tc.AttachmentToMasto(attachment) + if err != nil { + return nil, NewErrorNotFound(fmt.Errorf("error converting attachment: %s", err)) + } + + return &a, nil +} + +func (p *processor) MediaUpdate(authed *oauth.Auth, mediaAttachmentID string, form *apimodel.AttachmentUpdateRequest) (*apimodel.Attachment, ErrorWithCode) { + attachment := &gtsmodel.MediaAttachment{} + if err := p.db.GetByID(mediaAttachmentID, attachment); err != nil { + if _, ok := err.(db.ErrNoEntries); ok { + // attachment doesn't exist + return nil, NewErrorNotFound(errors.New("attachment doesn't exist in the db")) + } + return nil, NewErrorNotFound(fmt.Errorf("db error getting attachment: %s", err)) + } + + if attachment.AccountID != authed.Account.ID { + return nil, NewErrorNotFound(errors.New("attachment not owned by requesting account")) + } + + if form.Description != nil { + attachment.Description = *form.Description + if err := p.db.UpdateByID(mediaAttachmentID, attachment); err != nil { + return nil, NewErrorInternalError(fmt.Errorf("database error updating description: %s", err)) + } + } + + if form.Focus != nil { + focusx, focusy, err := parseFocus(*form.Focus) + if err != nil { + return nil, NewErrorBadRequest(err) + } + attachment.FileMeta.Focus.X = focusx + attachment.FileMeta.Focus.Y = focusy + if err := p.db.UpdateByID(mediaAttachmentID, attachment); err != nil { + return nil, NewErrorInternalError(fmt.Errorf("database error updating focus: %s", err)) + } + } + + a, err := p.tc.AttachmentToMasto(attachment) + if err != nil { + return nil, NewErrorNotFound(fmt.Errorf("error converting attachment: %s", err)) + } + + return &a, nil +} + +func (p *processor) FileGet(authed *oauth.Auth, form *apimodel.GetContentRequestForm) (*apimodel.Content, error) { // parse the form fields mediaSize, err := media.ParseMediaSize(form.MediaSize) if err != nil { @@ -186,3 +227,41 @@ func (p *processor) MediaGet(authed *oauth.Auth, form *apimodel.GetContentReques content.Content = bytes return content, nil } + +func parseFocus(focus string) (focusx, focusy float32, err error) { + if focus == "" { + return + } + spl := strings.Split(focus, ",") + if len(spl) != 2 { + err = fmt.Errorf("improperly formatted focus %s", focus) + return + } + xStr := spl[0] + yStr := spl[1] + if xStr == "" || yStr == "" { + err = fmt.Errorf("improperly formatted focus %s", focus) + return + } + fx, err := strconv.ParseFloat(xStr, 32) + if err != nil { + err = fmt.Errorf("improperly formatted focus %s: %s", focus, err) + return + } + if fx > 1 || fx < -1 { + err = fmt.Errorf("improperly formatted focus %s", focus) + return + } + focusx = float32(fx) + fy, err := strconv.ParseFloat(yStr, 32) + if err != nil { + err = fmt.Errorf("improperly formatted focus %s: %s", focus, err) + return + } + if fy > 1 || fy < -1 { + err = fmt.Errorf("improperly formatted focus %s", focus) + return + } + focusy = float32(fy) + return +} diff --git a/internal/message/processor.go b/internal/message/processor.go @@ -74,13 +74,18 @@ type Processor interface { // AppCreate processes the creation of a new API application AppCreate(authed *oauth.Auth, form *apimodel.ApplicationCreateRequest) (*apimodel.Application, error) + // FileGet handles the fetching of a media attachment file via the fileserver. + FileGet(authed *oauth.Auth, form *apimodel.GetContentRequestForm) (*apimodel.Content, error) + // InstanceGet retrieves instance information for serving at api/v1/instance InstanceGet(domain string) (*apimodel.Instance, ErrorWithCode) // MediaCreate handles the creation of a media attachment, using the given form. MediaCreate(authed *oauth.Auth, form *apimodel.AttachmentRequest) (*apimodel.Attachment, error) - // MediaGet handles the fetching of a media attachment, using the given request form. - MediaGet(authed *oauth.Auth, form *apimodel.GetContentRequestForm) (*apimodel.Content, error) + // MediaGet handles the GET of a media attachment with the given ID + MediaGet(authed *oauth.Auth, attachmentID string) (*apimodel.Attachment, ErrorWithCode) + // MediaUpdate handles the PUT of a media attachment with the given ID and form + MediaUpdate(authed *oauth.Auth, attachmentID string, form *apimodel.AttachmentUpdateRequest) (*apimodel.Attachment, ErrorWithCode) // StatusCreate processes the given form to create a new status, returning the api model representation of that status if it's OK. StatusCreate(authed *oauth.Auth, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, error) diff --git a/internal/oauth/server.go b/internal/oauth/server.go @@ -72,7 +72,10 @@ func New(database db.DB, log *logrus.Logger) Server { manager := manage.NewDefaultManager() manager.MapTokenStorage(ts) manager.MapClientStorage(cs) - manager.SetAuthorizeCodeTokenCfg(manage.DefaultAuthorizeCodeTokenCfg) + manager.SetAuthorizeCodeTokenCfg(&manage.Config{ + AccessTokenExp: 0, // access tokens don't expire -- they must be revoked + IsGenerateRefresh: false, // don't use refresh tokens + }) sc := &server.Config{ TokenType: "Bearer", // Must follow the spec. diff --git a/internal/oauth/tokenstore.go b/internal/oauth/tokenstore.go @@ -202,17 +202,17 @@ func TokenToPGToken(tkn *models.Token) *Token { // going to cause all sorts of interesting problems. So check first to make sure that the ExpiresIn is not equal // to the zero value of a time.Duration, which is 0s. If it *is* empty/nil, just leave the ExpiresAt at nil as well. - var cea time.Time + cea := time.Time{} if tkn.CodeExpiresIn != 0*time.Second { cea = now.Add(tkn.CodeExpiresIn) } - var aea time.Time + aea := time.Time{} if tkn.AccessExpiresIn != 0*time.Second { aea = now.Add(tkn.AccessExpiresIn) } - var rea time.Time + rea := time.Time{} if tkn.RefreshExpiresIn != 0*time.Second { rea = now.Add(tkn.RefreshExpiresIn) } @@ -240,6 +240,21 @@ func TokenToPGToken(tkn *models.Token) *Token { func TokenToOauthToken(pgt *Token) *models.Token { now := time.Now() + var codeExpiresIn time.Duration + if !pgt.CodeExpiresAt.IsZero() { + codeExpiresIn = pgt.CodeExpiresAt.Sub(now) + } + + var accessExpiresIn time.Duration + if !pgt.AccessExpiresAt.IsZero() { + accessExpiresIn = pgt.AccessExpiresAt.Sub(now) + } + + var refreshExpiresIn time.Duration + if !pgt.RefreshExpiresAt.IsZero() { + refreshExpiresIn = pgt.RefreshExpiresAt.Sub(now) + } + return &models.Token{ ClientID: pgt.ClientID, UserID: pgt.UserID, @@ -249,12 +264,12 @@ func TokenToOauthToken(pgt *Token) *models.Token { CodeChallenge: pgt.CodeChallenge, CodeChallengeMethod: pgt.CodeChallengeMethod, CodeCreateAt: pgt.CodeCreateAt, - CodeExpiresIn: pgt.CodeExpiresAt.Sub(now), + CodeExpiresIn: codeExpiresIn, Access: pgt.Access, AccessCreateAt: pgt.AccessCreateAt, - AccessExpiresIn: pgt.AccessExpiresAt.Sub(now), + AccessExpiresIn: accessExpiresIn, Refresh: pgt.Refresh, RefreshCreateAt: pgt.RefreshCreateAt, - RefreshExpiresIn: pgt.RefreshExpiresAt.Sub(now), + RefreshExpiresIn: refreshExpiresIn, } }