Merge branch 'master' of ssh://github.com/crusttech/crust
This commit is contained in:
commit
91db6b37f7
@ -2,8 +2,10 @@ package rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/crusttech/crust/sam/rest/handlers"
|
||||
"github.com/crusttech/crust/sam/rest/request"
|
||||
"github.com/crusttech/crust/sam/service"
|
||||
"github.com/crusttech/crust/sam/types"
|
||||
"github.com/pkg/errors"
|
||||
"io"
|
||||
"time"
|
||||
@ -16,12 +18,10 @@ type (
|
||||
svc service.AttachmentService
|
||||
}
|
||||
|
||||
tempAttachmentPayload struct {
|
||||
ThisIsATempSolutionForAttachmentPayload bool
|
||||
|
||||
Name string
|
||||
ModTime time.Time
|
||||
Download bool
|
||||
file struct {
|
||||
*types.Attachment
|
||||
content io.ReadSeeker
|
||||
download bool
|
||||
}
|
||||
)
|
||||
|
||||
@ -30,43 +30,47 @@ func (Attachment) New(svc service.AttachmentService) *Attachment {
|
||||
}
|
||||
|
||||
func (ctrl *Attachment) Original(ctx context.Context, r *request.AttachmentOriginal) (interface{}, error) {
|
||||
rval := tempAttachmentPayload{Download: r.Download}
|
||||
return ctrl.get(r.AttachmentID, false, r.Download)
|
||||
|
||||
if att, err := ctrl.svc.FindByID(r.AttachmentID); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
rval.Name = att.Name
|
||||
rval.ModTime = att.CreatedAt
|
||||
}
|
||||
|
||||
return rval, nil
|
||||
}
|
||||
|
||||
func (ctrl *Attachment) Preview(ctx context.Context, r *request.AttachmentPreview) (interface{}, error) {
|
||||
return nil, errors.New("Not implemented: Attachment.preview")
|
||||
return ctrl.get(r.AttachmentID, true, false)
|
||||
}
|
||||
|
||||
func (ctrl Attachment) get(ID uint64, preview, download bool) (interface{}, error) {
|
||||
rval := tempAttachmentPayload{Download: download}
|
||||
func (ctrl Attachment) get(ID uint64, preview, download bool) (handlers.Downloadable, error) {
|
||||
rval := &file{download: download}
|
||||
|
||||
if att, err := ctrl.svc.FindByID(ID); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
// @todo update this to io.ReadSeeker when store's Open() func rval is updated
|
||||
var rs io.Reader
|
||||
|
||||
rval.Attachment = att
|
||||
if preview {
|
||||
rs, err = ctrl.svc.OpenPreview(att)
|
||||
rval.content, err = ctrl.svc.OpenPreview(att)
|
||||
} else {
|
||||
rs, err = ctrl.svc.OpenOriginal(att)
|
||||
rval.content, err = ctrl.svc.OpenOriginal(att)
|
||||
}
|
||||
|
||||
rval.Name = att.Name
|
||||
rval.ModTime = att.CreatedAt
|
||||
|
||||
// @todo do something with rs :)
|
||||
_ = rs
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return rval, nil
|
||||
}
|
||||
|
||||
func (f *file) Download() bool {
|
||||
return f.download
|
||||
}
|
||||
|
||||
func (f *file) Name() string {
|
||||
return f.Attachment.Name
|
||||
}
|
||||
|
||||
func (f *file) ModTime() time.Time {
|
||||
return f.Attachment.CreatedAt
|
||||
}
|
||||
|
||||
func (f *file) Content() io.ReadSeeker {
|
||||
return f.content
|
||||
}
|
||||
|
||||
@ -16,6 +16,11 @@ type (
|
||||
token auth.TokenEncoder
|
||||
}
|
||||
|
||||
authPayload struct {
|
||||
JWT string
|
||||
User *types.User `json:"user"`
|
||||
}
|
||||
|
||||
authUserBasics interface {
|
||||
ValidateCredentials(ctx context.Context, username, password string) (*types.User, error)
|
||||
Create(ctx context.Context, input *types.User) (user *types.User, err error)
|
||||
@ -46,11 +51,12 @@ func (ctrl *Auth) tokenize(user *types.User, err error) (interface{}, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return struct {
|
||||
JWT string
|
||||
User *types.User `json:"user"`
|
||||
}{
|
||||
return &authPayload{
|
||||
JWT: ctrl.token.Encode(user),
|
||||
User: user,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ap authPayload) Token() string {
|
||||
return ap.JWT
|
||||
}
|
||||
|
||||
64
sam/rest/handlers/attachment_custom.go
Normal file
64
sam/rest/handlers/attachment_custom.go
Normal file
@ -0,0 +1,64 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/crusttech/crust/sam/rest/request"
|
||||
"io"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
// HTTP API interface
|
||||
type AttachmentDownloadable struct {
|
||||
Original func(http.ResponseWriter, *http.Request)
|
||||
Preview func(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
|
||||
type Downloadable interface {
|
||||
Name() string
|
||||
Download() bool
|
||||
ModTime() time.Time
|
||||
Content() io.ReadSeeker
|
||||
}
|
||||
|
||||
func NewAttachmentDownloadable(ah AttachmentAPI) *Attachment {
|
||||
serve := func(f interface{}, err error, w http.ResponseWriter, r *http.Request) {
|
||||
if err != nil {
|
||||
switch true {
|
||||
case err.Error() == "crust.sam.repository.AttachmentNotFound":
|
||||
http.Error(w, "Attachment not found", 404)
|
||||
default:
|
||||
http.Error(w, err.Error(), 500)
|
||||
}
|
||||
} else if dl, ok := f.(Downloadable); ok {
|
||||
if dl.Download() {
|
||||
w.Header().Add("Content-Disposition", "attachment; filename="+url.QueryEscape(dl.Name()))
|
||||
}
|
||||
|
||||
http.ServeContent(w, r, dl.Name(), dl.ModTime(), dl.Content())
|
||||
} else {
|
||||
http.Error(w, "Got incompatible type from controller", 500)
|
||||
}
|
||||
}
|
||||
|
||||
return &Attachment{
|
||||
Original: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.NewAttachmentOriginal()
|
||||
params.Fill(r)
|
||||
|
||||
f, err := ah.Original(r.Context(), params)
|
||||
serve(f, err, w, r)
|
||||
},
|
||||
|
||||
Preview: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.NewAttachmentPreview()
|
||||
params.Fill(r)
|
||||
|
||||
f, err := ah.Preview(r.Context(), params)
|
||||
serve(f, err, w, r)
|
||||
},
|
||||
}
|
||||
}
|
||||
76
sam/rest/handlers/auth_custom.go
Normal file
76
sam/rest/handlers/auth_custom.go
Normal file
@ -0,0 +1,76 @@
|
||||
package handlers
|
||||
|
||||
/*
|
||||
Hello! This file is auto-generated from `docs/src/spec.json`.
|
||||
|
||||
For development:
|
||||
In order to update the generated files, edit this file under the location,
|
||||
add your struct fields, imports, API definitions and whatever you want, and:
|
||||
|
||||
1. run [spec](https://github.com/titpetric/spec) in the same folder,
|
||||
2. run `./_gen.php` in this folder.
|
||||
|
||||
You may edit `auth.go`, `auth.util.go` or `auth_test.go` to
|
||||
implement your API calls, helper functions and tests. The file `auth.go`
|
||||
is only generated the first time, and will not be overwritten if it exists.
|
||||
*/
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/titpetric/factory/resputil"
|
||||
|
||||
"github.com/crusttech/crust/sam/rest/request"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
authPayload interface {
|
||||
Token() string
|
||||
}
|
||||
)
|
||||
|
||||
// Initializies custom auth handler that attaches cookie info
|
||||
//
|
||||
// Cookie with JWT is added on successful login or user creation
|
||||
//
|
||||
func NewAuthCustom(ah AuthAPI, cookieExp int) *Auth {
|
||||
setCookie := func(w http.ResponseWriter, reqUrl *url.URL) func(payload interface{}, err error) (interface{}, error) {
|
||||
return func(payload interface{}, err error) (interface{}, error) {
|
||||
if ap, ok := payload.(authPayload); ok && err == nil {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "jwt",
|
||||
Value: ap.Token(),
|
||||
|
||||
HttpOnly: false, // we need this for attachments & ws!
|
||||
Secure: reqUrl.Scheme == "https",
|
||||
Path: "/",
|
||||
//Domain: "localhost",
|
||||
|
||||
// @todo read from the config file.
|
||||
Expires: time.Now().Add(time.Duration(cookieExp) * time.Minute),
|
||||
})
|
||||
}
|
||||
|
||||
return payload, err
|
||||
}
|
||||
}
|
||||
|
||||
return &Auth{
|
||||
Login: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.NewAuthLogin()
|
||||
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
|
||||
return setCookie(w, r.URL)(ah.Login(r.Context(), params))
|
||||
})
|
||||
},
|
||||
Create: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.NewAuthCreate()
|
||||
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
|
||||
return setCookie(w, r.URL)(ah.Create(r.Context(), params))
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -12,7 +12,7 @@ import (
|
||||
|
||||
func MountRoutes(jwtAuth types.TokenEncoder) func(chi.Router) {
|
||||
// Initialize services
|
||||
fs, err := store.New("/tmp/crust/messages")
|
||||
fs, err := store.New("var/store")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to initialize stor: %v", err)
|
||||
}
|
||||
@ -37,7 +37,14 @@ func MountRoutes(jwtAuth types.TokenEncoder) func(chi.Router) {
|
||||
|
||||
// Initialize handers & controllers.
|
||||
return func(r chi.Router) {
|
||||
handlers.NewAuth(Auth{}.New(userSvc, jwtAuth)).MountRoutes(r)
|
||||
// Cookie expiration in minutes
|
||||
// @todo pull this from auth/jwt config
|
||||
var cookieExp = 3600
|
||||
|
||||
handlers.NewAuthCustom(Auth{}.New(userSvc, jwtAuth), cookieExp).MountRoutes(r)
|
||||
|
||||
// @todo solve cookie issues (
|
||||
handlers.NewAttachmentDownloadable(attachment).MountRoutes(r)
|
||||
|
||||
// Protect all _private_ routes
|
||||
r.Group(func(r chi.Router) {
|
||||
@ -48,7 +55,6 @@ func MountRoutes(jwtAuth types.TokenEncoder) func(chi.Router) {
|
||||
handlers.NewOrganisation(organisation).MountRoutes(r)
|
||||
handlers.NewTeam(team).MountRoutes(r)
|
||||
handlers.NewUser(user).MountRoutes(r)
|
||||
handlers.NewAttachment(attachment).MountRoutes(r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@ import (
|
||||
"github.com/crusttech/crust/store"
|
||||
"github.com/titpetric/factory"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
@ -18,7 +19,7 @@ import (
|
||||
type (
|
||||
attachment struct {
|
||||
rpo attachmentRepository
|
||||
sto attachmentStore
|
||||
sto store.Store
|
||||
|
||||
config struct {
|
||||
url string
|
||||
@ -30,20 +31,19 @@ type (
|
||||
FindByID(id uint64) (*types.Attachment, error)
|
||||
Create(ctx context.Context, channelId uint64, name string, size int64, fh io.ReadSeeker) (*types.Attachment, error)
|
||||
LoadFromMessages(ctx context.Context, mm types.MessageSet) (err error)
|
||||
OpenOriginal(att *types.Attachment) (io.Reader, error)
|
||||
OpenPreview(att *types.Attachment) (io.Reader, error)
|
||||
OpenOriginal(att *types.Attachment) (io.ReadSeeker, error)
|
||||
OpenPreview(att *types.Attachment) (io.ReadSeeker, error)
|
||||
}
|
||||
|
||||
attachmentRepository interface {
|
||||
repository.Transactionable
|
||||
repository.Attachment
|
||||
}
|
||||
|
||||
attachmentStore store.Store
|
||||
)
|
||||
|
||||
func Attachment(store attachmentStore) *attachment {
|
||||
func Attachment(store store.Store) *attachment {
|
||||
svc := &attachment{}
|
||||
|
||||
svc.config.url = "/attachment/%d/%s"
|
||||
svc.config.previewUrl = "/attachment/%d/%s/preview"
|
||||
svc.rpo = repository.New()
|
||||
@ -56,13 +56,11 @@ func (svc attachment) FindByID(id uint64) (*types.Attachment, error) {
|
||||
return svc.rpo.FindAttachmentByID(id)
|
||||
}
|
||||
|
||||
func (svc attachment) OpenOriginal(att *types.Attachment) (io.Reader, error) {
|
||||
// @todo update this to io.ReadSeeker when store's Open() func rval is updated
|
||||
func (svc attachment) OpenOriginal(att *types.Attachment) (io.ReadSeeker, error) {
|
||||
return svc.sto.Open(att.Url)
|
||||
}
|
||||
|
||||
func (svc attachment) OpenPreview(att *types.Attachment) (io.Reader, error) {
|
||||
// @todo update this to io.ReadSeeker when store's Open() func rval is updated
|
||||
func (svc attachment) OpenPreview(att *types.Attachment) (io.ReadSeeker, error) {
|
||||
return svc.sto.Open(att.PreviewUrl)
|
||||
|
||||
}
|
||||
@ -70,7 +68,7 @@ func (svc attachment) OpenPreview(att *types.Attachment) (io.Reader, error) {
|
||||
func (svc attachment) LoadFromMessages(ctx context.Context, mm types.MessageSet) (err error) {
|
||||
var ids []uint64
|
||||
mm.Walk(func(m *types.Message) error {
|
||||
if m.Type == "attachment" {
|
||||
if m.Type == types.MessageTypeAttachment || m.Type == types.MessageTypeInlineImage {
|
||||
ids = append(ids, m.ID)
|
||||
}
|
||||
return nil
|
||||
@ -111,11 +109,16 @@ func (svc attachment) Create(ctx context.Context, channelId uint64, name string,
|
||||
// Extract extension but make sure path.Ext is not confused by any leading/trailing dots
|
||||
var ext = strings.Trim(path.Ext(strings.Trim(name, ".")), ".")
|
||||
|
||||
// @todo extract mimetype and update att.Mimetype
|
||||
if err := svc.extractMeta(att, fh); err != nil {
|
||||
// @todo logmeta extraction failure
|
||||
}
|
||||
|
||||
log.Printf("Processing uploaded file (name: %s, size: %d, mime: %s)", att.Name, att.Size, att.Mimetype)
|
||||
|
||||
if svc.sto != nil {
|
||||
att.Url = svc.sto.Original(att.ID, ext)
|
||||
if err = svc.sto.Save(att.Url, fh); err != nil {
|
||||
log.Print(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@ -123,6 +126,8 @@ func (svc attachment) Create(ctx context.Context, channelId uint64, name string,
|
||||
svc.makePreview(att, fh)
|
||||
}
|
||||
|
||||
log.Printf("File %s stored as %s", att.Name, att.Url)
|
||||
|
||||
return att, svc.rpo.BeginWith(ctx, func(r repository.Interfaces) (err error) {
|
||||
|
||||
if att, err = r.CreateAttachment(att); err != nil {
|
||||
@ -131,8 +136,13 @@ func (svc attachment) Create(ctx context.Context, channelId uint64, name string,
|
||||
|
||||
msg := &types.Message{
|
||||
Message: name,
|
||||
Type: "attachment",
|
||||
Type: types.MessageTypeAttachment,
|
||||
ChannelID: channelId,
|
||||
UserID: currentUserID,
|
||||
}
|
||||
|
||||
if strings.HasPrefix(att.Mimetype, "image/") {
|
||||
msg.Type = types.MessageTypeInlineImage
|
||||
}
|
||||
|
||||
// Create the first message, doing this directly with repository to circumvent
|
||||
@ -145,6 +155,8 @@ func (svc attachment) Create(ctx context.Context, channelId uint64, name string,
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("File %s (id: %d) attached to message (id: %d)", att.Name, att.ID, msg.ID)
|
||||
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
Message struct {
|
||||
ID uint64 `json:"id" db:"id"`
|
||||
Type string `json:"type" db:"type"`
|
||||
Type MessageType `json:"type" db:"type"`
|
||||
Message string `json:"message" db:"message"`
|
||||
UserID uint64 `json:"userId" db:"rel_user"`
|
||||
ChannelID uint64 `json:"channelId" db:"rel_channel"`
|
||||
@ -27,6 +28,15 @@ type (
|
||||
UntilMessageID uint64
|
||||
Limit uint
|
||||
}
|
||||
|
||||
MessageType string
|
||||
)
|
||||
|
||||
const (
|
||||
MessageTypeSimpleMessage MessageType = ""
|
||||
MessageTypeChannelEvent MessageType = "channelEvent"
|
||||
MessageTypeInlineImage MessageType = "inlineImage"
|
||||
MessageTypeAttachment MessageType = "attachment"
|
||||
)
|
||||
|
||||
func (mm MessageSet) Walk(w func(*Message) error) (err error) {
|
||||
@ -48,3 +58,41 @@ func (mm MessageSet) FindById(ID uint64) *Message {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mtype MessageType) String() string {
|
||||
return string(mtype)
|
||||
}
|
||||
|
||||
func (mtype MessageType) IsValid() bool {
|
||||
switch mtype {
|
||||
case MessageTypeSimpleMessage,
|
||||
MessageTypeChannelEvent,
|
||||
MessageTypeInlineImage,
|
||||
MessageTypeAttachment:
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
//func (mtype *MessageType) Scan(value interface{}) error {
|
||||
// switch value.(type) {
|
||||
// case nil:
|
||||
// *mtype = MessageTypeSimpleMessage
|
||||
// case []uint8:
|
||||
// *mtype = MessageType(string(value.([]uint8)))
|
||||
// if !mtype.IsValid() {
|
||||
// return errors.Errorf("Can not scan %v into MessageType", value)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return nil
|
||||
//}
|
||||
|
||||
func (mtype MessageType) Value() (driver.Value, error) {
|
||||
if mtype == MessageTypeSimpleMessage {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return mtype.String(), nil
|
||||
}
|
||||
|
||||
19
sam/websocket/outgoing/attachment.go
Normal file
19
sam/websocket/outgoing/attachment.go
Normal file
@ -0,0 +1,19 @@
|
||||
package outgoing
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
Attachment struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"uid"`
|
||||
Url string `json:"url"`
|
||||
PreviewUrl string `json:"prw"`
|
||||
Size int64 `json:"sze"`
|
||||
Mimetype string `json:"typ"`
|
||||
Name string `json:"nme"`
|
||||
CreatedAt time.Time `json:"cat,omitempty"`
|
||||
UpdatedAt *time.Time `json:"uat,omitempty"`
|
||||
}
|
||||
)
|
||||
@ -32,18 +32,6 @@ type (
|
||||
MessageDelete struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
Attachment struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"uid"`
|
||||
Url string `json:"url"`
|
||||
PreviewUrl string `json:"prw"`
|
||||
Size int64 `json:"sze"`
|
||||
Mimetype string `json:"typ"`
|
||||
Name string `json:"nme"`
|
||||
CreatedAt time.Time `json:"cat,omitempty"`
|
||||
UpdatedAt *time.Time `json:"uat,omitempty"`
|
||||
}
|
||||
)
|
||||
|
||||
func (p *Message) EncodeMessage() ([]byte, error) {
|
||||
|
||||
@ -10,7 +10,7 @@ func payloadFromMessage(msg *types.Message) *outgoing.Message {
|
||||
Message: msg.Message,
|
||||
ID: uint64toa(msg.ID),
|
||||
ChannelID: uint64toa(msg.ChannelID),
|
||||
Type: msg.Type,
|
||||
Type: string(msg.Type),
|
||||
UserID: uint64toa(msg.UserID),
|
||||
ReplyTo: uint64toa(msg.ReplyTo),
|
||||
|
||||
|
||||
@ -11,7 +11,7 @@ import (
|
||||
|
||||
func messageService() service.MessageService {
|
||||
// @todo refactor, optimize this
|
||||
store, _ := fstore.New("")
|
||||
store, _ := fstore.New("var/store")
|
||||
attSvc := service.Attachment(store)
|
||||
msgSvc := service.Message(attSvc)
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user