Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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