Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions server/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ __debug_bin*
config/.env
config/museum.yaml
main
museum
credentials
84 changes: 82 additions & 2 deletions server/cmd/museum/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import (
"github.com/ente-io/museum/pkg/controller/family"
"github.com/ente-io/museum/pkg/controller/lock"
remoteStoreCtrl "github.com/ente-io/museum/pkg/controller/remotestore"
socialcontroller "github.com/ente-io/museum/pkg/controller/social"
"github.com/ente-io/museum/pkg/controller/storagebonus"
"github.com/ente-io/museum/pkg/controller/user"
userEntityCtrl "github.com/ente-io/museum/pkg/controller/userentity"
Expand All @@ -61,6 +62,7 @@ import (
fileDataRepo "github.com/ente-io/museum/pkg/repo/filedata"
"github.com/ente-io/museum/pkg/repo/passkey"
"github.com/ente-io/museum/pkg/repo/remotestore"
socialrepo "github.com/ente-io/museum/pkg/repo/social"
storageBonusRepo "github.com/ente-io/museum/pkg/repo/storagebonus"
userEntityRepo "github.com/ente-io/museum/pkg/repo/userentity"
"github.com/ente-io/museum/pkg/utils/billing"
Expand Down Expand Up @@ -263,6 +265,25 @@ func main() {
}

accessCtrl := access.NewAccessController(collectionRepo, fileRepo)
commentsRepo := &socialrepo.CommentsRepository{DB: db}
reactionsRepo := &socialrepo.ReactionsRepository{DB: db}
anonUsersRepo := &socialrepo.AnonUsersRepository{DB: db}
commentsController := &socialcontroller.CommentsController{
Repo: commentsRepo,
AccessCtrl: accessCtrl,
}
reactionsController := &socialcontroller.ReactionsController{
Repo: reactionsRepo,
CommentsRepo: commentsRepo,
AccessCtrl: accessCtrl,
}
socialController := &socialcontroller.Controller{
CommentsRepo: commentsRepo,
ReactionsRepo: reactionsRepo,
CollectionRepo: collectionRepo,
AccessCtrl: accessCtrl,
AnonUsersRepo: anonUsersRepo,
}
fileDataCtrl := filedata.New(fileDataRepo, accessCtrl, objectCleanupController, s3Config, fileRepo, collectionRepo)

fileController := &controller.FileController{
Expand Down Expand Up @@ -317,6 +338,26 @@ func main() {
UserRepo: userRepo,
JwtSecret: jwtSecretBytes,
}
publicCommentsCtrl := &publicCtrl.CommentsController{
CommentCtrl: commentsController,
CommentsRepo: commentsRepo,
ReactionsRepo: reactionsRepo,
UserRepo: userRepo,
UserAuthRepo: userAuthRepo,
AnonUsersRepo: anonUsersRepo,
JwtSecret: jwtSecretBytes,
}
publicReactionsCtrl := &publicCtrl.ReactionsController{
ReactionCtrl: reactionsController,
ReactionsRepo: reactionsRepo,
AnonUsersRepo: anonUsersRepo,
UserAuthRepo: userAuthRepo,
JwtSecret: jwtSecretBytes,
}
anonIdentityCtrl := &publicCtrl.AnonIdentityController{
JwtSecret: jwtSecretBytes,
AnonUsersRepo: anonUsersRepo,
}

collectionController := &collections.CollectionController{
CollectionRepo: collectionRepo,
Expand All @@ -330,6 +371,8 @@ func main() {
QueueRepo: queueRepo,
TaskRepo: taskLockingRepo,
CollectionActionsRepo: collectionActionRepo,
CommentsRepo: commentsRepo,
ReactionsRepo: reactionsRepo,
}

// Pending actions' controller/handler
Expand Down Expand Up @@ -383,10 +426,12 @@ func main() {
CollectionLinkRepo: collectionLinkRepo,
PublicCollectionCtrl: collectionLinkCtrl,
CollectionRepo: collectionRepo,
AnonUsersRepo: anonUsersRepo,
Cache: accessTokenCache,
BillingCtrl: billingController,
DiscordController: discordController,
RemoteStoreRepo: remoteStoreRepository,
AnonIdentitySecret: jwtSecretBytes,
}
fileLinkMiddleware := &middleware.FileLinkMiddleware{
FileLinkRepo: fileLinkRepo,
Expand Down Expand Up @@ -438,7 +483,11 @@ func main() {
familiesJwtAuthAPI.Use(rateLimiter.GlobalRateLimiter(), authMiddleware.TokenAuthMiddleware(jwt.FAMILIES.Ptr()), rateLimiter.APIRateLimitForUserMiddleware(urlSanitizer))

publicCollectionAPI := server.Group("/public-collection")
publicCollectionAPI.Use(rateLimiter.GlobalRateLimiter(), collectionLinkMiddleware.Authenticate(urlSanitizer))
publicCollectionAPI.Use(
rateLimiter.GlobalRateLimiter(),
collectionLinkMiddleware.Authenticate(urlSanitizer),
rateLimiter.APIRateLimitMiddleware(urlSanitizer),
)
fileLinkApi := server.Group("/file-link")
fileLinkApi.Use(rateLimiter.GlobalRateLimiter(), fileLinkMiddleware.Authenticate(urlSanitizer))

Expand Down Expand Up @@ -513,6 +562,27 @@ func main() {
privateAPI.GET("/trash/v2/diff", trashHandler.GetDiffV2)
privateAPI.POST("/trash/delete", trashHandler.Delete)
privateAPI.POST("/trash/empty", trashHandler.Empty)
commentsHandler := &api.CommentsHandler{Controller: commentsController}
reactionsHandler := &api.ReactionsHandler{Controller: reactionsController}
socialHandler := &api.SocialHandler{Controller: socialController}
publicSocialHandler := &api.PublicCommentsHandler{
CommentsCtrl: publicCommentsCtrl,
ReactionsCtrl: publicReactionsCtrl,
AnonIdentityCtrl: anonIdentityCtrl,
}
privateAPI.GET("/comments/diff", commentsHandler.Diff)
privateAPI.POST("/comments", commentsHandler.Create)
privateAPI.PUT("/comments/:commentID", commentsHandler.Update)
privateAPI.DELETE("/comments/:commentID", commentsHandler.Delete)

privateAPI.GET("/reactions/diff", reactionsHandler.Diff)
privateAPI.PUT("/reactions", reactionsHandler.Upsert)
privateAPI.DELETE("/reactions/:reactionID", reactionsHandler.Delete)

privateAPI.GET("/social/diff", socialHandler.UnifiedDiff)
privateAPI.GET("/social/anon-profiles", socialHandler.AnonProfiles)
privateAPI.GET("/comments-reactions/counts", socialHandler.Counts)
privateAPI.GET("/comments-reactions/updated-at", socialHandler.LatestUpdates)

emergencyCtrl := &emergency.Controller{
Repo: &emergencyRepo.Repository{DB: db},
Expand Down Expand Up @@ -617,7 +687,6 @@ func main() {
FileDataCtrl: fileDataCtrl,
StorageBonusController: storageBonusCtrl,
}

fileLinkApi.GET("/info", fileHandler.LinkInfo)
fileLinkApi.GET("/pass-info", fileHandler.PasswordInfo)
fileLinkApi.GET("/thumbnail", fileHandler.LinkThumbnail)
Expand All @@ -634,6 +703,17 @@ func main() {
publicCollectionAPI.GET("/multipart-upload-urls", publicCollectionHandler.GetMultipartUploadURLs)
publicCollectionAPI.POST("/file", publicCollectionHandler.CreateFile)
publicCollectionAPI.POST("/verify-password", publicCollectionHandler.VerifyPassword)
publicCollectionAPI.GET("/social/diff", publicSocialHandler.SocialDiff)
publicCollectionAPI.GET("/comments/diff", publicSocialHandler.CommentDiff)
publicCollectionAPI.POST("/comments", publicSocialHandler.CreateComment)
publicCollectionAPI.PUT("/comments/:commentID", publicSocialHandler.UpdateComment)
publicCollectionAPI.DELETE("/comments/:commentID", publicSocialHandler.DeleteComment)
publicCollectionAPI.GET("/reactions/diff", publicSocialHandler.ReactionDiff)
publicCollectionAPI.POST("/reactions", publicSocialHandler.CreateReaction)
publicCollectionAPI.DELETE("/reactions/:reactionID", publicSocialHandler.DeleteReaction)
publicCollectionAPI.GET("/participants/masked-emails", publicSocialHandler.Participants)
publicCollectionAPI.GET("/anon-profiles", publicSocialHandler.AnonProfiles)
publicCollectionAPI.POST("/anon-identity", publicSocialHandler.CreateAnonIdentity)

castAPI := server.Group("/cast")

Expand Down
62 changes: 62 additions & 0 deletions server/ente/anonymous_identity.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package ente

import (
"fmt"
"time"

"github.com/golang-jwt/jwt/v4"
)

const anonIdentityTokenType = "anon-identity"

// AnonymousIdentityClaim represents the JWT issued for anonymous commenters.
type AnonymousIdentityClaim struct {
Typ string `json:"typ"`
jwt.RegisteredClaims
}

// NewAnonymousIdentityToken signs a token for the provided anonUserID.
func NewAnonymousIdentityToken(secret []byte, anonUserID string) (string, int64, error) {
issuedAt := time.Now()
expiry := issuedAt.AddDate(1, 0, 0) // 1 year validity
claim := AnonymousIdentityClaim{
Typ: anonIdentityTokenType,
RegisteredClaims: jwt.RegisteredClaims{
Subject: anonUserID,
Issuer: "museum",
IssuedAt: jwt.NewNumericDate(issuedAt),
ExpiresAt: jwt.NewNumericDate(expiry),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claim)
tokenString, err := token.SignedString(secret)
if err != nil {
return "", 0, err
}
return tokenString, MicrosecondsFromTime(expiry), nil
}

// ParseAnonymousIdentityToken validates the token and returns the parsed claim.
func ParseAnonymousIdentityToken(secret []byte, tokenString string) (*AnonymousIdentityClaim, error) {
token, err := jwt.ParseWithClaims(tokenString, &AnonymousIdentityClaim{}, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
}
return secret, nil
})
if err != nil {
return nil, err
}
claim, ok := token.Claims.(*AnonymousIdentityClaim)
if !ok || !token.Valid || claim.Typ != anonIdentityTokenType {
return nil, fmt.Errorf("invalid anonymous identity token")
}
if claim.Subject == "" {
return nil, fmt.Errorf("missing subject in anonymous identity token")
}
return claim, nil
}

func MicrosecondsFromTime(t time.Time) int64 {
return t.UnixNano() / 1000
}
27 changes: 27 additions & 0 deletions server/ente/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,24 @@ var ErrPublicCollectDisabled = ApiError{
HttpStatusCode: http.StatusMethodNotAllowed,
}

var ErrPublicCommentDisabled = ApiError{
Code: PublicCommentDisabled,
Message: "User has not enabled public comments for this url",
HttpStatusCode: http.StatusMethodNotAllowed,
}

var ErrPublicCommentTooLong = ApiError{
Code: PublicCommentTooLong,
Message: "Comments are limited to 280 characters",
HttpStatusCode: http.StatusBadRequest,
}

var ErrAnonNameTooLong = ApiError{
Code: AnonNameTooLong,
Message: "Anonymous names are limited to 50 characters",
HttpStatusCode: http.StatusBadRequest,
}

var ErrNotFoundError = ApiError{
Code: NotFoundError,
Message: "",
Expand Down Expand Up @@ -243,6 +261,15 @@ const (
// PublicCollectDisabled error code indicates that the user has not enabled public collect
PublicCollectDisabled ErrorCode = "PUBLIC_COLLECT_DISABLED"

// PublicCommentDisabled error code indicates that the user has not enabled public comments
PublicCommentDisabled ErrorCode = "PUBLIC_COMMENT_DISABLED"

// PublicCommentTooLong indicates that comment text exceeded allowed limit
PublicCommentTooLong ErrorCode = "PUBLIC_COMMENT_TOO_LONG"

// AnonNameTooLong indicates that anonymous display name exceeded allowed limit
AnonNameTooLong ErrorCode = "ANON_NAME_TOO_LONG"

// CollectionNotEmpty is thrown when user attempts to delete a collection but keep files but all files from that
// collections have been moved yet.
CollectionNotEmpty ErrorCode = "COLLECTION_NOT_EMPTY"
Expand Down
18 changes: 12 additions & 6 deletions server/ente/public_collection.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
type CreatePublicAccessTokenRequest struct {
CollectionID int64 `json:"collectionID" binding:"required"`
EnableCollect bool `json:"enableCollect"`
EnableComment bool `json:"enableComment"`
// defaults to true
EnableJoin *bool `json:"enableJoin"`
ValidTill int64 `json:"validTill"`
Expand All @@ -28,14 +29,15 @@ type UpdatePublicAccessTokenRequest struct {
OpsLimit *int64 `json:"opsLimit"`
EnableDownload *bool `json:"enableDownload"`
EnableCollect *bool `json:"enableCollect"`
EnableComment *bool `json:"enableComment"`
DisablePassword *bool `json:"disablePassword"`
EnableJoin *bool `json:"enableJoin"`
MinRole *CollectionParticipantRole `json:"minRole"`
}

func (ut *UpdatePublicAccessTokenRequest) Validate() error {
if ut.DeviceLimit == nil && ut.ValidTill == nil && ut.DisablePassword == nil &&
ut.Nonce == nil && ut.PassHash == nil && ut.EnableDownload == nil && ut.EnableCollect == nil && ut.EnableJoin == nil && ut.MinRole == nil {
ut.Nonce == nil && ut.PassHash == nil && ut.EnableDownload == nil && ut.EnableCollect == nil && ut.EnableComment == nil && ut.EnableJoin == nil && ut.MinRole == nil {
return NewBadRequestWithMessage("all parameters are missing")
}

Expand Down Expand Up @@ -86,6 +88,7 @@ type CollectionLinkRow struct {
OpsLimit *int64
EnableDownload bool
EnableCollect bool
EnableComment bool
EnableJoin bool
MinRole *CollectionParticipantRole
}
Expand Down Expand Up @@ -114,6 +117,7 @@ type PublicURL struct {
EnableDownload bool `json:"enableDownload"`
// Enable collect indicates whether folks can upload files in a publicly shared url
EnableCollect bool `json:"enableCollect"`
EnableComment bool `json:"enableComment"`
PasswordEnabled bool `json:"passwordEnabled"`
// Nonce contains the nonce value for the password if the link is password protected.
Nonce *string `json:"nonce,omitempty"`
Expand All @@ -124,10 +128,11 @@ type PublicURL struct {
}

type PublicAccessContext struct {
ID int64
IP string
UserAgent string
CollectionID int64
ID int64
IP string
UserAgent string
CollectionID int64
EnableComment bool
}

func FilterPublicURLsForRole(urls []PublicURL, role CollectionParticipantRole) []PublicURL {
Expand All @@ -154,7 +159,8 @@ type PublicCollectionSummary struct {
UpdatedAt int64
DeviceAccessCount int
// not empty value of passHash indicates that the link is password protected.
PassHash *string
PassHash *string
EnableComment bool
}

type AbuseReportRequest struct {
Expand Down
11 changes: 11 additions & 0 deletions server/ente/social/anon_user.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package social

// AnonUser captures encrypted profile metadata for anonymous public commenters.
type AnonUser struct {
ID string `json:"anonUserID"`
CollectionID int64 `json:"collectionID"`
Cipher string `json:"cipher"`
Nonce string `json:"nonce"`
CreatedAt int64 `json:"createdAt"`
UpdatedAt int64 `json:"updatedAt"`
}
16 changes: 16 additions & 0 deletions server/ente/social/comment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package social

// Comment represents an encrypted comment or reply stored in the database.
type Comment struct {
ID string `json:"id"`
CollectionID int64 `json:"collectionID"`
FileID *int64 `json:"fileID,omitempty"`
ParentCommentID *string `json:"parentCommentID,omitempty"`
UserID int64 `json:"userID"`
AnonUserID *string `json:"anonUserID,omitempty"`
Cipher string `json:"cipher,omitempty"`
Nonce string `json:"nonce,omitempty"`
IsDeleted bool `json:"isDeleted"`
CreatedAt int64 `json:"createdAt"`
UpdatedAt int64 `json:"updatedAt"`
}
16 changes: 16 additions & 0 deletions server/ente/social/reaction.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package social

// Reaction represents an encrypted reaction scoped to a collection, file, or comment.
type Reaction struct {
ID string `json:"id"`
CollectionID int64 `json:"collectionID"`
FileID *int64 `json:"fileID,omitempty"`
CommentID *string `json:"commentID,omitempty"`
UserID int64 `json:"userID"`
AnonUserID *string `json:"anonUserID,omitempty"`
Cipher string `json:"cipher,omitempty"`
Nonce string `json:"nonce,omitempty"`
IsDeleted bool `json:"isDeleted"`
CreatedAt int64 `json:"createdAt"`
UpdatedAt int64 `json:"updatedAt"`
}
Loading