diff --git a/go-socket b/go-socket index be5b708..1134ef2 100755 Binary files a/go-socket and b/go-socket differ diff --git a/main.go b/main.go index 057ec06..17316f0 100644 --- a/main.go +++ b/main.go @@ -69,6 +69,10 @@ func main() { http.HandleFunc("PUT /hub/join", withCORS(httpRequest.HandleHubJoin)) http.HandleFunc("PATCH /hub/name", withCORS(httpRequest.HandleHubSetName)) http.HandleFunc("PATCH /hub/color", withCORS(httpRequest.HandleHubSetColor)) + http.HandleFunc("PATCH /hub/icon", withCORS(httpRequest.HandleHubSetIcon)) + http.HandleFunc("GET /hub/icon", withCORS(httpRequest.HandleGetHubIcon)) + http.HandleFunc("PATCH /hub/bg", withCORS(httpRequest.HandleHubSetBg)) + http.HandleFunc("GET /hub/bg", withCORS(httpRequest.HandleGetHubBg)) http.HandleFunc("DELETE /hub", withCORS(httpRequest.HandleHubRemove)) http.HandleFunc("PATCH /hub/usercolorallowed", withCORS(httpRequest.HandleHubToggleUserColorAllowed)) http.HandleFunc("DELETE /hub/user", withCORS(httpRequest.HandleHubUserRemove)) @@ -93,7 +97,7 @@ func main() { http.HandleFunc("POST /connection/message", withCORS(httpRequest.HandleDm)) http.HandleFunc("GET /ws", wsServer.ServeWsConnection) - http.Handle("GET /client/", http.StripPrefix("/client/", http.FileServer(http.Dir("machine-client")))) + http.Handle("GET /client/", http.StripPrefix("/client/", http.FileServer(http.Dir("test-client")))) log.Println("beep boop; server server started") log.Fatal(http.ListenAndServe(":"+strconv.Itoa(int(config.Port)), nil)) diff --git a/packages/httpRequest/files.go b/packages/httpRequest/files.go index 4a50a0b..9f2c97d 100644 --- a/packages/httpRequest/files.go +++ b/packages/httpRequest/files.go @@ -29,18 +29,12 @@ func HandleAttachmentFileUpload(response http.ResponseWriter, request *http.Requ return } - request.Body = http.MaxBytesReader(response, request.Body, int64(config.MaxRequestWithFileBytes)) - if err = request.ParseMultipartForm(int64(config.MaxRequestBytes)); err != nil { http.Error(response, "invalid multipart form", http.StatusBadRequest) return } - conn, ok := getConnectionWithResponseOnFail(response, request.FormValue("connectionid"), user) - if !ok { - return - } - + target := request.FormValue("target_id") file, header, err := request.FormFile("file") if err != nil { http.Error(response, "missing file", http.StatusBadRequest) @@ -49,11 +43,31 @@ func HandleAttachmentFileUpload(response http.ResponseWriter, request *http.Requ defer file.Close() contentType := header.Header.Get("Content-Type") - key := minio.GetKey(&minio.GetKeyOptions{ - ConnectionId: conn.Id, - MimeType: contentType, - UploadType: minio.ConnectionFile, - }) + var key string + + if conn, ok := getConnection(ctx, target, user); ok { + key = minio.GetKey(&minio.GetKeyOptions{ + ConnectionId: conn.Id, + MimeType: contentType, + UploadType: minio.ConnectionFile, + }) + } else if channel, ok := getChannelFromUser(user, target); ok { + channel.Mu.RLock() + perms := channel.UsersCachedPermissions[user.Id] + channel.Mu.RUnlock() + if !perms.CanMessage() { + http.Error(response, "forbidden", http.StatusForbidden) + return + } + key = minio.GetKey(&minio.GetKeyOptions{ + ChannelId: channel.Id, + MimeType: contentType, + UploadType: minio.HubChannelFile, + }) + } else { + http.Error(response, "cannot find target", http.StatusBadRequest) + return + } if err = minio.Upload(ctx, key, file, header.Size, contentType, map[string]string{ "originalName": header.Filename, @@ -322,13 +336,27 @@ func HandleAttachmentFileDownload(response http.ResponseWriter, request *http.Re return } - conn, ok := getConnectionWithResponseOnFail(response, request.FormValue("connectionid"), user) - if !ok { + target := request.URL.Query().Get("target_id") + key := request.URL.Query().Get("key") + + var validPrefix string + if conn, ok := getConnection(ctx, target, user); ok { + validPrefix = string(minio.ConnectionFilePrefix) + conn.Id.String() + "/" + } else if channel, ok := getChannelFromUser(user, target); ok { + channel.Mu.RLock() + perms := channel.UsersCachedPermissions[user.Id] + channel.Mu.RUnlock() + if !perms.CanReadHistory() { + http.Error(response, "forbidden", http.StatusForbidden) + return + } + validPrefix = string(minio.HubChannelFilePrefix) + channel.Id.String() + "/" + } else { + http.Error(response, "cannot find target", http.StatusBadRequest) return } - key := request.URL.Query().Get("key") - if !strings.HasPrefix(key, string(minio.ConnectionFilePrefix)+conn.Id.String()+"/") { + if !strings.HasPrefix(key, validPrefix) { http.Error(response, "no such file", http.StatusUnauthorized) return } diff --git a/packages/httpRequest/get.go b/packages/httpRequest/get.go index eeb5014..1ed2682 100644 --- a/packages/httpRequest/get.go +++ b/packages/httpRequest/get.go @@ -36,6 +36,45 @@ func getUserByToken(ctx context.Context, token string) (*types.User, error) { return getUserById(ctx, userId) } +func getConnection(ctx context.Context, connectionIdStr string, user *types.User) (*types.Connection, bool) { + connectionId, err := convertions.StringToUuid(connectionIdStr) + if err != nil { + return nil, false + } + if conn, ok := cache.GetConnection(user, connectionId); ok { + return conn, true + } + conn, err := postgresql.ConnectionGetById(ctx, connectionId) + if err != nil { + return nil, false + } + if conn.RequestorId != user.Id && conn.RecipientId != user.Id { + return nil, false + } + user.Mu.Lock() + user.Connections[conn.Id] = conn + user.Mu.Unlock() + return conn, true +} + +func getChannelFromUser(user *types.User, channelIdStr string) (*types.HubChannel, bool) { + channelId, err := convertions.StringToUuid(channelIdStr) + if err != nil { + return nil, false + } + user.Mu.RLock() + defer user.Mu.RUnlock() + for _, hub := range user.Hubs { + hub.Mu.RLock() + ch, ok := hub.Channels[channelId] + hub.Mu.RUnlock() + if ok { + return ch, true + } + } + return nil, false +} + func getConnectionWithResponseOnFail(response http.ResponseWriter, connectionIdStr string, user *types.User) (*types.Connection, bool) { connectionId, err := convertions.StringToUuid(connectionIdStr) if err != nil { diff --git a/packages/httpRequest/helper.go b/packages/httpRequest/helper.go index fc3f78b..9653f2b 100644 --- a/packages/httpRequest/helper.go +++ b/packages/httpRequest/helper.go @@ -41,6 +41,8 @@ func validCheckWithResponseOnFail(response http.ResponseWriter, request *http.Re maxSize = int64(config.MaxRequestBytes) } + request.Body = http.MaxBytesReader(response, request.Body, maxSize) + if request.ContentLength > maxSize { io.Copy(io.Discard, request.Body) http.Error(response, "Request too large", http.StatusRequestEntityTooLarge) diff --git a/packages/httpRequest/hubs.go b/packages/httpRequest/hubs.go index 79ab776..1ecfc06 100644 --- a/packages/httpRequest/hubs.go +++ b/packages/httpRequest/hubs.go @@ -551,6 +551,74 @@ func HandleHubSetBg(response http.ResponseWriter, request *http.Request) { response.WriteHeader(http.StatusCreated) } +func HandleGetHubIcon(response http.ResponseWriter, request *http.Request) { + if !validCheckWithResponseOnFail(response, request, normal) { + return + } + ctx := request.Context() + _, _, hub, err := getHubUserIfValidWithResponseOnFail(ctx, response, request) + if err != nil { + return + } + + if hub.IconUrl == "" { + http.Error(response, "hub has no icon", http.StatusNoContent) + return + } + + url, meta, err := minio.GetDownloadUrlAndMetadata(ctx, hub.IconUrl) + if err != nil { + http.Error(response, "internal server error", http.StatusInternalServerError) + return + } + + iconData, err := json.Marshal(map[string]any{ + "url": url.String(), + "metadata": meta, + }) + if err != nil { + http.Error(response, "json error", http.StatusInternalServerError) + return + } + + response.WriteHeader(http.StatusOK) + response.Write(iconData) +} + +func HandleGetHubBg(response http.ResponseWriter, request *http.Request) { + if !validCheckWithResponseOnFail(response, request, normal) { + return + } + ctx := request.Context() + _, _, hub, err := getHubUserIfValidWithResponseOnFail(ctx, response, request) + if err != nil { + return + } + + if hub.BgUrl == "" { + http.Error(response, "hub has no background", http.StatusNoContent) + return + } + + url, meta, err := minio.GetDownloadUrlAndMetadata(ctx, hub.BgUrl) + if err != nil { + http.Error(response, "internal server error", http.StatusInternalServerError) + return + } + + bgData, err := json.Marshal(map[string]any{ + "url": url.String(), + "metadata": meta, + }) + if err != nil { + http.Error(response, "json error", http.StatusInternalServerError) + return + } + + response.WriteHeader(http.StatusOK) + response.Write(bgData) +} + func HandleHubRemove(response http.ResponseWriter, request *http.Request) { _, hub, _, _, ok := hubPermissionContext(response, request, normal, types.PermissionRemoveHub) if !ok { @@ -852,7 +920,7 @@ func HandleRoleSetColor(response http.ResponseWriter, request *http.Request) { } color, err := convertions.StringToRgba(request.FormValue("new_color")) if err != nil { - http.Error(response, "invalid newcolor", http.StatusBadRequest) + http.Error(response, "invalid new_color", http.StatusBadRequest) return } diff --git a/packages/minio/minio.go b/packages/minio/minio.go index c5dad4f..58d256a 100644 --- a/packages/minio/minio.go +++ b/packages/minio/minio.go @@ -26,17 +26,19 @@ const ( UserProfileBg HubIcon HubBackground + ChannelIcon ) type DataTypePrefix string const ( ConnectionFilePrefix DataTypePrefix = "connection/" - HubChannelFilePrefix DataTypePrefix = "hub/" + HubChannelFilePrefix DataTypePrefix = "hubChannel/" UserAvatarPrefix DataTypePrefix = "userAvatar/" UserProfileBgPrefix DataTypePrefix = "userProfileBg/" HubIconPrefix DataTypePrefix = "hubIcon/" HubBackgroundPrefix DataTypePrefix = "hubBackground/" + ChannelIconPrefix DataTypePrefix = "channelIcon/" ) type GetKeyOptions struct { diff --git a/packages/postgresql/postgresql.go b/packages/postgresql/postgresql.go index c01b3e8..9a34b70 100644 --- a/packages/postgresql/postgresql.go +++ b/packages/postgresql/postgresql.go @@ -181,6 +181,19 @@ func ConnectionDelete(ctx context.Context, conn *types.Connection) error { return err } +func ConnectionGetById(ctx context.Context, id uuid.UUID) (*types.Connection, error) { + conn := types.NewConn() + err := dbConn.QueryRow(ctx, ` + SELECT id, requestor_id, recipient_id, state, created_at + FROM user_connections + WHERE id = $1 + `, id).Scan(&conn.Id, &conn.RequestorId, &conn.RecipientId, &conn.State, &conn.CreatedAt) + if err != nil { + return nil, err + } + return conn, nil +} + func ConnectionsGetBelongingToUser(ctx context.Context, user *types.User) error { rows, err := dbConn.Query(ctx, ` SELECT id, requestor_id, recipient_id, state, created_at diff --git a/packages/types/types.go b/packages/types/types.go index f03e29e..f9a8976 100644 --- a/packages/types/types.go +++ b/packages/types/types.go @@ -38,7 +38,7 @@ type User struct { WsConn *websocket.Conn `json:"-"` Id uuid.UUID `json:"-"` Connections map[uuid.UUID]*Connection `json:"-"` - Hubs map[uuid.UUID]*Hub `json:"-"` + Hubs map[uuid.UUID]*Hub `json:"hubs-to-delete"` Color Rgba `json:"color"` } @@ -249,7 +249,7 @@ const ( CachedUserCanMessage ) -const CachedUserPermissionsAll = CachedUserCanMessage | CachedUserCanReadHistory | CachedUserCanReadHistory +const CachedUserPermissionsAll = CachedUserCanView | CachedUserCanReadHistory | CachedUserCanMessage func (p *CachedUserPermissions) SetCanView() { *p |= CachedUserCanView } func (p *CachedUserPermissions) ClearCanView() { *p &^= CachedUserCanView } @@ -287,12 +287,11 @@ func NewHub() *Hub { } type HubRole struct { - Name string `json:"role"` - CreatedAt time.Time `json:"createdAt"` - Permissions Permissions `json:"permissions"` - Color Rgba `json:"color"` - Id uint8 `json:"id"` - BoundedGroup uint8 `json:"boundedGroup"` // BoundedGroup 0 for global + Name string `json:"role"` + CreatedAt time.Time `json:"createdAt"` + Permissions Permissions `json:"permissions"` + Color Rgba `json:"color"` + Id uint8 `json:"id"` } func (h *HubRole) GrantPermission(r Permissions) { diff --git a/packages/wsServer/wsServer.go b/packages/wsServer/wsServer.go index f4dae95..83a89d8 100644 --- a/packages/wsServer/wsServer.go +++ b/packages/wsServer/wsServer.go @@ -32,7 +32,7 @@ func ServeWsConnection(responseWriter http.ResponseWriter, request *http.Request var user = types.User{WsConn: connection} var isAuthenticated bool - var ignoreCache bool + var ignoreCache bool = true defer func() { closeConnection(&user, ignoreCache) }() for { diff --git a/machine-client/.gitignore b/test-client/.gitignore similarity index 100% rename from machine-client/.gitignore rename to test-client/.gitignore diff --git a/machine-client/index.html b/test-client/index.html similarity index 92% rename from machine-client/index.html rename to test-client/index.html index 94868c7..b4d5140 100644 --- a/machine-client/index.html +++ b/test-client/index.html @@ -53,6 +53,10 @@ + + + + @@ -178,7 +182,7 @@
-
+
@@ -286,6 +290,36 @@
+ +
+
+
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+
+
@@ -541,6 +575,10 @@ 'get-users': { method:'GET', path:'/users', title:'GET /users — get multiple users by IDs', fields:[{id:'gus-token',dest:'header',name:'token'},{id:'gus-targetids',dest:'query',name:'target_ids'}] }, 'hub-set-name': { method:'PATCH', path:'/hub/name', title:'PATCH /hub/name — set hub name', fields:[{id:'hsn-token',dest:'header',name:'token'},{id:'hsn-hubid',dest:'header',name:'hub_id'},{id:'hsn-newname',dest:'body',name:'new_name'}] }, 'hub-set-color': { method:'PATCH', path:'/hub/color', title:'PATCH /hub/color — set hub color (R,G,B,A)', fields:[{id:'hsc-token',dest:'header',name:'token'},{id:'hsc-hubid',dest:'header',name:'hub_id'},{id:'hsc-newcolor',dest:'body',name:'new_color'}] }, + 'hub-set-icon': { title:'PATCH /hub/icon — set hub icon image' }, + 'get-hub-icon': { method:'GET', path:'/hub/icon', title:'GET /hub/icon — get hub icon URL', fields:[{id:'ghi-token',dest:'header',name:'token'},{id:'ghi-hubid',dest:'header',name:'hub_id'}] }, + 'hub-set-bg': { title:'PATCH /hub/bg — set hub background image' }, + 'get-hub-bg': { method:'GET', path:'/hub/bg', title:'GET /hub/bg — get hub background URL', fields:[{id:'ghbg-token',dest:'header',name:'token'},{id:'ghbg-hubid',dest:'header',name:'hub_id'}] }, 'hub-remove': { method:'DELETE', path:'/hub', title:'DELETE /hub — remove hub', fields:[{id:'hr-token',dest:'header',name:'token'},{id:'hr-hubid',dest:'header',name:'hub_id'}] }, 'hub-toggle-color': { method:'PATCH', path:'/hub/usercolorallowed', title:'PATCH /hub/usercolorallowed — toggle user color allowed', fields:[{id:'htc-token',dest:'header',name:'token'},{id:'htc-hubid',dest:'header',name:'hub_id'}] }, 'hub-user-remove': { method:'DELETE', path:'/hub/user', title:'DELETE /hub/user — remove user from hub', fields:[{id:'hur-token',dest:'header',name:'token'},{id:'hur-hubid',dest:'header',name:'hub_id'},{id:'hur-targetid',dest:'query',name:'target_id'}] }, @@ -778,9 +816,9 @@ async function submitFileDownload() { const token = document.getElementById('fd-token').value; - const connectionid = document.getElementById('fd-connectionid').value; + const targetId = document.getElementById('fd-targetid').value; const key = document.getElementById('fd-key').value; - const query = new URLSearchParams({ connection_id: connectionid, key }); + const query = new URLSearchParams({ target_id: targetId, key }); log('GET /file', query.toString(), 'log-info'); try { const resp = await fetch(baseUrl() + '/file?' + query.toString(), { method: 'GET', headers: { token } }); @@ -845,6 +883,36 @@ } catch(e) { log('HTTP ERR', e.message, 'log-err'); } } + async function submitHubIconUpload() { + const token = document.getElementById('hsi-token').value; + const hubid = document.getElementById('hsi-hubid').value; + const fileInput = document.getElementById('hsi-file'); + if (!fileInput.files.length) { log('HTTP ERR', 'no file selected', 'log-err'); return; } + const form = new FormData(); + form.append('file', fileInput.files[0]); + log('PATCH /hub/icon', 'file=' + fileInput.files[0].name, 'log-info'); + try { + const resp = await fetch(baseUrl() + '/hub/icon', { method: 'PATCH', headers: { token, hub_id: hubid }, body: form }); + const text = await resp.text(); + log('HTTP ' + resp.status, text, resp.ok ? 'log-http' : 'log-err'); + } catch(e) { log('HTTP ERR', e.message, 'log-err'); } + } + + async function submitHubBgUpload() { + const token = document.getElementById('hsb-token').value; + const hubid = document.getElementById('hsb-hubid').value; + const fileInput = document.getElementById('hsb-file'); + if (!fileInput.files.length) { log('HTTP ERR', 'no file selected', 'log-err'); return; } + const form = new FormData(); + form.append('file', fileInput.files[0]); + log('PATCH /hub/bg', 'file=' + fileInput.files[0].name, 'log-info'); + try { + const resp = await fetch(baseUrl() + '/hub/bg', { method: 'PATCH', headers: { token, hub_id: hubid }, body: form }); + const text = await resp.text(); + log('HTTP ' + resp.status, text, resp.ok ? 'log-http' : 'log-err'); + } catch(e) { log('HTTP ERR', e.message, 'log-err'); } + } + async function submitGetUserProfileBg() { const token = document.getElementById('gpb-token').value; const userid = document.getElementById('gpb-userid').value; diff --git a/machine-client/style.css b/test-client/style.css similarity index 100% rename from machine-client/style.css rename to test-client/style.css diff --git a/todo.txt b/todo.txt index b54d371..89ce19f 100644 --- a/todo.txt +++ b/todo.txt @@ -1,8 +1,11 @@ +check hubs attachment working +add option to change join role +add channel icons + +make hub persistent when user not ws connected collect count of unread messages for each conn (add db table in future) -setting avatar and profilebg -check when mutex needed -change api endpoints and body/url/header to follow same case +fix cache user banners