From 2d73f9138042bcd20aa401e74b7e08f6e66f2383 Mon Sep 17 00:00:00 2001 From: Nityananda Gohain Date: Thu, 6 Mar 2025 15:39:45 +0530 Subject: [PATCH] Fix: Multitenancy support for ORG (#7155) * fix: support multitenancy in org * fix: register and login working now * fix: changes to migration * fix: migrations run both on sqlite and postgres * fix: remove user flags from fe and be * fix: remove ingestion keys from update * fix: multitenancy support for apdex settings * fix: render ts for users correctly * fix: fix migration to run for new tenants * fix: clean up migrations * fix: address comments * Update pkg/sqlmigration/013_update_organization.go Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * fix: fix build * fix: force invites with org id * Update pkg/query-service/auth/auth.go Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * fix: address comments * fix: address comments * fix: provier with their own dialect * fix: update dialects * fix: remove unwanted change * Update pkg/query-service/app/http_handler.go Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * fix: different files for types --------- Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> --- ee/query-service/app/api/cloudIntegrations.go | 25 +- ee/query-service/app/api/pat.go | 8 +- ee/query-service/app/server.go | 5 +- ee/query-service/auth/auth.go | 10 +- ee/query-service/dao/interface.go | 3 +- ee/query-service/dao/sqlite/auth.go | 25 +- ee/query-service/dao/sqlite/pat.go | 17 +- ee/query-service/model/domain.go | 4 +- frontend/src/api/user/setFlags.ts | 26 -- .../ReleaseNote/ReleaseNoteProps.ts | 4 - .../ReleaseNote/Releases/ReleaseNote0120.tsx | 61 --- frontend/src/components/ReleaseNote/index.tsx | 68 --- .../src/container/ListAlertRules/index.tsx | 4 - .../OrganizationSettings/Members/index.tsx | 2 +- .../DashboardsListPage/DashboardsListPage.tsx | 5 - frontend/src/pages/Services/index.tsx | 6 - frontend/src/providers/App/App.tsx | 9 - frontend/src/providers/App/types.ts | 2 - frontend/src/providers/App/utils.ts | 1 - frontend/src/tests/test-utils.tsx | 2 - frontend/src/types/api/user/getUser.ts | 2 - frontend/src/types/api/user/setFlags.ts | 12 - pkg/query-service/app/apdex.go | 17 +- pkg/query-service/app/auth.go | 5 +- pkg/query-service/app/http_handler.go | 88 ++-- pkg/query-service/app/parser.go | 20 +- pkg/query-service/app/server.go | 5 +- pkg/query-service/auth/auth.go | 121 ++--- pkg/query-service/auth/rbac.go | 22 +- pkg/query-service/common/user.go | 6 +- pkg/query-service/dao/interface.go | 50 +-- pkg/query-service/dao/sqlite/apdex.go | 43 +- pkg/query-service/dao/sqlite/connection.go | 5 +- pkg/query-service/dao/sqlite/rbac.go | 415 ++++++++---------- pkg/query-service/model/auth.go | 5 - pkg/query-service/model/db.go | 91 +--- pkg/query-service/telemetry/telemetry.go | 9 +- .../integration/filter_suggestions_test.go | 4 +- .../integration/logparsingpipeline_test.go | 4 +- .../signoz_cloud_integrations_test.go | 4 +- .../integration/signoz_integrations_test.go | 3 +- .../tests/integration/test_utils.go | 15 +- pkg/query-service/utils/testutils.go | 8 +- pkg/signoz/signoz.go | 3 + pkg/sqlmigration/009_add_pats.go | 2 +- pkg/sqlmigration/012_modify_org_domain.go | 6 + pkg/sqlmigration/013_update_organization.go | 152 +++++++ pkg/sqlmigration/sqlmigration.go | 26 ++ pkg/sqlstore/postgressqlstore/dialect.go | 116 +++++ pkg/sqlstore/postgressqlstore/provider.go | 6 + pkg/sqlstore/sqlitesqlstore/dialect.go | 115 +++++ pkg/sqlstore/sqlitesqlstore/provider.go | 6 + pkg/sqlstore/sqlstore.go | 10 + pkg/sqlstore/sqlstoretest/dialect.go | 26 ++ pkg/sqlstore/sqlstoretest/provider.go | 22 +- pkg/types/auditable.go | 13 + pkg/types/organization.go | 73 +-- pkg/types/user.go | 54 +++ 58 files changed, 1023 insertions(+), 848 deletions(-) delete mode 100644 frontend/src/api/user/setFlags.ts delete mode 100644 frontend/src/components/ReleaseNote/ReleaseNoteProps.ts delete mode 100644 frontend/src/components/ReleaseNote/Releases/ReleaseNote0120.tsx delete mode 100644 frontend/src/components/ReleaseNote/index.tsx delete mode 100644 frontend/src/types/api/user/setFlags.ts create mode 100644 pkg/sqlmigration/013_update_organization.go create mode 100644 pkg/sqlstore/postgressqlstore/dialect.go create mode 100644 pkg/sqlstore/sqlitesqlstore/dialect.go create mode 100644 pkg/sqlstore/sqlstoretest/dialect.go create mode 100644 pkg/types/auditable.go create mode 100644 pkg/types/user.go diff --git a/ee/query-service/app/api/cloudIntegrations.go b/ee/query-service/app/api/cloudIntegrations.go index da1f482688..8b2e38bb12 100644 --- a/ee/query-service/app/api/cloudIntegrations.go +++ b/ee/query-service/app/api/cloudIntegrations.go @@ -18,6 +18,7 @@ import ( baseconstants "go.signoz.io/signoz/pkg/query-service/constants" "go.signoz.io/signoz/pkg/query-service/dao" basemodel "go.signoz.io/signoz/pkg/query-service/model" + "go.signoz.io/signoz/pkg/types" "go.uber.org/zap" ) @@ -45,7 +46,7 @@ func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseW return } - apiKey, apiErr := ah.getOrCreateCloudIntegrationPAT(r.Context(), currentUser.OrgId, cloudProvider) + apiKey, apiErr := ah.getOrCreateCloudIntegrationPAT(r.Context(), currentUser.OrgID, cloudProvider) if apiErr != nil { RespondError(w, basemodel.WrapApiError( apiErr, "couldn't provision PAT for cloud integration:", @@ -124,7 +125,7 @@ func (ah *APIHandler) getOrCreateCloudIntegrationPAT(ctx context.Context, orgId )) } for _, p := range allPats { - if p.UserID == integrationUser.Id && p.Name == integrationPATName { + if p.UserID == integrationUser.ID && p.Name == integrationPATName { return p.Token, nil } } @@ -136,7 +137,7 @@ func (ah *APIHandler) getOrCreateCloudIntegrationPAT(ctx context.Context, orgId newPAT := model.PAT{ Token: generatePATToken(), - UserID: integrationUser.Id, + UserID: integrationUser.ID, Name: integrationPATName, Role: baseconstants.ViewerGroup, ExpiresAt: 0, @@ -154,7 +155,7 @@ func (ah *APIHandler) getOrCreateCloudIntegrationPAT(ctx context.Context, orgId func (ah *APIHandler) getOrCreateCloudIntegrationUser( ctx context.Context, orgId string, cloudProvider string, -) (*basemodel.User, *basemodel.ApiError) { +) (*types.User, *basemodel.ApiError) { cloudIntegrationUserId := fmt.Sprintf("%s-integration", cloudProvider) integrationUserResult, apiErr := ah.AppDao().GetUser(ctx, cloudIntegrationUserId) @@ -171,19 +172,21 @@ func (ah *APIHandler) getOrCreateCloudIntegrationUser( zap.String("cloudProvider", cloudProvider), ) - newUser := &basemodel.User{ - Id: cloudIntegrationUserId, - Name: fmt.Sprintf("%s integration", cloudProvider), - Email: fmt.Sprintf("%s@signoz.io", cloudIntegrationUserId), - CreatedAt: time.Now().Unix(), - OrgId: orgId, + newUser := &types.User{ + ID: cloudIntegrationUserId, + Name: fmt.Sprintf("%s integration", cloudProvider), + Email: fmt.Sprintf("%s@signoz.io", cloudIntegrationUserId), + TimeAuditable: types.TimeAuditable{ + CreatedAt: time.Now(), + }, + OrgID: orgId, } viewerGroup, apiErr := dao.DB().GetGroupByName(ctx, baseconstants.ViewerGroup) if apiErr != nil { return nil, basemodel.WrapApiError(apiErr, "couldn't get viewer group for creating integration user") } - newUser.GroupId = viewerGroup.ID + newUser.GroupID = viewerGroup.ID passwordHash, err := auth.PasswordHash(uuid.NewString()) if err != nil { diff --git a/ee/query-service/app/api/pat.go b/ee/query-service/app/api/pat.go index 08304788e8..a2e0341927 100644 --- a/ee/query-service/app/api/pat.go +++ b/ee/query-service/app/api/pat.go @@ -54,7 +54,7 @@ func (ah *APIHandler) createPAT(w http.ResponseWriter, r *http.Request) { } // All the PATs are associated with the user creating the PAT. - pat.UserID = user.Id + pat.UserID = user.ID pat.CreatedAt = time.Now().Unix() pat.UpdatedAt = time.Now().Unix() pat.LastUsed = 0 @@ -112,7 +112,7 @@ func (ah *APIHandler) updatePAT(w http.ResponseWriter, r *http.Request) { return } - req.UpdatedByUserID = user.Id + req.UpdatedByUserID = user.ID id := mux.Vars(r)["id"] req.UpdatedAt = time.Now().Unix() zap.L().Info("Got Update PAT request", zap.Any("pat", req)) @@ -135,7 +135,7 @@ func (ah *APIHandler) getPATs(w http.ResponseWriter, r *http.Request) { }, nil) return } - zap.L().Info("Get PATs for user", zap.String("user_id", user.Id)) + zap.L().Info("Get PATs for user", zap.String("user_id", user.ID)) pats, apierr := ah.AppDao().ListPATs(ctx) if apierr != nil { RespondError(w, apierr, nil) @@ -157,7 +157,7 @@ func (ah *APIHandler) revokePAT(w http.ResponseWriter, r *http.Request) { } zap.L().Info("Revoke PAT with id", zap.String("id", id)) - if apierr := ah.AppDao().RevokePAT(ctx, id, user.Id); apierr != nil { + if apierr := ah.AppDao().RevokePAT(ctx, id, user.ID); apierr != nil { RespondError(w, apierr, nil) return } diff --git a/ee/query-service/app/server.go b/ee/query-service/app/server.go index 685c7910b5..2b6f0e4d05 100644 --- a/ee/query-service/app/server.go +++ b/ee/query-service/app/server.go @@ -25,6 +25,7 @@ import ( "go.signoz.io/signoz/ee/query-service/rules" "go.signoz.io/signoz/pkg/http/middleware" "go.signoz.io/signoz/pkg/signoz" + "go.signoz.io/signoz/pkg/types" "go.signoz.io/signoz/pkg/types/authtypes" "go.signoz.io/signoz/pkg/web" @@ -340,14 +341,14 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h r := baseapp.NewRouter() // add auth middleware - getUserFromRequest := func(ctx context.Context) (*basemodel.UserPayload, error) { + getUserFromRequest := func(ctx context.Context) (*types.GettableUser, error) { user, err := auth.GetUserFromRequestContext(ctx, apiHandler) if err != nil { return nil, err } - if user.User.OrgId == "" { + if user.User.OrgID == "" { return nil, basemodel.UnauthorizedError(errors.New("orgId is missing in the claims")) } diff --git a/ee/query-service/auth/auth.go b/ee/query-service/auth/auth.go index 11fdb5a7da..06dd125aed 100644 --- a/ee/query-service/auth/auth.go +++ b/ee/query-service/auth/auth.go @@ -7,14 +7,14 @@ import ( "go.signoz.io/signoz/ee/query-service/app/api" baseauth "go.signoz.io/signoz/pkg/query-service/auth" - basemodel "go.signoz.io/signoz/pkg/query-service/model" "go.signoz.io/signoz/pkg/query-service/telemetry" + "go.signoz.io/signoz/pkg/types" "go.signoz.io/signoz/pkg/types/authtypes" "go.uber.org/zap" ) -func GetUserFromRequestContext(ctx context.Context, apiHandler *api.APIHandler) (*basemodel.UserPayload, error) { +func GetUserFromRequestContext(ctx context.Context, apiHandler *api.APIHandler) (*types.GettableUser, error) { patToken, ok := authtypes.UUIDFromContext(ctx) if ok && patToken != "" { zap.L().Debug("Received a non-zero length PAT token") @@ -40,9 +40,9 @@ func GetUserFromRequestContext(ctx context.Context, apiHandler *api.APIHandler) } telemetry.GetInstance().SetPatTokenUser() dao.UpdatePATLastUsed(ctx, patToken, time.Now().Unix()) - user.User.GroupId = group.ID - user.User.Id = pat.Id - return &basemodel.UserPayload{ + user.User.GroupID = group.ID + user.User.ID = pat.Id + return &types.GettableUser{ User: user.User, Role: pat.Role, }, nil diff --git a/ee/query-service/dao/interface.go b/ee/query-service/dao/interface.go index 1708a4ada2..0666298e47 100644 --- a/ee/query-service/dao/interface.go +++ b/ee/query-service/dao/interface.go @@ -10,6 +10,7 @@ import ( basedao "go.signoz.io/signoz/pkg/query-service/dao" baseint "go.signoz.io/signoz/pkg/query-service/interfaces" basemodel "go.signoz.io/signoz/pkg/query-service/model" + "go.signoz.io/signoz/pkg/types" "go.signoz.io/signoz/pkg/types/authtypes" ) @@ -39,7 +40,7 @@ type ModelDao interface { GetPAT(ctx context.Context, pat string) (*model.PAT, basemodel.BaseApiError) UpdatePATLastUsed(ctx context.Context, pat string, lastUsed int64) basemodel.BaseApiError GetPATByID(ctx context.Context, id string) (*model.PAT, basemodel.BaseApiError) - GetUserByPAT(ctx context.Context, token string) (*basemodel.UserPayload, basemodel.BaseApiError) + GetUserByPAT(ctx context.Context, token string) (*types.GettableUser, basemodel.BaseApiError) ListPATs(ctx context.Context) ([]model.PAT, basemodel.BaseApiError) RevokePAT(ctx context.Context, id string, userID string) basemodel.BaseApiError } diff --git a/ee/query-service/dao/sqlite/auth.go b/ee/query-service/dao/sqlite/auth.go index ca7df0fd14..d30a3063d8 100644 --- a/ee/query-service/dao/sqlite/auth.go +++ b/ee/query-service/dao/sqlite/auth.go @@ -14,11 +14,12 @@ import ( baseconst "go.signoz.io/signoz/pkg/query-service/constants" basemodel "go.signoz.io/signoz/pkg/query-service/model" "go.signoz.io/signoz/pkg/query-service/utils" + "go.signoz.io/signoz/pkg/types" "go.signoz.io/signoz/pkg/types/authtypes" "go.uber.org/zap" ) -func (m *modelDao) createUserForSAMLRequest(ctx context.Context, email string) (*basemodel.User, basemodel.BaseApiError) { +func (m *modelDao) createUserForSAMLRequest(ctx context.Context, email string) (*types.User, basemodel.BaseApiError) { // get auth domain from email domain domain, apierr := m.GetDomainByEmail(ctx, email) if apierr != nil { @@ -42,15 +43,17 @@ func (m *modelDao) createUserForSAMLRequest(ctx context.Context, email string) ( return nil, apiErr } - user := &basemodel.User{ - Id: uuid.NewString(), - Name: "", - Email: email, - Password: hash, - CreatedAt: time.Now().Unix(), + user := &types.User{ + ID: uuid.NewString(), + Name: "", + Email: email, + Password: hash, + TimeAuditable: types.TimeAuditable{ + CreatedAt: time.Now(), + }, ProfilePictureURL: "", // Currently unused - GroupId: group.ID, - OrgId: domain.OrgId, + GroupID: group.ID, + OrgID: domain.OrgId, } user, apiErr = m.CreateUser(ctx, user, false) @@ -73,7 +76,7 @@ func (m *modelDao) PrepareSsoRedirect(ctx context.Context, redirectUri, email st return "", model.BadRequestStr("invalid user email received from the auth provider") } - user := &basemodel.User{} + user := &types.User{} if userPayload == nil { newUser, apiErr := m.createUserForSAMLRequest(ctx, email) @@ -95,7 +98,7 @@ func (m *modelDao) PrepareSsoRedirect(ctx context.Context, redirectUri, email st return fmt.Sprintf("%s?jwt=%s&usr=%s&refreshjwt=%s", redirectUri, tokenStore.AccessJwt, - user.Id, + user.ID, tokenStore.RefreshJwt), nil } diff --git a/ee/query-service/dao/sqlite/pat.go b/ee/query-service/dao/sqlite/pat.go index 75169db685..14ca411afe 100644 --- a/ee/query-service/dao/sqlite/pat.go +++ b/ee/query-service/dao/sqlite/pat.go @@ -8,6 +8,7 @@ import ( "go.signoz.io/signoz/ee/query-service/model" basemodel "go.signoz.io/signoz/pkg/query-service/model" + "go.signoz.io/signoz/pkg/types" "go.uber.org/zap" ) @@ -42,10 +43,10 @@ func (m *modelDao) CreatePAT(ctx context.Context, p model.PAT) (model.PAT, basem } } else { p.CreatedByUser = model.User{ - Id: createdByUser.Id, + Id: createdByUser.ID, Name: createdByUser.Name, Email: createdByUser.Email, - CreatedAt: createdByUser.CreatedAt, + CreatedAt: createdByUser.CreatedAt.Unix(), ProfilePictureURL: createdByUser.ProfilePictureURL, NotFound: false, } @@ -95,10 +96,10 @@ func (m *modelDao) ListPATs(ctx context.Context) ([]model.PAT, basemodel.BaseApi } } else { pats[i].CreatedByUser = model.User{ - Id: createdByUser.Id, + Id: createdByUser.ID, Name: createdByUser.Name, Email: createdByUser.Email, - CreatedAt: createdByUser.CreatedAt, + CreatedAt: createdByUser.CreatedAt.Unix(), ProfilePictureURL: createdByUser.ProfilePictureURL, NotFound: false, } @@ -111,10 +112,10 @@ func (m *modelDao) ListPATs(ctx context.Context) ([]model.PAT, basemodel.BaseApi } } else { pats[i].UpdatedByUser = model.User{ - Id: updatedByUser.Id, + Id: updatedByUser.ID, Name: updatedByUser.Name, Email: updatedByUser.Email, - CreatedAt: updatedByUser.CreatedAt, + CreatedAt: updatedByUser.CreatedAt.Unix(), ProfilePictureURL: updatedByUser.ProfilePictureURL, NotFound: false, } @@ -170,8 +171,8 @@ func (m *modelDao) GetPATByID(ctx context.Context, id string) (*model.PAT, basem } // deprecated -func (m *modelDao) GetUserByPAT(ctx context.Context, token string) (*basemodel.UserPayload, basemodel.BaseApiError) { - users := []basemodel.UserPayload{} +func (m *modelDao) GetUserByPAT(ctx context.Context, token string) (*types.GettableUser, basemodel.BaseApiError) { + users := []types.GettableUser{} query := `SELECT u.id, diff --git a/ee/query-service/model/domain.go b/ee/query-service/model/domain.go index 59a2493525..863b949da7 100644 --- a/ee/query-service/model/domain.go +++ b/ee/query-service/model/domain.go @@ -11,7 +11,7 @@ import ( saml2 "github.com/russellhaering/gosaml2" "go.signoz.io/signoz/ee/query-service/sso" "go.signoz.io/signoz/ee/query-service/sso/saml" - basemodel "go.signoz.io/signoz/pkg/query-service/model" + "go.signoz.io/signoz/pkg/types" "go.uber.org/zap" ) @@ -33,7 +33,7 @@ type OrgDomain struct { SamlConfig *SamlConfig `json:"samlConfig"` GoogleAuthConfig *GoogleOAuthConfig `json:"googleAuthConfig"` - Org *basemodel.Organization + Org *types.Organization } func (od *OrgDomain) String() string { diff --git a/frontend/src/api/user/setFlags.ts b/frontend/src/api/user/setFlags.ts deleted file mode 100644 index 0ae9b1855d..0000000000 --- a/frontend/src/api/user/setFlags.ts +++ /dev/null @@ -1,26 +0,0 @@ -import axios from 'api'; -import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; -import { AxiosError } from 'axios'; -import { ErrorResponse, SuccessResponse } from 'types/api'; -import { PayloadProps, Props } from 'types/api/user/setFlags'; - -const setFlags = async ( - props: Props, -): Promise | ErrorResponse> => { - try { - const response = await axios.patch(`/user/${props.userId}/flags`, { - ...props.flags, - }); - - return { - statusCode: 200, - error: null, - message: response.data?.status, - payload: response.data, - }; - } catch (error) { - return ErrorResponseHandler(error as AxiosError); - } -}; - -export default setFlags; diff --git a/frontend/src/components/ReleaseNote/ReleaseNoteProps.ts b/frontend/src/components/ReleaseNote/ReleaseNoteProps.ts deleted file mode 100644 index f2407592cb..0000000000 --- a/frontend/src/components/ReleaseNote/ReleaseNoteProps.ts +++ /dev/null @@ -1,4 +0,0 @@ -export default interface ReleaseNoteProps { - path?: string; - release?: string; -} diff --git a/frontend/src/components/ReleaseNote/Releases/ReleaseNote0120.tsx b/frontend/src/components/ReleaseNote/Releases/ReleaseNote0120.tsx deleted file mode 100644 index b6f991fc60..0000000000 --- a/frontend/src/components/ReleaseNote/Releases/ReleaseNote0120.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { Button, Space } from 'antd'; -import setFlags from 'api/user/setFlags'; -import MessageTip from 'components/MessageTip'; -import { useAppContext } from 'providers/App/App'; -import { useCallback } from 'react'; -import { UserFlags } from 'types/api/user/setFlags'; - -import ReleaseNoteProps from '../ReleaseNoteProps'; - -export default function ReleaseNote0120({ - release, -}: ReleaseNoteProps): JSX.Element | null { - const { user, setUserFlags } = useAppContext(); - - const handleDontShow = useCallback(async (): Promise => { - const flags: UserFlags = { ReleaseNote0120Hide: 'Y' }; - - try { - setUserFlags(flags); - if (!user) { - // no user is set, so escape the routine - return; - } - - const response = await setFlags({ userId: user.id, flags }); - - if (response.statusCode !== 200) { - console.log('failed to complete do not show status', response.error); - } - } catch (e) { - // here we do not nothing as the cost of error is minor, - // the user can switch the do no show option again in the further. - console.log('unexpected error: failed to complete do not show status', e); - } - }, [setUserFlags, user]); - - return ( - - You are using {release} of SigNoz. We have introduced distributed setup in - v0.12.0 release. If you use or plan to use clickhouse queries in dashboard - or alerts, you might want to read about querying the new distributed tables{' '} - - here - - - } - action={ - - - - } - /> - ); -} diff --git a/frontend/src/components/ReleaseNote/index.tsx b/frontend/src/components/ReleaseNote/index.tsx deleted file mode 100644 index 8ce17ce497..0000000000 --- a/frontend/src/components/ReleaseNote/index.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import ReleaseNoteProps from 'components/ReleaseNote/ReleaseNoteProps'; -import ReleaseNote0120 from 'components/ReleaseNote/Releases/ReleaseNote0120'; -import ROUTES from 'constants/routes'; -import { useAppContext } from 'providers/App/App'; -import { useSelector } from 'react-redux'; -import { AppState } from 'store/reducers'; -import { UserFlags } from 'types/api/user/setFlags'; -import AppReducer from 'types/reducer/app'; - -interface ComponentMapType { - match: ( - path: string | undefined, - version: string, - userFlags: UserFlags | null, - ) => boolean; - component: ({ path, release }: ReleaseNoteProps) => JSX.Element | null; -} - -const allComponentMap: ComponentMapType[] = [ - { - match: ( - path: string | undefined, - version: string, - userFlags: UserFlags | null, - ): boolean => { - if (!path) { - return false; - } - const allowedPaths: string[] = [ - ROUTES.LIST_ALL_ALERT, - ROUTES.APPLICATION, - ROUTES.ALL_DASHBOARD, - ]; - - return ( - userFlags?.ReleaseNote0120Hide !== 'Y' && - allowedPaths.includes(path) && - version.startsWith('v0.12') - ); - }, - component: ReleaseNote0120, - }, -]; - -// ReleaseNote prints release specific warnings and notes that -// user needs to be aware of before using the upgraded version. -function ReleaseNote({ path }: ReleaseNoteProps): JSX.Element | null { - const { user } = useAppContext(); - const { currentVersion } = useSelector( - (state) => state.app, - ); - - const c = allComponentMap.find((item) => - item.match(path, currentVersion, user.flags), - ); - - if (!c) { - return null; - } - - return ; -} - -ReleaseNote.defaultProps = { - path: '', -}; - -export default ReleaseNote; diff --git a/frontend/src/container/ListAlertRules/index.tsx b/frontend/src/container/ListAlertRules/index.tsx index 0efa96bacd..239c39abcc 100644 --- a/frontend/src/container/ListAlertRules/index.tsx +++ b/frontend/src/container/ListAlertRules/index.tsx @@ -1,21 +1,18 @@ import { Space } from 'antd'; import getAll from 'api/alerts/getAll'; import logEvent from 'api/common/logEvent'; -import ReleaseNote from 'components/ReleaseNote'; import Spinner from 'components/Spinner'; import { useNotifications } from 'hooks/useNotifications'; import { isUndefined } from 'lodash-es'; import { useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { useQuery } from 'react-query'; -import { useLocation } from 'react-router-dom'; import { AlertsEmptyState } from './AlertsEmptyState/AlertsEmptyState'; import ListAlert from './ListAlert'; function ListAlertRules(): JSX.Element { const { t } = useTranslation('common'); - const location = useLocation(); const { data, isError, isLoading, refetch, status } = useQuery('allAlerts', { queryFn: getAll, cacheTime: 0, @@ -70,7 +67,6 @@ function ListAlertRules(): JSX.Element { return ( - - {dayjs.unix(Number(joinedOn)).format(DATE_TIME_FORMATS.MONTH_DATE_FULL)} + {dayjs(joinedOn).format(DATE_TIME_FORMATS.MONTH_DATE_FULL)} ); }, diff --git a/frontend/src/pages/DashboardsListPage/DashboardsListPage.tsx b/frontend/src/pages/DashboardsListPage/DashboardsListPage.tsx index 0fc7dda7b3..6779fd945a 100644 --- a/frontend/src/pages/DashboardsListPage/DashboardsListPage.tsx +++ b/frontend/src/pages/DashboardsListPage/DashboardsListPage.tsx @@ -1,14 +1,10 @@ import './DashboardsListPage.styles.scss'; import { Space, Typography } from 'antd'; -import ReleaseNote from 'components/ReleaseNote'; import ListOfAllDashboard from 'container/ListOfDashboard'; import { LayoutGrid } from 'lucide-react'; -import { useLocation } from 'react-router-dom'; function DashboardsListPage(): JSX.Element { - const location = useLocation(); - return ( -
Dashboards diff --git a/frontend/src/pages/Services/index.tsx b/frontend/src/pages/Services/index.tsx index 2c2b1f5c17..242f1f2b85 100644 --- a/frontend/src/pages/Services/index.tsx +++ b/frontend/src/pages/Services/index.tsx @@ -1,15 +1,9 @@ import { Space } from 'antd'; -import ReleaseNote from 'components/ReleaseNote'; import ServicesApplication from 'container/ServiceApplication'; -import { useLocation } from 'react-router-dom'; function Metrics(): JSX.Element { - const location = useLocation(); - return ( - - ); diff --git a/frontend/src/providers/App/App.tsx b/frontend/src/providers/App/App.tsx index 0451ffbb58..3fee8de485 100644 --- a/frontend/src/providers/App/App.tsx +++ b/frontend/src/providers/App/App.tsx @@ -21,7 +21,6 @@ import { FeatureFlagProps as FeatureFlags } from 'types/api/features/getFeatures import { PayloadProps as LicensesResModel } from 'types/api/licenses/getAll'; import { LicenseV3ResModel } from 'types/api/licensesV3/getActive'; import { Organization } from 'types/api/user/getOrganization'; -import { UserFlags } from 'types/api/user/setFlags'; import { OrgPreference } from 'types/reducer/app'; import { USER_ROLES } from 'types/roles'; @@ -158,13 +157,6 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element { } }, [orgPreferencesData, isFetchingOrgPreferences]); - function setUserFlags(userflags: UserFlags): void { - setUser((prev) => ({ - ...prev, - flags: userflags, - })); - } - function updateUser(user: IUser): void { setUser((prev) => ({ ...prev, @@ -252,7 +244,6 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element { orgPreferencesFetchError, licensesRefetch, updateUser, - setUserFlags, updateOrgPreferences, updateOrg, }), diff --git a/frontend/src/providers/App/types.ts b/frontend/src/providers/App/types.ts index 0833be6d6e..efb4ccce26 100644 --- a/frontend/src/providers/App/types.ts +++ b/frontend/src/providers/App/types.ts @@ -3,7 +3,6 @@ import { PayloadProps as LicensesResModel } from 'types/api/licenses/getAll'; import { LicenseV3ResModel } from 'types/api/licensesV3/getActive'; import { Organization } from 'types/api/user/getOrganization'; import { PayloadProps as User } from 'types/api/user/getUser'; -import { UserFlags } from 'types/api/user/setFlags'; import { OrgPreference } from 'types/reducer/app'; export interface IAppContext { @@ -26,7 +25,6 @@ export interface IAppContext { orgPreferencesFetchError: unknown; licensesRefetch: () => void; updateUser: (user: IUser) => void; - setUserFlags: (flags: UserFlags) => void; updateOrgPreferences: (orgPreferences: OrgPreference[]) => void; updateOrg(orgId: string, updatedOrgName: string): void; } diff --git a/frontend/src/providers/App/utils.ts b/frontend/src/providers/App/utils.ts index 4916c2771c..798362c79c 100644 --- a/frontend/src/providers/App/utils.ts +++ b/frontend/src/providers/App/utils.ts @@ -20,7 +20,6 @@ function getUserDefaults(): IUser { name: '', profilePictureURL: '', createdAt: 0, - flags: {}, organization: '', orgId: '', role: 'VIEWER', diff --git a/frontend/src/tests/test-utils.tsx b/frontend/src/tests/test-utils.tsx index 75772eb50b..56311f7a9b 100644 --- a/frontend/src/tests/test-utils.tsx +++ b/frontend/src/tests/test-utils.tsx @@ -128,7 +128,6 @@ export function getAppContextMock( name: 'John Doe', profilePictureURL: '', createdAt: 1732544623, - flags: {}, organization: 'Nightswatch', orgId: 'does-not-matter-id', role: role as ROLES, @@ -326,7 +325,6 @@ export function getAppContextMock( orgPreferencesFetchError: null, isLoggedIn: true, updateUser: jest.fn(), - setUserFlags: jest.fn(), updateOrg: jest.fn(), updateOrgPreferences: jest.fn(), licensesRefetch: jest.fn(), diff --git a/frontend/src/types/api/user/getUser.ts b/frontend/src/types/api/user/getUser.ts index bedbbe9b3c..be88234ff3 100644 --- a/frontend/src/types/api/user/getUser.ts +++ b/frontend/src/types/api/user/getUser.ts @@ -1,4 +1,3 @@ -import { UserFlags } from 'types/api/user/setFlags'; import { User } from 'types/reducer/app'; import { ROLES } from 'types/roles'; @@ -16,6 +15,5 @@ export interface PayloadProps { profilePictureURL: string; organization: string; role: ROLES; - flags: UserFlags; groupId: string; } diff --git a/frontend/src/types/api/user/setFlags.ts b/frontend/src/types/api/user/setFlags.ts deleted file mode 100644 index 9dc2bc09aa..0000000000 --- a/frontend/src/types/api/user/setFlags.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { User } from 'types/reducer/app'; - -export interface UserFlags { - ReleaseNote0120Hide?: string; -} - -export type PayloadProps = UserFlags; - -export interface Props { - userId: User['userId']; - flags: UserFlags; -} diff --git a/pkg/query-service/app/apdex.go b/pkg/query-service/app/apdex.go index 83d76be181..df6f03ccde 100644 --- a/pkg/query-service/app/apdex.go +++ b/pkg/query-service/app/apdex.go @@ -1,21 +1,27 @@ package app import ( - "context" + "errors" "net/http" "strings" "go.signoz.io/signoz/pkg/query-service/dao" "go.signoz.io/signoz/pkg/query-service/model" + "go.signoz.io/signoz/pkg/types/authtypes" ) func (aH *APIHandler) setApdexSettings(w http.ResponseWriter, r *http.Request) { + claims, ok := authtypes.ClaimsFromContext(r.Context()) + if !ok { + RespondError(w, &model.ApiError{Err: errors.New("unauthorized"), Typ: model.ErrorUnauthorized}, nil) + return + } req, err := parseSetApdexScoreRequest(r) if aH.HandleError(w, err, http.StatusBadRequest) { return } - if err := dao.DB().SetApdexSettings(context.Background(), req); err != nil { + if err := dao.DB().SetApdexSettings(r.Context(), claims.OrgID, req); err != nil { RespondError(w, &model.ApiError{Err: err, Typ: model.ErrorInternal}, nil) return } @@ -25,7 +31,12 @@ func (aH *APIHandler) setApdexSettings(w http.ResponseWriter, r *http.Request) { func (aH *APIHandler) getApdexSettings(w http.ResponseWriter, r *http.Request) { services := r.URL.Query().Get("services") - apdexSet, err := dao.DB().GetApdexSettings(context.Background(), strings.Split(strings.TrimSpace(services), ",")) + claims, ok := authtypes.ClaimsFromContext(r.Context()) + if !ok { + RespondError(w, &model.ApiError{Err: errors.New("unauthorized"), Typ: model.ErrorUnauthorized}, nil) + return + } + apdexSet, err := dao.DB().GetApdexSettings(r.Context(), claims.OrgID, strings.Split(strings.TrimSpace(services), ",")) if err != nil { RespondError(w, &model.ApiError{Err: err, Typ: model.ErrorInternal}, nil) return diff --git a/pkg/query-service/app/auth.go b/pkg/query-service/app/auth.go index aa2529b017..30ac833ee5 100644 --- a/pkg/query-service/app/auth.go +++ b/pkg/query-service/app/auth.go @@ -9,13 +9,14 @@ import ( "go.signoz.io/signoz/pkg/query-service/auth" "go.signoz.io/signoz/pkg/query-service/constants" "go.signoz.io/signoz/pkg/query-service/model" + "go.signoz.io/signoz/pkg/types" ) type AuthMiddleware struct { - GetUserFromRequest func(r context.Context) (*model.UserPayload, error) + GetUserFromRequest func(r context.Context) (*types.GettableUser, error) } -func NewAuthMiddleware(f func(ctx context.Context) (*model.UserPayload, error)) *AuthMiddleware { +func NewAuthMiddleware(f func(ctx context.Context) (*types.GettableUser, error)) *AuthMiddleware { return &AuthMiddleware{ GetUserFromRequest: f, } diff --git a/pkg/query-service/app/http_handler.go b/pkg/query-service/app/http_handler.go index e1ea40176c..d6e293fe97 100644 --- a/pkg/query-service/app/http_handler.go +++ b/pkg/query-service/app/http_handler.go @@ -6,7 +6,6 @@ import ( "encoding/json" "errors" "fmt" - "go.signoz.io/signoz/pkg/query-service/app/metricsexplorer" "io" "math" "net/http" @@ -19,6 +18,8 @@ import ( "text/template" "time" + "go.signoz.io/signoz/pkg/query-service/app/metricsexplorer" + "github.com/gorilla/mux" "github.com/gorilla/websocket" jsoniter "github.com/json-iterator/go" @@ -51,6 +52,7 @@ import ( "go.signoz.io/signoz/pkg/query-service/contextlinks" v3 "go.signoz.io/signoz/pkg/query-service/model/v3" "go.signoz.io/signoz/pkg/query-service/postprocess" + "go.signoz.io/signoz/pkg/types" "go.signoz.io/signoz/pkg/types/authtypes" "go.uber.org/zap" @@ -597,8 +599,6 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *AuthMiddleware) { router.HandleFunc("/api/v1/user/{id}", am.SelfAccess(aH.editUser)).Methods(http.MethodPut) router.HandleFunc("/api/v1/user/{id}", am.AdminAccess(aH.deleteUser)).Methods(http.MethodDelete) - router.HandleFunc("/api/v1/user/{id}/flags", am.SelfAccess(aH.patchUserFlag)).Methods(http.MethodPatch) - router.HandleFunc("/api/v1/rbac/role/{id}", am.SelfAccess(aH.getRole)).Methods(http.MethodGet) router.HandleFunc("/api/v1/rbac/role/{id}", am.AdminAccess(aH.editRole)).Methods(http.MethodPut) @@ -2108,8 +2108,13 @@ func (aH *APIHandler) revokeInvite(w http.ResponseWriter, r *http.Request) { // listPendingInvites is used to list the pending invites. func (aH *APIHandler) listPendingInvites(w http.ResponseWriter, r *http.Request) { - ctx := context.Background() - invites, err := dao.DB().GetInvites(ctx) + ctx := r.Context() + claims, ok := authtypes.ClaimsFromContext(ctx) + if !ok { + RespondError(w, &model.ApiError{Err: errors.New("failed to get org id from context"), Typ: model.ErrorInternal}, nil) + return + } + invites, err := dao.DB().GetInvites(ctx, claims.OrgID) if err != nil { RespondError(w, err, nil) return @@ -2120,7 +2125,7 @@ func (aH *APIHandler) listPendingInvites(w http.ResponseWriter, r *http.Request) var resp []*model.InvitationResponseObject for _, inv := range invites { - org, apiErr := dao.DB().GetOrg(ctx, inv.OrgId) + org, apiErr := dao.DB().GetOrg(ctx, inv.OrgID) if apiErr != nil { RespondError(w, apiErr, nil) } @@ -2128,7 +2133,7 @@ func (aH *APIHandler) listPendingInvites(w http.ResponseWriter, r *http.Request) Name: inv.Name, Email: inv.Email, Token: inv.Token, - CreatedAt: inv.CreatedAt, + CreatedAt: inv.CreatedAt.Unix(), Role: inv.Role, Organization: org.Name, }) @@ -2271,13 +2276,15 @@ func (aH *APIHandler) editUser(w http.ResponseWriter, r *http.Request) { old.ProfilePictureURL = update.ProfilePictureURL } - _, apiErr = dao.DB().EditUser(ctx, &model.User{ - Id: old.Id, - Name: old.Name, - OrgId: old.OrgId, - Email: old.Email, - Password: old.Password, - CreatedAt: old.CreatedAt, + _, apiErr = dao.DB().EditUser(ctx, &types.User{ + ID: old.ID, + Name: old.Name, + OrgID: old.OrgID, + Email: old.Email, + Password: old.Password, + TimeAuditable: types.TimeAuditable{ + CreatedAt: old.CreatedAt, + }, ProfilePictureURL: old.ProfilePictureURL, }) if apiErr != nil { @@ -2319,7 +2326,7 @@ func (aH *APIHandler) deleteUser(w http.ResponseWriter, r *http.Request) { return } - if user.GroupId == adminGroup.ID && len(adminUsers) == 1 { + if user.GroupID == adminGroup.ID && len(adminUsers) == 1 { RespondError(w, &model.ApiError{ Typ: model.ErrorInternal, Err: errors.New("cannot delete the last admin user")}, nil) @@ -2334,37 +2341,6 @@ func (aH *APIHandler) deleteUser(w http.ResponseWriter, r *http.Request) { aH.WriteJSON(w, r, map[string]string{"data": "user deleted successfully"}) } -// addUserFlag patches a user flags with the changes -func (aH *APIHandler) patchUserFlag(w http.ResponseWriter, r *http.Request) { - // read user id from path var - userId := mux.Vars(r)["id"] - - // read input into user flag - defer r.Body.Close() - b, err := io.ReadAll(r.Body) - if err != nil { - zap.L().Error("failed read user flags from http request for userId ", zap.String("userId", userId), zap.Error(err)) - RespondError(w, model.BadRequestStr("received user flags in invalid format"), nil) - return - } - flags := make(map[string]string, 0) - - err = json.Unmarshal(b, &flags) - if err != nil { - zap.L().Error("failed parsing user flags for userId ", zap.String("userId", userId), zap.Error(err)) - RespondError(w, model.BadRequestStr("received user flags in invalid format"), nil) - return - } - - newflags, apiError := dao.DB().UpdateUserFlags(r.Context(), userId, flags) - if !apiError.IsNil() { - RespondError(w, apiError, nil) - return - } - - aH.Respond(w, newflags) -} - func (aH *APIHandler) getRole(w http.ResponseWriter, r *http.Request) { id := mux.Vars(r)["id"] @@ -2380,7 +2356,7 @@ func (aH *APIHandler) getRole(w http.ResponseWriter, r *http.Request) { }, nil) return } - group, err := dao.DB().GetGroup(context.Background(), user.GroupId) + group, err := dao.DB().GetGroup(context.Background(), user.GroupID) if err != nil { RespondError(w, err, "Failed to get group") return @@ -2416,7 +2392,7 @@ func (aH *APIHandler) editRole(w http.ResponseWriter, r *http.Request) { } // Make sure that the request is not demoting the last admin user. - if user.GroupId == auth.AuthCacheObj.AdminGroupId { + if user.GroupID == auth.AuthCacheObj.AdminGroupId { adminUsers, apiErr := dao.DB().GetUsersByGroup(ctx, auth.AuthCacheObj.AdminGroupId) if apiErr != nil { RespondError(w, apiErr, "Failed to fetch adminUsers") @@ -2431,7 +2407,7 @@ func (aH *APIHandler) editRole(w http.ResponseWriter, r *http.Request) { } } - apiErr = dao.DB().UpdateUserGroup(context.Background(), user.Id, newGroup.ID) + apiErr = dao.DB().UpdateUserGroup(context.Background(), user.ID, newGroup.ID) if apiErr != nil { RespondError(w, apiErr, "Failed to add user to group") return @@ -2465,7 +2441,7 @@ func (aH *APIHandler) editOrg(w http.ResponseWriter, r *http.Request) { return } - req.Id = id + req.ID = id if apiErr := dao.DB().EditOrg(context.Background(), req); apiErr != nil { RespondError(w, apiErr, "Failed to update org in the DB") return @@ -3528,7 +3504,7 @@ func (aH *APIHandler) getUserPreference( user := common.GetUserFromContext(r.Context()) preference, apiErr := preferences.GetUserPreference( - r.Context(), preferenceId, user.User.OrgId, user.User.Id, + r.Context(), preferenceId, user.User.OrgID, user.User.ID, ) if apiErr != nil { RespondError(w, apiErr, nil) @@ -3551,7 +3527,7 @@ func (aH *APIHandler) updateUserPreference( RespondError(w, model.BadRequest(err), nil) return } - preference, apiErr := preferences.UpdateUserPreference(r.Context(), preferenceId, req.PreferenceValue, user.User.Id) + preference, apiErr := preferences.UpdateUserPreference(r.Context(), preferenceId, req.PreferenceValue, user.User.ID) if apiErr != nil { RespondError(w, apiErr, nil) return @@ -3565,7 +3541,7 @@ func (aH *APIHandler) getAllUserPreferences( ) { user := common.GetUserFromContext(r.Context()) preference, apiErr := preferences.GetAllUserPreferences( - r.Context(), user.User.OrgId, user.User.Id, + r.Context(), user.User.OrgID, user.User.ID, ) if apiErr != nil { RespondError(w, apiErr, nil) @@ -3581,7 +3557,7 @@ func (aH *APIHandler) getOrgPreference( preferenceId := mux.Vars(r)["preferenceId"] user := common.GetUserFromContext(r.Context()) preference, apiErr := preferences.GetOrgPreference( - r.Context(), preferenceId, user.User.OrgId, + r.Context(), preferenceId, user.User.OrgID, ) if apiErr != nil { RespondError(w, apiErr, nil) @@ -3604,7 +3580,7 @@ func (aH *APIHandler) updateOrgPreference( RespondError(w, model.BadRequest(err), nil) return } - preference, apiErr := preferences.UpdateOrgPreference(r.Context(), preferenceId, req.PreferenceValue, user.User.OrgId) + preference, apiErr := preferences.UpdateOrgPreference(r.Context(), preferenceId, req.PreferenceValue, user.User.OrgID) if apiErr != nil { RespondError(w, apiErr, nil) return @@ -3618,7 +3594,7 @@ func (aH *APIHandler) getAllOrgPreferences( ) { user := common.GetUserFromContext(r.Context()) preference, apiErr := preferences.GetAllOrgPreferences( - r.Context(), user.User.OrgId, + r.Context(), user.User.OrgID, ) if apiErr != nil { RespondError(w, apiErr, nil) diff --git a/pkg/query-service/app/parser.go b/pkg/query-service/app/parser.go index d1b6f2f23f..936a38a798 100644 --- a/pkg/query-service/app/parser.go +++ b/pkg/query-service/app/parser.go @@ -6,9 +6,6 @@ import ( "encoding/json" "errors" "fmt" - "go.signoz.io/signoz/pkg/query-service/app/integrations/messagingQueues/kafka" - queues2 "go.signoz.io/signoz/pkg/query-service/app/integrations/messagingQueues/queues" - "go.signoz.io/signoz/pkg/query-service/app/integrations/thirdPartyApi" "math" "net/http" "strconv" @@ -16,6 +13,10 @@ import ( "text/template" "time" + "go.signoz.io/signoz/pkg/query-service/app/integrations/messagingQueues/kafka" + queues2 "go.signoz.io/signoz/pkg/query-service/app/integrations/messagingQueues/queues" + "go.signoz.io/signoz/pkg/query-service/app/integrations/thirdPartyApi" + "github.com/SigNoz/govaluate" "github.com/gorilla/mux" promModel "github.com/prometheus/common/model" @@ -32,6 +33,7 @@ import ( "go.signoz.io/signoz/pkg/query-service/postprocess" "go.signoz.io/signoz/pkg/query-service/utils" querytemplate "go.signoz.io/signoz/pkg/query-service/utils/queryTemplate" + "go.signoz.io/signoz/pkg/types" ) var allowedFunctions = []string{"count", "ratePerSec", "sum", "avg", "min", "max", "p50", "p90", "p95", "p99"} @@ -465,8 +467,8 @@ func parseGetTTL(r *http.Request) (*model.GetTTLParams, error) { return &model.GetTTLParams{Type: typeTTL}, nil } -func parseUserRequest(r *http.Request) (*model.User, error) { - var req model.User +func parseUserRequest(r *http.Request) (*types.User, error) { + var req types.User if err := json.NewDecoder(r.Body).Decode(&req); err != nil { return nil, err } @@ -519,8 +521,8 @@ func parseInviteUsersRequest(r *http.Request) (*model.BulkInviteRequest, error) return &req, nil } -func parseSetApdexScoreRequest(r *http.Request) (*model.ApdexSettings, error) { - var req model.ApdexSettings +func parseSetApdexScoreRequest(r *http.Request) (*types.ApdexSettings, error) { + var req types.ApdexSettings if err := json.NewDecoder(r.Body).Decode(&req); err != nil { return nil, err } @@ -566,8 +568,8 @@ func parseUserRoleRequest(r *http.Request) (*model.UserRole, error) { return &req, nil } -func parseEditOrgRequest(r *http.Request) (*model.Organization, error) { - var req model.Organization +func parseEditOrgRequest(r *http.Request) (*types.Organization, error) { + var req types.Organization if err := json.NewDecoder(r.Body).Decode(&req); err != nil { return nil, err } diff --git a/pkg/query-service/app/server.go b/pkg/query-service/app/server.go index b2e6a5ddf0..69c8f4b9ff 100644 --- a/pkg/query-service/app/server.go +++ b/pkg/query-service/app/server.go @@ -25,6 +25,7 @@ import ( opAmpModel "go.signoz.io/signoz/pkg/query-service/app/opamp/model" "go.signoz.io/signoz/pkg/query-service/app/preferences" "go.signoz.io/signoz/pkg/signoz" + "go.signoz.io/signoz/pkg/types" "go.signoz.io/signoz/pkg/types/authtypes" "go.signoz.io/signoz/pkg/web" @@ -291,14 +292,14 @@ func (s *Server) createPublicServer(api *APIHandler, web web.Web) (*http.Server, r.Use(middleware.NewLogging(zap.L(), s.serverOptions.Config.APIServer.Logging.ExcludedRoutes).Wrap) // add auth middleware - getUserFromRequest := func(ctx context.Context) (*model.UserPayload, error) { + getUserFromRequest := func(ctx context.Context) (*types.GettableUser, error) { user, err := auth.GetUserFromReqContext(ctx) if err != nil { return nil, err } - if user.User.OrgId == "" { + if user.User.OrgID == "" { return nil, model.UnauthorizedError(errors.New("orgId is missing in the claims")) } diff --git a/pkg/query-service/auth/auth.go b/pkg/query-service/auth/auth.go index 4ca00707ea..69e5415e81 100644 --- a/pkg/query-service/auth/auth.go +++ b/pkg/query-service/auth/auth.go @@ -17,6 +17,7 @@ import ( "go.signoz.io/signoz/pkg/query-service/telemetry" "go.signoz.io/signoz/pkg/query-service/utils" smtpservice "go.signoz.io/signoz/pkg/query-service/utils/smtpService" + "go.signoz.io/signoz/pkg/types" "go.signoz.io/signoz/pkg/types/authtypes" "go.uber.org/zap" "golang.org/x/crypto/bcrypt" @@ -61,6 +62,10 @@ func Invite(ctx context.Context, req *model.InviteRequest) (*model.InviteRespons return nil, errors.New("User already exists with the same email") } + claims, ok := authtypes.ClaimsFromContext(ctx) + if !ok { + return nil, errors.New("failed to extract OrgID from context") + } // Check if an invite already exists invite, apiErr := dao.DB().GetInviteFromEmail(ctx, req.Email) if apiErr != nil { @@ -75,23 +80,18 @@ func Invite(ctx context.Context, req *model.InviteRequest) (*model.InviteRespons return nil, errors.Wrap(err, "invalid invite request") } - claims, ok := authtypes.ClaimsFromContext(ctx) - if !ok { - return nil, errors.Wrap(err, "failed to extract admin user id") - } - au, apiErr := dao.DB().GetUser(ctx, claims.UserID) if apiErr != nil { return nil, errors.Wrap(err, "failed to query admin user from the DB") } - inv := &model.InvitationObject{ + inv := &types.Invite{ Name: req.Name, Email: req.Email, Token: token, - CreatedAt: time.Now().Unix(), + CreatedAt: time.Now(), Role: req.Role, - OrgId: au.OrgId, + OrgID: au.OrgID, } if err := dao.DB().CreateInviteEntry(ctx, inv); err != nil { @@ -157,7 +157,7 @@ func InviteUsers(ctx context.Context, req *model.BulkInviteRequest) (*model.Bulk } // Helper function to handle individual invites -func inviteUser(ctx context.Context, req *model.InviteRequest, au *model.UserPayload) (*model.InviteResponse, error) { +func inviteUser(ctx context.Context, req *model.InviteRequest, au *types.GettableUser) (*model.InviteResponse, error) { token, err := utils.RandomHex(opaqueTokenSize) if err != nil { return nil, errors.Wrap(err, "failed to generate invite token") @@ -186,13 +186,13 @@ func inviteUser(ctx context.Context, req *model.InviteRequest, au *model.UserPay return nil, errors.Wrap(err, "invalid invite request") } - inv := &model.InvitationObject{ + inv := &types.Invite{ Name: req.Name, Email: req.Email, Token: token, - CreatedAt: time.Now().Unix(), + CreatedAt: time.Now(), Role: req.Role, - OrgId: au.OrgId, + OrgID: au.OrgID, } if err := dao.DB().CreateInviteEntry(ctx, inv); err != nil { @@ -211,7 +211,7 @@ func inviteUser(ctx context.Context, req *model.InviteRequest, au *model.UserPay return &model.InviteResponse{Email: inv.Email, InviteToken: inv.Token}, nil } -func inviteEmail(req *model.InviteRequest, au *model.UserPayload, token string) { +func inviteEmail(req *model.InviteRequest, au *types.GettableUser, token string) { smtp := smtpservice.GetInstance() data := InviteEmailData{ CustomerName: req.Name, @@ -251,7 +251,12 @@ func RevokeInvite(ctx context.Context, email string) error { return ErrorInvalidInviteToken } - if err := dao.DB().DeleteInvitation(ctx, email); err != nil { + claims, ok := authtypes.ClaimsFromContext(ctx) + if !ok { + return errors.New("failed to org id from context") + } + + if err := dao.DB().DeleteInvitation(ctx, claims.OrgID, email); err != nil { return errors.Wrap(err.Err, "failed to write to DB") } return nil @@ -272,7 +277,7 @@ func GetInvite(ctx context.Context, token string) (*model.InvitationResponseObje // TODO(Ahsan): This is not the best way to add org name in the invite response. We should // either include org name in the invite table or do a join query. - org, apiErr := dao.DB().GetOrg(ctx, inv.OrgId) + org, apiErr := dao.DB().GetOrg(ctx, inv.OrgID) if apiErr != nil { return nil, errors.Wrap(apiErr.Err, "failed to query the DB") } @@ -280,13 +285,13 @@ func GetInvite(ctx context.Context, token string) (*model.InvitationResponseObje Name: inv.Name, Email: inv.Email, Token: inv.Token, - CreatedAt: inv.CreatedAt, + CreatedAt: inv.CreatedAt.Unix(), Role: inv.Role, Organization: org.Name, }, nil } -func ValidateInvite(ctx context.Context, req *RegisterRequest) (*model.InvitationObject, error) { +func ValidateInvite(ctx context.Context, req *RegisterRequest) (*types.Invite, error) { invitation, err := dao.DB().GetInviteFromEmail(ctx, req.Email) if err != nil { return nil, errors.Wrap(err.Err, "Failed to read from DB") @@ -303,14 +308,14 @@ func ValidateInvite(ctx context.Context, req *RegisterRequest) (*model.Invitatio return invitation, nil } -func CreateResetPasswordToken(ctx context.Context, userId string) (*model.ResetPasswordEntry, error) { +func CreateResetPasswordToken(ctx context.Context, userId string) (*types.ResetPasswordRequest, error) { token, err := utils.RandomHex(opaqueTokenSize) if err != nil { return nil, errors.Wrap(err, "failed to generate reset password token") } - req := &model.ResetPasswordEntry{ - UserId: userId, + req := &types.ResetPasswordRequest{ + UserID: userId, Token: token, } if apiErr := dao.DB().CreateResetPasswordEntry(ctx, req); err != nil { @@ -334,7 +339,7 @@ func ResetPassword(ctx context.Context, req *model.ResetPasswordRequest) error { return errors.Wrap(err, "Failed to generate password hash") } - if apiErr := dao.DB().UpdateUserPassword(ctx, hash, entry.UserId); apiErr != nil { + if apiErr := dao.DB().UpdateUserPassword(ctx, hash, entry.UserID); apiErr != nil { return apiErr.Err } @@ -360,7 +365,7 @@ func ChangePassword(ctx context.Context, req *model.ChangePasswordRequest) *mode return model.InternalError(errors.New("Failed to generate password hash")) } - if apiErr := dao.DB().UpdateUserPassword(ctx, hash, user.Id); apiErr != nil { + if apiErr := dao.DB().UpdateUserPassword(ctx, hash, user.ID); apiErr != nil { return apiErr } @@ -369,6 +374,7 @@ func ChangePassword(ctx context.Context, req *model.ChangePasswordRequest) *mode type RegisterRequest struct { Name string `json:"name"` + OrgID string `json:"orgId"` OrgName string `json:"orgName"` Email string `json:"email"` Password string `json:"password"` @@ -380,7 +386,7 @@ type RegisterRequest struct { SourceUrl string `json:"sourceUrl"` } -func RegisterFirstUser(ctx context.Context, req *RegisterRequest) (*model.User, *model.ApiError) { +func RegisterFirstUser(ctx context.Context, req *RegisterRequest) (*types.User, *model.ApiError) { if req.Email == "" { return nil, model.BadRequest(model.ErrEmailRequired{}) @@ -392,8 +398,9 @@ func RegisterFirstUser(ctx context.Context, req *RegisterRequest) (*model.User, groupName := constants.AdminGroup + // modify this to use bun org, apierr := dao.DB().CreateOrg(ctx, - &model.Organization{Name: req.OrgName, IsAnonymous: req.IsAnonymous, HasOptedUpdates: req.HasOptedUpdates}) + &types.Organization{Name: req.OrgName, IsAnonymous: req.IsAnonymous, HasOptedUpdates: req.HasOptedUpdates}) if apierr != nil { zap.L().Error("CreateOrg failed", zap.Error(apierr.ToError())) return nil, apierr @@ -414,22 +421,24 @@ func RegisterFirstUser(ctx context.Context, req *RegisterRequest) (*model.User, return nil, model.InternalError(model.ErrSignupFailed{}) } - user := &model.User{ - Id: uuid.NewString(), - Name: req.Name, - Email: req.Email, - Password: hash, - CreatedAt: time.Now().Unix(), + user := &types.User{ + ID: uuid.NewString(), + Name: req.Name, + Email: req.Email, + Password: hash, + TimeAuditable: types.TimeAuditable{ + CreatedAt: time.Now(), + }, ProfilePictureURL: "", // Currently unused - GroupId: group.ID, - OrgId: org.Id, + GroupID: group.ID, + OrgID: org.ID, } return dao.DB().CreateUser(ctx, user, true) } // RegisterInvitedUser handles registering a invited user -func RegisterInvitedUser(ctx context.Context, req *RegisterRequest, nopassword bool) (*model.User, *model.ApiError) { +func RegisterInvitedUser(ctx context.Context, req *RegisterRequest, nopassword bool) (*types.User, *model.ApiError) { if req.InviteToken == "" { return nil, model.BadRequest(ErrorAskAdmin) @@ -459,7 +468,7 @@ func RegisterInvitedUser(ctx context.Context, req *RegisterRequest, nopassword b return &userPayload.User, nil } - if invite.OrgId == "" { + if invite.OrgID == "" { zap.L().Error("failed to find org in the invite") return nil, model.InternalError(fmt.Errorf("invalid invite, org not found")) } @@ -492,15 +501,17 @@ func RegisterInvitedUser(ctx context.Context, req *RegisterRequest, nopassword b } } - user := &model.User{ - Id: uuid.NewString(), - Name: req.Name, - Email: req.Email, - Password: hash, - CreatedAt: time.Now().Unix(), + user := &types.User{ + ID: uuid.NewString(), + Name: req.Name, + Email: req.Email, + Password: hash, + TimeAuditable: types.TimeAuditable{ + CreatedAt: time.Now(), + }, ProfilePictureURL: "", // Currently unused - GroupId: group.ID, - OrgId: invite.OrgId, + GroupID: group.ID, + OrgID: invite.OrgID, } // TODO(Ahsan): Ideally create user and delete invitation should happen in a txn. @@ -510,7 +521,7 @@ func RegisterInvitedUser(ctx context.Context, req *RegisterRequest, nopassword b return nil, apiErr } - apiErr = dao.DB().DeleteInvitation(ctx, user.Email) + apiErr = dao.DB().DeleteInvitation(ctx, user.OrgID, user.Email) if apiErr != nil { zap.L().Error("delete invitation failed", zap.Error(apiErr.Err)) return nil, apiErr @@ -525,7 +536,7 @@ func RegisterInvitedUser(ctx context.Context, req *RegisterRequest, nopassword b // Register registers a new user. For the first register request, it doesn't need an invite token // and also the first registration is an enforced ADMIN registration. Every subsequent request will // need an invite token to go through. -func Register(ctx context.Context, req *RegisterRequest) (*model.User, *model.ApiError) { +func Register(ctx context.Context, req *RegisterRequest) (*types.User, *model.ApiError) { users, err := dao.DB().GetUsers(ctx) if err != nil { return nil, model.InternalError(fmt.Errorf("failed to get user count")) @@ -562,24 +573,24 @@ func Login(ctx context.Context, request *model.LoginRequest, jwt *authtypes.JWT) return &model.LoginResponse{ UserJwtObject: userjwt, - UserId: user.User.Id, + UserId: user.User.ID, }, nil } -func claimsToUserPayload(claims authtypes.Claims) (*model.UserPayload, error) { - user := &model.UserPayload{ - User: model.User{ - Id: claims.UserID, - GroupId: claims.GroupID, +func claimsToUserPayload(claims authtypes.Claims) (*types.GettableUser, error) { + user := &types.GettableUser{ + User: types.User{ + ID: claims.UserID, + GroupID: claims.GroupID, Email: claims.Email, - OrgId: claims.OrgID, + OrgID: claims.OrgID, }, } return user, nil } // authenticateLogin is responsible for querying the DB and validating the credentials. -func authenticateLogin(ctx context.Context, req *model.LoginRequest, jwt *authtypes.JWT) (*model.UserPayload, error) { +func authenticateLogin(ctx context.Context, req *model.LoginRequest, jwt *authtypes.JWT) (*types.GettableUser, error) { // If refresh token is valid, then simply authorize the login request. if len(req.RefreshToken) > 0 { // parse the refresh token @@ -624,17 +635,17 @@ func passwordMatch(hash, password string) bool { return err == nil } -func GenerateJWTForUser(user *model.User, jwt *authtypes.JWT) (model.UserJwtObject, error) { +func GenerateJWTForUser(user *types.User, jwt *authtypes.JWT) (model.UserJwtObject, error) { j := model.UserJwtObject{} var err error j.AccessJwtExpiry = time.Now().Add(jwt.JwtExpiry).Unix() - j.AccessJwt, err = jwt.AccessToken(user.OrgId, user.Id, user.GroupId, user.Email) + j.AccessJwt, err = jwt.AccessToken(user.OrgID, user.ID, user.GroupID, user.Email) if err != nil { return j, errors.Errorf("failed to encode jwt: %v", err) } j.RefreshJwtExpiry = time.Now().Add(jwt.JwtRefresh).Unix() - j.RefreshJwt, err = jwt.RefreshToken(user.OrgId, user.Id, user.GroupId, user.Email) + j.RefreshJwt, err = jwt.RefreshToken(user.OrgID, user.ID, user.GroupID, user.Email) if err != nil { return j, errors.Errorf("failed to encode jwt: %v", err) } diff --git a/pkg/query-service/auth/rbac.go b/pkg/query-service/auth/rbac.go index 9575c0d7f9..b1aaad1f3e 100644 --- a/pkg/query-service/auth/rbac.go +++ b/pkg/query-service/auth/rbac.go @@ -6,7 +6,7 @@ import ( "github.com/pkg/errors" "go.signoz.io/signoz/pkg/query-service/constants" "go.signoz.io/signoz/pkg/query-service/dao" - "go.signoz.io/signoz/pkg/query-service/model" + "go.signoz.io/signoz/pkg/types" "go.signoz.io/signoz/pkg/types/authtypes" ) @@ -48,28 +48,28 @@ func InitAuthCache(ctx context.Context) error { return nil } -func GetUserFromReqContext(ctx context.Context) (*model.UserPayload, error) { +func GetUserFromReqContext(ctx context.Context) (*types.GettableUser, error) { claims, ok := authtypes.ClaimsFromContext(ctx) if !ok { return nil, errors.New("no claims found in context") } - user := &model.UserPayload{ - User: model.User{ - Id: claims.UserID, - GroupId: claims.GroupID, + user := &types.GettableUser{ + User: types.User{ + ID: claims.UserID, + GroupID: claims.GroupID, Email: claims.Email, - OrgId: claims.OrgID, + OrgID: claims.OrgID, }, } return user, nil } -func IsSelfAccessRequest(user *model.UserPayload, id string) bool { return user.Id == id } +func IsSelfAccessRequest(user *types.GettableUser, id string) bool { return user.ID == id } -func IsViewer(user *model.UserPayload) bool { return user.GroupId == AuthCacheObj.ViewerGroupId } -func IsEditor(user *model.UserPayload) bool { return user.GroupId == AuthCacheObj.EditorGroupId } -func IsAdmin(user *model.UserPayload) bool { return user.GroupId == AuthCacheObj.AdminGroupId } +func IsViewer(user *types.GettableUser) bool { return user.GroupID == AuthCacheObj.ViewerGroupId } +func IsEditor(user *types.GettableUser) bool { return user.GroupID == AuthCacheObj.EditorGroupId } +func IsAdmin(user *types.GettableUser) bool { return user.GroupID == AuthCacheObj.AdminGroupId } func ValidatePassword(password string) error { if len(password) < minimumPasswordLength { diff --git a/pkg/query-service/common/user.go b/pkg/query-service/common/user.go index ecfc519fc0..a7ce56f9af 100644 --- a/pkg/query-service/common/user.go +++ b/pkg/query-service/common/user.go @@ -4,11 +4,11 @@ import ( "context" "go.signoz.io/signoz/pkg/query-service/constants" - "go.signoz.io/signoz/pkg/query-service/model" + "go.signoz.io/signoz/pkg/types" ) -func GetUserFromContext(ctx context.Context) *model.UserPayload { - user, ok := ctx.Value(constants.ContextUserKey).(*model.UserPayload) +func GetUserFromContext(ctx context.Context) *types.GettableUser { + user, ok := ctx.Value(constants.ContextUserKey).(*types.GettableUser) if !ok { return nil } diff --git a/pkg/query-service/dao/interface.go b/pkg/query-service/dao/interface.go index 68fdfb0088..1349ff79fc 100644 --- a/pkg/query-service/dao/interface.go +++ b/pkg/query-service/dao/interface.go @@ -13,28 +13,28 @@ type ModelDao interface { } type Queries interface { - GetInviteFromEmail(ctx context.Context, email string) (*model.InvitationObject, *model.ApiError) - GetInviteFromToken(ctx context.Context, token string) (*model.InvitationObject, *model.ApiError) - GetInvites(ctx context.Context) ([]model.InvitationObject, *model.ApiError) + GetInviteFromEmail(ctx context.Context, email string) (*types.Invite, *model.ApiError) + GetInviteFromToken(ctx context.Context, token string) (*types.Invite, *model.ApiError) + GetInvites(ctx context.Context, orgID string) ([]types.Invite, *model.ApiError) - GetUser(ctx context.Context, id string) (*model.UserPayload, *model.ApiError) - GetUserByEmail(ctx context.Context, email string) (*model.UserPayload, *model.ApiError) - GetUsers(ctx context.Context) ([]model.UserPayload, *model.ApiError) - GetUsersWithOpts(ctx context.Context, limit int) ([]model.UserPayload, *model.ApiError) + GetUser(ctx context.Context, id string) (*types.GettableUser, *model.ApiError) + GetUserByEmail(ctx context.Context, email string) (*types.GettableUser, *model.ApiError) + GetUsers(ctx context.Context) ([]types.GettableUser, *model.ApiError) + GetUsersWithOpts(ctx context.Context, limit int) ([]types.GettableUser, *model.ApiError) - GetGroup(ctx context.Context, id string) (*model.Group, *model.ApiError) + GetGroup(ctx context.Context, id string) (*types.Group, *model.ApiError) GetGroupByName(ctx context.Context, name string) (*types.Group, *model.ApiError) - GetGroups(ctx context.Context) ([]model.Group, *model.ApiError) + GetGroups(ctx context.Context) ([]types.Group, *model.ApiError) - GetOrgs(ctx context.Context) ([]model.Organization, *model.ApiError) - GetOrgByName(ctx context.Context, name string) (*model.Organization, *model.ApiError) - GetOrg(ctx context.Context, id string) (*model.Organization, *model.ApiError) + GetOrgs(ctx context.Context) ([]types.Organization, *model.ApiError) + GetOrgByName(ctx context.Context, name string) (*types.Organization, *model.ApiError) + GetOrg(ctx context.Context, id string) (*types.Organization, *model.ApiError) - GetResetPasswordEntry(ctx context.Context, token string) (*model.ResetPasswordEntry, *model.ApiError) - GetUsersByOrg(ctx context.Context, orgId string) ([]model.UserPayload, *model.ApiError) - GetUsersByGroup(ctx context.Context, groupId string) ([]model.UserPayload, *model.ApiError) + GetResetPasswordEntry(ctx context.Context, token string) (*types.ResetPasswordRequest, *model.ApiError) + GetUsersByOrg(ctx context.Context, orgId string) ([]types.GettableUser, *model.ApiError) + GetUsersByGroup(ctx context.Context, groupId string) ([]types.GettableUser, *model.ApiError) - GetApdexSettings(ctx context.Context, services []string) ([]model.ApdexSettings, *model.ApiError) + GetApdexSettings(ctx context.Context, orgID string, services []string) ([]types.ApdexSettings, *model.ApiError) GetIngestionKeys(ctx context.Context) ([]model.IngestionKey, *model.ApiError) @@ -42,29 +42,27 @@ type Queries interface { } type Mutations interface { - CreateInviteEntry(ctx context.Context, req *model.InvitationObject) *model.ApiError - DeleteInvitation(ctx context.Context, email string) *model.ApiError + CreateInviteEntry(ctx context.Context, req *types.Invite) *model.ApiError + DeleteInvitation(ctx context.Context, orgID string, email string) *model.ApiError - CreateUser(ctx context.Context, user *model.User, isFirstUser bool) (*model.User, *model.ApiError) - EditUser(ctx context.Context, update *model.User) (*model.User, *model.ApiError) + CreateUser(ctx context.Context, user *types.User, isFirstUser bool) (*types.User, *model.ApiError) + EditUser(ctx context.Context, update *types.User) (*types.User, *model.ApiError) DeleteUser(ctx context.Context, id string) *model.ApiError - UpdateUserFlags(ctx context.Context, userId string, flags map[string]string) (model.UserFlag, *model.ApiError) - CreateGroup(ctx context.Context, group *types.Group) (*types.Group, *model.ApiError) DeleteGroup(ctx context.Context, id string) *model.ApiError - CreateOrg(ctx context.Context, org *model.Organization) (*model.Organization, *model.ApiError) - EditOrg(ctx context.Context, org *model.Organization) *model.ApiError + CreateOrg(ctx context.Context, org *types.Organization) (*types.Organization, *model.ApiError) + EditOrg(ctx context.Context, org *types.Organization) *model.ApiError DeleteOrg(ctx context.Context, id string) *model.ApiError - CreateResetPasswordEntry(ctx context.Context, req *model.ResetPasswordEntry) *model.ApiError + CreateResetPasswordEntry(ctx context.Context, req *types.ResetPasswordRequest) *model.ApiError DeleteResetPasswordEntry(ctx context.Context, token string) *model.ApiError UpdateUserPassword(ctx context.Context, hash, userId string) *model.ApiError UpdateUserGroup(ctx context.Context, userId, groupId string) *model.ApiError - SetApdexSettings(ctx context.Context, set *model.ApdexSettings) *model.ApiError + SetApdexSettings(ctx context.Context, orgID string, set *types.ApdexSettings) *model.ApiError InsertIngestionKey(ctx context.Context, ingestionKey *model.IngestionKey) *model.ApiError } diff --git a/pkg/query-service/dao/sqlite/apdex.go b/pkg/query-service/dao/sqlite/apdex.go index 63280d8fb6..610133ae34 100644 --- a/pkg/query-service/dao/sqlite/apdex.go +++ b/pkg/query-service/dao/sqlite/apdex.go @@ -3,24 +3,21 @@ package sqlite import ( "context" - "github.com/jmoiron/sqlx" + "github.com/uptrace/bun" "go.signoz.io/signoz/pkg/query-service/model" + "go.signoz.io/signoz/pkg/types" ) const defaultApdexThreshold = 0.5 -func (mds *ModelDaoSqlite) GetApdexSettings(ctx context.Context, services []string) ([]model.ApdexSettings, *model.ApiError) { - var apdexSettings []model.ApdexSettings +func (mds *ModelDaoSqlite) GetApdexSettings(ctx context.Context, orgID string, services []string) ([]types.ApdexSettings, *model.ApiError) { + var apdexSettings []types.ApdexSettings - query, args, err := sqlx.In("SELECT * FROM apdex_settings WHERE service_name IN (?)", services) - if err != nil { - return nil, &model.ApiError{ - Err: err, - } - } - query = mds.db.Rebind(query) - - err = mds.db.Select(&apdexSettings, query, args...) + err := mds.bundb.NewSelect(). + Model(&apdexSettings). + Where("org_id = ?", orgID). + Where("service_name IN (?)", bun.In(services)). + Scan(ctx) if err != nil { return nil, &model.ApiError{ Err: err, @@ -38,7 +35,7 @@ func (mds *ModelDaoSqlite) GetApdexSettings(ctx context.Context, services []stri } if !found { - apdexSettings = append(apdexSettings, model.ApdexSettings{ + apdexSettings = append(apdexSettings, types.ApdexSettings{ ServiceName: service, Threshold: defaultApdexThreshold, }) @@ -48,18 +45,16 @@ func (mds *ModelDaoSqlite) GetApdexSettings(ctx context.Context, services []stri return apdexSettings, nil } -func (mds *ModelDaoSqlite) SetApdexSettings(ctx context.Context, apdexSettings *model.ApdexSettings) *model.ApiError { +func (mds *ModelDaoSqlite) SetApdexSettings(ctx context.Context, orgID string, apdexSettings *types.ApdexSettings) *model.ApiError { + // Set the org_id from the parameter since it's required for the foreign key constraint + apdexSettings.OrgID = orgID - _, err := mds.db.NamedExec(` - INSERT OR REPLACE INTO apdex_settings ( - service_name, - threshold, - exclude_status_codes - ) VALUES ( - :service_name, - :threshold, - :exclude_status_codes - )`, apdexSettings) + _, err := mds.bundb.NewInsert(). + Model(apdexSettings). + On("CONFLICT (org_id, service_name) DO UPDATE"). + Set("threshold = EXCLUDED.threshold"). + Set("exclude_status_codes = EXCLUDED.exclude_status_codes"). + Exec(ctx) if err != nil { return &model.ApiError{ Err: err, diff --git a/pkg/query-service/dao/sqlite/connection.go b/pkg/query-service/dao/sqlite/connection.go index 697b1b3797..430ad862db 100644 --- a/pkg/query-service/dao/sqlite/connection.go +++ b/pkg/query-service/dao/sqlite/connection.go @@ -7,7 +7,6 @@ import ( "github.com/pkg/errors" "github.com/uptrace/bun" "go.signoz.io/signoz/pkg/query-service/constants" - "go.signoz.io/signoz/pkg/query-service/model" "go.signoz.io/signoz/pkg/query-service/telemetry" "go.signoz.io/signoz/pkg/sqlstore" "go.signoz.io/signoz/pkg/types" @@ -62,13 +61,13 @@ func (mds *ModelDaoSqlite) initializeOrgPreferences(ctx context.Context) error { return errors.Errorf("Found %d organizations, expected one or none.", len(orgs)) } - var org model.Organization + var org types.Organization if len(orgs) == 1 { org = orgs[0] } // set telemetry fields from userPreferences - telemetry.GetInstance().SetDistinctId(org.Id) + telemetry.GetInstance().SetDistinctId(org.ID) users, _ := mds.GetUsers(ctx) countUsers := len(users) diff --git a/pkg/query-service/dao/sqlite/rbac.go b/pkg/query-service/dao/sqlite/rbac.go index 799baffa2c..1718aadad3 100644 --- a/pkg/query-service/dao/sqlite/rbac.go +++ b/pkg/query-service/dao/sqlite/rbac.go @@ -2,7 +2,6 @@ package sqlite import ( "context" - "encoding/json" "fmt" "time" @@ -13,33 +12,38 @@ import ( "go.signoz.io/signoz/pkg/types" ) -func (mds *ModelDaoSqlite) CreateInviteEntry(ctx context.Context, - req *model.InvitationObject) *model.ApiError { +func (mds *ModelDaoSqlite) CreateInviteEntry(ctx context.Context, req *types.Invite) *model.ApiError { + _, err := mds.bundb.NewInsert(). + Model(req). + Exec(ctx) - _, err := mds.db.ExecContext(ctx, - `INSERT INTO invites (email, name, token, role, created_at, org_id) - VALUES (?, ?, ?, ?, ?, ?);`, - req.Email, req.Name, req.Token, req.Role, req.CreatedAt, req.OrgId) if err != nil { return &model.ApiError{Typ: model.ErrorInternal, Err: err} } return nil } -func (mds *ModelDaoSqlite) DeleteInvitation(ctx context.Context, email string) *model.ApiError { - _, err := mds.db.ExecContext(ctx, `DELETE from invites where email=?;`, email) +func (mds *ModelDaoSqlite) DeleteInvitation(ctx context.Context, orgID string, email string) *model.ApiError { + _, err := mds.bundb.NewDelete(). + Model(&types.Invite{}). + Where("org_id = ?", orgID). + Where("email = ?", email). + Exec(ctx) if err != nil { return &model.ApiError{Typ: model.ErrorInternal, Err: err} } return nil } +// TODO: Make this work with org id func (mds *ModelDaoSqlite) GetInviteFromEmail(ctx context.Context, email string, -) (*model.InvitationObject, *model.ApiError) { +) (*types.Invite, *model.ApiError) { - invites := []model.InvitationObject{} - err := mds.db.Select(&invites, - `SELECT * FROM invites WHERE email=?;`, email) + invites := []types.Invite{} + err := mds.bundb.NewSelect(). + Model(&invites). + Where("email = ?", email). + Scan(ctx) if err != nil { return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err} @@ -57,11 +61,14 @@ func (mds *ModelDaoSqlite) GetInviteFromEmail(ctx context.Context, email string, } func (mds *ModelDaoSqlite) GetInviteFromToken(ctx context.Context, token string, -) (*model.InvitationObject, *model.ApiError) { +) (*types.Invite, *model.ApiError) { + // This won't take org id because it's a public facing API - invites := []model.InvitationObject{} - err := mds.db.Select(&invites, - `SELECT * FROM invites WHERE token=?;`, token) + invites := []types.Invite{} + err := mds.bundb.NewSelect(). + Model(&invites). + Where("token = ?", token). + Scan(ctx) if err != nil { return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err} @@ -76,11 +83,12 @@ func (mds *ModelDaoSqlite) GetInviteFromToken(ctx context.Context, token string, return &invites[0], nil } -func (mds *ModelDaoSqlite) GetInvites(ctx context.Context, -) ([]model.InvitationObject, *model.ApiError) { - - invites := []model.InvitationObject{} - err := mds.db.Select(&invites, "SELECT * FROM invites") +func (mds *ModelDaoSqlite) GetInvites(ctx context.Context, orgID string) ([]types.Invite, *model.ApiError) { + invites := []types.Invite{} + err := mds.bundb.NewSelect(). + Model(&invites). + Where("org_id = ?", orgID). + Scan(ctx) if err != nil { return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err} } @@ -88,13 +96,13 @@ func (mds *ModelDaoSqlite) GetInvites(ctx context.Context, } func (mds *ModelDaoSqlite) CreateOrg(ctx context.Context, - org *model.Organization) (*model.Organization, *model.ApiError) { + org *types.Organization) (*types.Organization, *model.ApiError) { - org.Id = uuid.NewString() - org.CreatedAt = time.Now().Unix() - _, err := mds.db.ExecContext(ctx, - `INSERT INTO organizations (id, name, created_at,is_anonymous,has_opted_updates) VALUES (?, ?, ?, ?, ?);`, - org.Id, org.Name, org.CreatedAt, org.IsAnonymous, org.HasOptedUpdates) + org.ID = uuid.NewString() + org.CreatedAt = time.Now() + _, err := mds.bundb.NewInsert(). + Model(org). + Exec(ctx) if err != nil { return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err} @@ -103,14 +111,18 @@ func (mds *ModelDaoSqlite) CreateOrg(ctx context.Context, } func (mds *ModelDaoSqlite) GetOrg(ctx context.Context, - id string) (*model.Organization, *model.ApiError) { + id string) (*types.Organization, *model.ApiError) { - orgs := []model.Organization{} - err := mds.db.Select(&orgs, `SELECT * FROM organizations WHERE id=?;`, id) + orgs := []types.Organization{} - if err != nil { + if err := mds.bundb.NewSelect(). + Model(&orgs). + Where("id = ?", id). + Scan(ctx); err != nil { return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err} } + + // TODO(nitya): remove for multitenancy if len(orgs) > 1 { return nil, &model.ApiError{ Typ: model.ErrorInternal, @@ -125,11 +137,14 @@ func (mds *ModelDaoSqlite) GetOrg(ctx context.Context, } func (mds *ModelDaoSqlite) GetOrgByName(ctx context.Context, - name string) (*model.Organization, *model.ApiError) { + name string) (*types.Organization, *model.ApiError) { - orgs := []model.Organization{} + orgs := []types.Organization{} - if err := mds.db.Select(&orgs, `SELECT * FROM organizations WHERE name=?;`, name); err != nil { + if err := mds.bundb.NewSelect(). + Model(&orgs). + Where("name = ?", name). + Scan(ctx); err != nil { return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err} } @@ -145,9 +160,11 @@ func (mds *ModelDaoSqlite) GetOrgByName(ctx context.Context, return &orgs[0], nil } -func (mds *ModelDaoSqlite) GetOrgs(ctx context.Context) ([]model.Organization, *model.ApiError) { - orgs := []model.Organization{} - err := mds.db.Select(&orgs, `SELECT * FROM organizations`) +func (mds *ModelDaoSqlite) GetOrgs(ctx context.Context) ([]types.Organization, *model.ApiError) { + var orgs []types.Organization + err := mds.bundb.NewSelect(). + Model(&orgs). + Scan(ctx) if err != nil { return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err} @@ -155,24 +172,31 @@ func (mds *ModelDaoSqlite) GetOrgs(ctx context.Context) ([]model.Organization, * return orgs, nil } -func (mds *ModelDaoSqlite) EditOrg(ctx context.Context, org *model.Organization) *model.ApiError { +func (mds *ModelDaoSqlite) EditOrg(ctx context.Context, org *types.Organization) *model.ApiError { + _, err := mds.bundb.NewUpdate(). + Model(org). + Column("name"). + Column("has_opted_updates"). + Column("is_anonymous"). + Where("id = ?", org.ID). + Exec(ctx) - q := `UPDATE organizations SET name=?,has_opted_updates=?,is_anonymous=? WHERE id=?;` - - _, err := mds.db.ExecContext(ctx, q, org.Name, org.HasOptedUpdates, org.IsAnonymous, org.Id) if err != nil { return &model.ApiError{Typ: model.ErrorInternal, Err: err} } telemetry.GetInstance().SetTelemetryAnonymous(org.IsAnonymous) - telemetry.GetInstance().SetDistinctId(org.Id) + telemetry.GetInstance().SetDistinctId(org.ID) return nil } func (mds *ModelDaoSqlite) DeleteOrg(ctx context.Context, id string) *model.ApiError { + _, err := mds.bundb.NewDelete(). + Model(&types.Organization{}). + Where("id = ?", id). + Exec(ctx) - _, err := mds.db.ExecContext(ctx, `DELETE from organizations where id=?;`, id) if err != nil { return &model.ApiError{Typ: model.ErrorInternal, Err: err} } @@ -180,14 +204,10 @@ func (mds *ModelDaoSqlite) DeleteOrg(ctx context.Context, id string) *model.ApiE } func (mds *ModelDaoSqlite) CreateUser(ctx context.Context, - user *model.User, isFirstUser bool) (*model.User, *model.ApiError) { - - _, err := mds.db.ExecContext(ctx, - `INSERT INTO users (id, name, email, password, created_at, profile_picture_url, group_id, org_id) - VALUES (?, ?, ?, ?, ?, ?, ?,?);`, - user.Id, user.Name, user.Email, user.Password, user.CreatedAt, - user.ProfilePictureURL, user.GroupId, user.OrgId, - ) + user *types.User, isFirstUser bool) (*types.User, *model.ApiError) { + _, err := mds.bundb.NewInsert(). + Model(user). + Exec(ctx) if err != nil { return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err} @@ -210,11 +230,15 @@ func (mds *ModelDaoSqlite) CreateUser(ctx context.Context, } func (mds *ModelDaoSqlite) EditUser(ctx context.Context, - update *model.User) (*model.User, *model.ApiError) { + update *types.User) (*types.User, *model.ApiError) { + _, err := mds.bundb.NewUpdate(). + Model(update). + Column("name"). + Column("org_id"). + Column("email"). + Where("id = ?", update.ID). + Exec(ctx) - _, err := mds.db.ExecContext(ctx, - `UPDATE users SET name=?,org_id=?,email=? WHERE id=?;`, update.Name, - update.OrgId, update.Email, update.Id) if err != nil { return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err} } @@ -224,8 +248,13 @@ func (mds *ModelDaoSqlite) EditUser(ctx context.Context, func (mds *ModelDaoSqlite) UpdateUserPassword(ctx context.Context, passwordHash, userId string) *model.ApiError { - q := `UPDATE users SET password=? WHERE id=?;` - if _, err := mds.db.ExecContext(ctx, q, passwordHash, userId); err != nil { + _, err := mds.bundb.NewUpdate(). + Model(&types.User{}). + Set("password = ?", passwordHash). + Where("id = ?", userId). + Exec(ctx) + + if err != nil { return &model.ApiError{Typ: model.ErrorInternal, Err: err} } return nil @@ -233,16 +262,24 @@ func (mds *ModelDaoSqlite) UpdateUserPassword(ctx context.Context, passwordHash, func (mds *ModelDaoSqlite) UpdateUserGroup(ctx context.Context, userId, groupId string) *model.ApiError { - q := `UPDATE users SET group_id=? WHERE id=?;` - if _, err := mds.db.ExecContext(ctx, q, groupId, userId); err != nil { + _, err := mds.bundb.NewUpdate(). + Model(&types.User{}). + Set("group_id = ?", groupId). + Where("id = ?", userId). + Exec(ctx) + + if err != nil { return &model.ApiError{Typ: model.ErrorInternal, Err: err} } return nil } func (mds *ModelDaoSqlite) DeleteUser(ctx context.Context, id string) *model.ApiError { + result, err := mds.bundb.NewDelete(). + Model(&types.User{}). + Where("id = ?", id). + Exec(ctx) - result, err := mds.db.ExecContext(ctx, `DELETE from users where id=?;`, id) if err != nil { return &model.ApiError{Typ: model.ErrorInternal, Err: err} } @@ -262,30 +299,19 @@ func (mds *ModelDaoSqlite) DeleteUser(ctx context.Context, id string) *model.Api } func (mds *ModelDaoSqlite) GetUser(ctx context.Context, - id string) (*model.UserPayload, *model.ApiError) { + id string) (*types.GettableUser, *model.ApiError) { - users := []model.UserPayload{} - query := `select - u.id, - u.name, - u.email, - u.password, - u.created_at, - u.profile_picture_url, - u.org_id, - u.group_id, - g.name as role, - o.name as organization, - COALESCE((select uf.flags - from user_flags uf - where u.id = uf.user_id), '') as flags - from users u, groups g, organizations o - where - g.id=u.group_id and - o.id = u.org_id and - u.id=?;` + users := []types.GettableUser{} + query := mds.bundb.NewSelect(). + Table("users"). + Column("users.id", "users.name", "users.email", "users.password", "users.created_at", "users.profile_picture_url", "users.org_id", "users.group_id"). + ColumnExpr("g.name as role"). + ColumnExpr("o.name as organization"). + Join("JOIN groups g ON g.id = users.group_id"). + Join("JOIN organizations o ON o.id = users.org_id"). + Where("users.id = ?", id) - if err := mds.db.Select(&users, query, id); err != nil { + if err := query.Scan(ctx, &users); err != nil { return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err} } if len(users) > 1 { @@ -303,7 +329,7 @@ func (mds *ModelDaoSqlite) GetUser(ctx context.Context, } func (mds *ModelDaoSqlite) GetUserByEmail(ctx context.Context, - email string) (*model.UserPayload, *model.ApiError) { + email string) (*types.GettableUser, *model.ApiError) { if email == "" { return nil, &model.ApiError{ @@ -312,27 +338,20 @@ func (mds *ModelDaoSqlite) GetUserByEmail(ctx context.Context, } } - users := []model.UserPayload{} - query := `select - u.id, - u.name, - u.email, - u.password, - u.created_at, - u.profile_picture_url, - u.org_id, - u.group_id, - g.name as role, - o.name as organization - from users u, groups g, organizations o - where - g.id=u.group_id and - o.id = u.org_id and - u.email=?;` + users := []types.GettableUser{} + query := mds.bundb.NewSelect(). + Table("users"). + Column("users.id", "users.name", "users.email", "users.password", "users.created_at", "users.profile_picture_url", "users.org_id", "users.group_id"). + ColumnExpr("g.name as role"). + ColumnExpr("o.name as organization"). + Join("JOIN groups g ON g.id = users.group_id"). + Join("JOIN organizations o ON o.id = users.org_id"). + Where("users.email = ?", email) - if err := mds.db.Select(&users, query, email); err != nil { + if err := query.Scan(ctx, &users); err != nil { return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err} } + if len(users) > 1 { return nil, &model.ApiError{ Typ: model.ErrorInternal, @@ -347,34 +366,26 @@ func (mds *ModelDaoSqlite) GetUserByEmail(ctx context.Context, } // GetUsers fetches total user count -func (mds *ModelDaoSqlite) GetUsers(ctx context.Context) ([]model.UserPayload, *model.ApiError) { +func (mds *ModelDaoSqlite) GetUsers(ctx context.Context) ([]types.GettableUser, *model.ApiError) { return mds.GetUsersWithOpts(ctx, 0) } // GetUsersWithOpts fetches users and supports additional search options -func (mds *ModelDaoSqlite) GetUsersWithOpts(ctx context.Context, limit int) ([]model.UserPayload, *model.ApiError) { - users := []model.UserPayload{} +func (mds *ModelDaoSqlite) GetUsersWithOpts(ctx context.Context, limit int) ([]types.GettableUser, *model.ApiError) { + users := []types.GettableUser{} - query := `select - u.id, - u.name, - u.email, - u.password, - u.created_at, - u.profile_picture_url, - u.org_id, - u.group_id, - g.name as role, - o.name as organization - from users u, groups g, organizations o - where - g.id = u.group_id and - o.id = u.org_id` + query := mds.bundb.NewSelect(). + Table("users"). + Column("users.id", "users.name", "users.email", "users.password", "users.created_at", "users.profile_picture_url", "users.org_id", "users.group_id"). + ColumnExpr("g.name as role"). + ColumnExpr("o.name as organization"). + Join("JOIN groups g ON g.id = users.group_id"). + Join("JOIN organizations o ON o.id = users.org_id") if limit > 0 { - query = fmt.Sprintf("%s LIMIT %d", query, limit) + query.Limit(limit) } - err := mds.db.Select(&users, query) + err := query.Scan(ctx, &users) if err != nil { return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err} @@ -383,54 +394,42 @@ func (mds *ModelDaoSqlite) GetUsersWithOpts(ctx context.Context, limit int) ([]m } func (mds *ModelDaoSqlite) GetUsersByOrg(ctx context.Context, - orgId string) ([]model.UserPayload, *model.ApiError) { + orgId string) ([]types.GettableUser, *model.ApiError) { - users := []model.UserPayload{} - query := `select - u.id, - u.name, - u.email, - u.password, - u.created_at, - u.profile_picture_url, - u.org_id, - u.group_id, - g.name as role, - o.name as organization - from users u, groups g, organizations o - where - u.group_id = g.id and - u.org_id = o.id and - u.org_id=?;` + users := []types.GettableUser{} - if err := mds.db.Select(&users, query, orgId); err != nil { + query := mds.bundb.NewSelect(). + Table("users"). + Column("users.id", "users.name", "users.email", "users.password", "users.created_at", "users.profile_picture_url", "users.org_id", "users.group_id"). + ColumnExpr("g.name as role"). + ColumnExpr("o.name as organization"). + Join("JOIN groups g ON g.id = users.group_id"). + Join("JOIN organizations o ON o.id = users.org_id"). + Where("users.org_id = ?", orgId) + + err := query.Scan(ctx, &users) + if err != nil { return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err} } return users, nil } func (mds *ModelDaoSqlite) GetUsersByGroup(ctx context.Context, - groupId string) ([]model.UserPayload, *model.ApiError) { + groupId string) ([]types.GettableUser, *model.ApiError) { - users := []model.UserPayload{} - query := `select - u.id, - u.name, - u.email, - u.password, - u.created_at, - u.profile_picture_url, - u.org_id, - u.group_id, - g.name as role, - o.name as organization - from users u, groups g, organizations o - where - u.group_id = g.id and - o.id = u.org_id and - u.group_id=?;` + users := []types.GettableUser{} - if err := mds.db.Select(&users, query, groupId); err != nil { + query := mds.bundb.NewSelect(). + Table("users"). + Column("users.id", "users.name", "users.email", "users.password", "users.created_at", "users.profile_picture_url", "users.org_id", "users.group_id"). + ColumnExpr("g.name as role"). + ColumnExpr("o.name as organization"). + Join("JOIN groups g ON g.id = users.group_id"). + Join("JOIN organizations o ON o.id = users.org_id"). + Where("users.group_id = ?", groupId) + + err := query.Scan(ctx, &users) + if err != nil { return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err} } return users, nil @@ -452,17 +451,25 @@ func (mds *ModelDaoSqlite) CreateGroup(ctx context.Context, func (mds *ModelDaoSqlite) DeleteGroup(ctx context.Context, id string) *model.ApiError { - if _, err := mds.db.ExecContext(ctx, `DELETE from groups where id=?;`, id); err != nil { + _, err := mds.bundb.NewDelete(). + Model(&types.Group{}). + Where("id = ?", id). + Exec(ctx) + + if err != nil { return &model.ApiError{Typ: model.ErrorInternal, Err: err} } return nil } func (mds *ModelDaoSqlite) GetGroup(ctx context.Context, - id string) (*model.Group, *model.ApiError) { + id string) (*types.Group, *model.ApiError) { - groups := []model.Group{} - if err := mds.db.Select(&groups, `SELECT id, name FROM groups WHERE id=?`, id); err != nil { + groups := []types.Group{} + if err := mds.bundb.NewSelect(). + Model(&groups). + Where("id = ?", id). + Scan(ctx); err != nil { return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err} } @@ -505,10 +512,13 @@ func (mds *ModelDaoSqlite) GetGroupByName(ctx context.Context, return &groups[0], nil } -func (mds *ModelDaoSqlite) GetGroups(ctx context.Context) ([]model.Group, *model.ApiError) { +// TODO(nitya): should have org id +func (mds *ModelDaoSqlite) GetGroups(ctx context.Context) ([]types.Group, *model.ApiError) { - groups := []model.Group{} - if err := mds.db.Select(&groups, "SELECT * FROM groups"); err != nil { + groups := []types.Group{} + if err := mds.bundb.NewSelect(). + Model(&groups). + Scan(ctx); err != nil { return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err} } @@ -516,10 +526,11 @@ func (mds *ModelDaoSqlite) GetGroups(ctx context.Context) ([]model.Group, *model } func (mds *ModelDaoSqlite) CreateResetPasswordEntry(ctx context.Context, - req *model.ResetPasswordEntry) *model.ApiError { + req *types.ResetPasswordRequest) *model.ApiError { - q := `INSERT INTO reset_password_request (user_id, token) VALUES (?, ?);` - if _, err := mds.db.ExecContext(ctx, q, req.UserId, req.Token); err != nil { + if _, err := mds.bundb.NewInsert(). + Model(req). + Exec(ctx); err != nil { return &model.ApiError{Typ: model.ErrorInternal, Err: err} } return nil @@ -527,7 +538,11 @@ func (mds *ModelDaoSqlite) CreateResetPasswordEntry(ctx context.Context, func (mds *ModelDaoSqlite) DeleteResetPasswordEntry(ctx context.Context, token string) *model.ApiError { - _, err := mds.db.ExecContext(ctx, `DELETE from reset_password_request where token=?;`, token) + _, err := mds.bundb.NewDelete(). + Model(&types.ResetPasswordRequest{}). + Where("token = ?", token). + Exec(ctx) + if err != nil { return &model.ApiError{Typ: model.ErrorInternal, Err: err} } @@ -535,12 +550,14 @@ func (mds *ModelDaoSqlite) DeleteResetPasswordEntry(ctx context.Context, } func (mds *ModelDaoSqlite) GetResetPasswordEntry(ctx context.Context, - token string) (*model.ResetPasswordEntry, *model.ApiError) { + token string) (*types.ResetPasswordRequest, *model.ApiError) { - entries := []model.ResetPasswordEntry{} + entries := []types.ResetPasswordRequest{} - q := `SELECT user_id,token FROM reset_password_request WHERE token=?;` - if err := mds.db.Select(&entries, q, token); err != nil { + if err := mds.bundb.NewSelect(). + Model(&entries). + Where("token = ?", token). + Scan(ctx); err != nil { return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err} } if len(entries) > 1 { @@ -554,54 +571,6 @@ func (mds *ModelDaoSqlite) GetResetPasswordEntry(ctx context.Context, return &entries[0], nil } -// CreateUserFlags inserts user specific flags -func (mds *ModelDaoSqlite) UpdateUserFlags(ctx context.Context, userId string, flags map[string]string) (model.UserFlag, *model.ApiError) { - - if len(flags) == 0 { - // nothing to do as flags are empty. In this method, we only append the flags - // but not set them to empty - return flags, nil - } - - // fetch existing flags - userPayload, apiError := mds.GetUser(ctx, userId) - if apiError != nil { - return nil, apiError - } - - for k, v := range userPayload.Flags { - if _, ok := flags[k]; !ok { - // insert only missing keys as we want to retain the - // flags in the db that are not part of this request - flags[k] = v - } - } - - // append existing flags with new ones - - // write the updated flags - flagsBytes, err := json.Marshal(flags) - if err != nil { - return nil, model.InternalError(err) - } - - if len(userPayload.Flags) == 0 { - q := `INSERT INTO user_flags (user_id, flags) VALUES (?, ?);` - - if _, err := mds.db.ExecContext(ctx, q, userId, string(flagsBytes)); err != nil { - return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err} - } - } else { - q := `UPDATE user_flags SET flags = ? WHERE user_id = ?;` - - if _, err := mds.db.ExecContext(ctx, q, userId, string(flagsBytes)); err != nil { - return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err} - } - } - - return flags, nil -} - func (mds *ModelDaoSqlite) PrecheckLogin(ctx context.Context, email, sourceUrl string) (*model.PrecheckResponse, model.BaseApiError) { // assume user is valid unless proven otherwise and assign default values for rest of the fields resp := &model.PrecheckResponse{IsUser: true, CanSelfRegister: false, SSO: false, SsoUrl: "", SsoError: ""} diff --git a/pkg/query-service/model/auth.go b/pkg/query-service/model/auth.go index c9f5991472..a75d301e86 100644 --- a/pkg/query-service/model/auth.go +++ b/pkg/query-service/model/auth.go @@ -88,11 +88,6 @@ type ChangePasswordRequest struct { NewPassword string `json:"newPassword"` } -type ResetPasswordEntry struct { - UserId string `json:"userId" db:"user_id"` - Token string `json:"token" db:"token"` -} - type UserRole struct { UserId string `json:"user_id"` GroupName string `json:"group_name"` diff --git a/pkg/query-service/model/db.go b/pkg/query-service/model/db.go index 2968bf6606..f7509d0172 100644 --- a/pkg/query-service/model/db.go +++ b/pkg/query-service/model/db.go @@ -1,46 +1,10 @@ package model -import ( - "database/sql/driver" - "encoding/json" - "fmt" - "time" -) +import "time" -type Organization struct { - Id string `json:"id" db:"id"` - Name string `json:"name" db:"name"` - CreatedAt int64 `json:"createdAt" db:"created_at"` - IsAnonymous bool `json:"isAnonymous" db:"is_anonymous"` - HasOptedUpdates bool `json:"hasOptedUpdates" db:"has_opted_updates"` -} - -// InvitationObject represents the token object stored in the db -type InvitationObject struct { - Id string `json:"id" db:"id"` - Email string `json:"email" db:"email"` - Name string `json:"name" db:"name"` - Token string `json:"token" db:"token"` - CreatedAt int64 `json:"createdAt" db:"created_at"` - Role string `json:"role" db:"role"` - OrgId string `json:"orgId" db:"org_id"` -} - -type User struct { - Id string `json:"id" db:"id"` - Name string `json:"name" db:"name"` - Email string `json:"email" db:"email"` - Password string `json:"password,omitempty" db:"password"` - CreatedAt int64 `json:"createdAt" db:"created_at"` - ProfilePictureURL string `json:"profilePictureURL" db:"profile_picture_url"` - OrgId string `json:"orgId,omitempty" db:"org_id"` - GroupId string `json:"groupId,omitempty" db:"group_id"` -} - -type ApdexSettings struct { - ServiceName string `json:"serviceName" db:"service_name"` - Threshold float64 `json:"threshold" db:"threshold"` - ExcludeStatusCodes string `json:"excludeStatusCodes" db:"exclude_status_codes"` // sqlite doesn't support array type +type ResetPasswordRequest struct { + Password string `json:"password"` + Token string `json:"token"` } type IngestionKey struct { @@ -51,50 +15,3 @@ type IngestionKey struct { IngestionURL string `json:"ingestionURL" db:"ingestion_url"` DataRegion string `json:"dataRegion" db:"data_region"` } - -type UserFlag map[string]string - -func (uf UserFlag) Value() (driver.Value, error) { - f := make(map[string]string, 0) - for k, v := range uf { - f[k] = v - } - return json.Marshal(f) -} - -func (uf *UserFlag) Scan(value interface{}) error { - if value == "" { - return nil - } - - b, ok := value.(string) - if !ok { - return fmt.Errorf("type assertion to []byte failed while scanning user flag") - } - f := make(map[string]string, 0) - if err := json.Unmarshal([]byte(b), &f); err != nil { - return err - } - *uf = make(UserFlag, len(f)) - for k, v := range f { - (*uf)[k] = v - } - return nil -} - -type UserPayload struct { - User - Role string `json:"role"` - Organization string `json:"organization"` - Flags UserFlag `json:"flags"` -} - -type Group struct { - Id string `json:"id" db:"id"` - Name string `json:"name" db:"name"` -} - -type ResetPasswordRequest struct { - Password string `json:"password"` - Token string `json:"token"` -} diff --git a/pkg/query-service/telemetry/telemetry.go b/pkg/query-service/telemetry/telemetry.go index 01cefe3a69..a3f36850cd 100644 --- a/pkg/query-service/telemetry/telemetry.go +++ b/pkg/query-service/telemetry/telemetry.go @@ -20,6 +20,7 @@ import ( "go.signoz.io/signoz/pkg/query-service/model" v3 "go.signoz.io/signoz/pkg/query-service/model/v3" "go.signoz.io/signoz/pkg/query-service/version" + "go.signoz.io/signoz/pkg/types" ) const ( @@ -206,7 +207,7 @@ type Telemetry struct { alertsInfoCallback func(ctx context.Context) (*model.AlertsInfo, error) userCountCallback func(ctx context.Context) (int, error) userRoleCallback func(ctx context.Context, groupId string) (string, error) - getUsersCallback func(ctx context.Context) ([]model.UserPayload, *model.ApiError) + getUsersCallback func(ctx context.Context) ([]types.GettableUser, *model.ApiError) dashboardsInfoCallback func(ctx context.Context) (*model.DashboardsInfo, error) savedViewsInfoCallback func(ctx context.Context) (*model.SavedViewsInfo, error) } @@ -223,7 +224,7 @@ func (a *Telemetry) SetUserRoleCallback(callback func(ctx context.Context, group a.userRoleCallback = callback } -func (a *Telemetry) SetGetUsersCallback(callback func(ctx context.Context) ([]model.UserPayload, *model.ApiError)) { +func (a *Telemetry) SetGetUsersCallback(callback func(ctx context.Context) ([]types.GettableUser, *model.ApiError)) { a.getUsersCallback = callback } @@ -524,7 +525,7 @@ func getOutboundIP() string { return string(ip) } -func (a *Telemetry) IdentifyUser(user *model.User) { +func (a *Telemetry) IdentifyUser(user *types.User) { if user.Email == DEFAULT_CLOUD_EMAIL { return } @@ -534,7 +535,7 @@ func (a *Telemetry) IdentifyUser(user *model.User) { return } // extract user group from user.groupId - role, _ := a.userRoleCallback(context.Background(), user.GroupId) + role, _ := a.userRoleCallback(context.Background(), user.GroupID) if a.saasOperator != nil { if role != "" { diff --git a/pkg/query-service/tests/integration/filter_suggestions_test.go b/pkg/query-service/tests/integration/filter_suggestions_test.go index 3fd05afa0b..24780698c4 100644 --- a/pkg/query-service/tests/integration/filter_suggestions_test.go +++ b/pkg/query-service/tests/integration/filter_suggestions_test.go @@ -17,9 +17,9 @@ import ( "go.signoz.io/signoz/pkg/query-service/constants" "go.signoz.io/signoz/pkg/query-service/dao" "go.signoz.io/signoz/pkg/query-service/featureManager" - "go.signoz.io/signoz/pkg/query-service/model" v3 "go.signoz.io/signoz/pkg/query-service/model/v3" "go.signoz.io/signoz/pkg/query-service/utils" + "go.signoz.io/signoz/pkg/types" "go.uber.org/zap" ) @@ -260,7 +260,7 @@ func (tb *FilterSuggestionsTestBed) mockAttribValuesQueryResponse( type FilterSuggestionsTestBed struct { t *testing.T - testUser *model.User + testUser *types.User qsHttpHandler http.Handler mockClickhouse mockhouse.ClickConnMockCommon } diff --git a/pkg/query-service/tests/integration/logparsingpipeline_test.go b/pkg/query-service/tests/integration/logparsingpipeline_test.go index e7b588a9c8..a4cd1fad1e 100644 --- a/pkg/query-service/tests/integration/logparsingpipeline_test.go +++ b/pkg/query-service/tests/integration/logparsingpipeline_test.go @@ -23,11 +23,11 @@ import ( opampModel "go.signoz.io/signoz/pkg/query-service/app/opamp/model" "go.signoz.io/signoz/pkg/query-service/constants" "go.signoz.io/signoz/pkg/query-service/dao" - "go.signoz.io/signoz/pkg/query-service/model" v3 "go.signoz.io/signoz/pkg/query-service/model/v3" "go.signoz.io/signoz/pkg/query-service/queryBuilderToExpr" "go.signoz.io/signoz/pkg/query-service/utils" "go.signoz.io/signoz/pkg/sqlstore" + "go.signoz.io/signoz/pkg/types" "golang.org/x/exp/maps" "golang.org/x/exp/slices" ) @@ -441,7 +441,7 @@ func TestCanSavePipelinesWithoutConnectedAgents(t *testing.T) { // configuring log pipelines and provides test helpers. type LogPipelinesTestBed struct { t *testing.T - testUser *model.User + testUser *types.User apiHandler *app.APIHandler agentConfMgr *agentConf.Manager opampServer *opamp.Server diff --git a/pkg/query-service/tests/integration/signoz_cloud_integrations_test.go b/pkg/query-service/tests/integration/signoz_cloud_integrations_test.go index 12d3cf62a5..37f29c0624 100644 --- a/pkg/query-service/tests/integration/signoz_cloud_integrations_test.go +++ b/pkg/query-service/tests/integration/signoz_cloud_integrations_test.go @@ -17,9 +17,9 @@ import ( "go.signoz.io/signoz/pkg/query-service/auth" "go.signoz.io/signoz/pkg/query-service/dao" "go.signoz.io/signoz/pkg/query-service/featureManager" - "go.signoz.io/signoz/pkg/query-service/model" "go.signoz.io/signoz/pkg/query-service/utils" "go.signoz.io/signoz/pkg/sqlstore" + "go.signoz.io/signoz/pkg/types" "go.uber.org/zap" ) @@ -338,7 +338,7 @@ func TestConfigReturnedWhenAgentChecksIn(t *testing.T) { type CloudIntegrationsTestBed struct { t *testing.T - testUser *model.User + testUser *types.User qsHttpHandler http.Handler mockClickhouse mockhouse.ClickConnMockCommon } diff --git a/pkg/query-service/tests/integration/signoz_integrations_test.go b/pkg/query-service/tests/integration/signoz_integrations_test.go index f70797d8ef..9c021836d1 100644 --- a/pkg/query-service/tests/integration/signoz_integrations_test.go +++ b/pkg/query-service/tests/integration/signoz_integrations_test.go @@ -23,6 +23,7 @@ import ( v3 "go.signoz.io/signoz/pkg/query-service/model/v3" "go.signoz.io/signoz/pkg/query-service/utils" "go.signoz.io/signoz/pkg/sqlstore" + "go.signoz.io/signoz/pkg/types" "go.uber.org/zap" ) @@ -367,7 +368,7 @@ func TestDashboardsForInstalledIntegrationDashboards(t *testing.T) { type IntegrationsTestBed struct { t *testing.T - testUser *model.User + testUser *types.User qsHttpHandler http.Handler mockClickhouse mockhouse.ClickConnMockCommon } diff --git a/pkg/query-service/tests/integration/test_utils.go b/pkg/query-service/tests/integration/test_utils.go index af68e41047..cbead9e2df 100644 --- a/pkg/query-service/tests/integration/test_utils.go +++ b/pkg/query-service/tests/integration/test_utils.go @@ -25,6 +25,7 @@ import ( "go.signoz.io/signoz/pkg/query-service/dao" "go.signoz.io/signoz/pkg/query-service/interfaces" "go.signoz.io/signoz/pkg/query-service/model" + "go.signoz.io/signoz/pkg/types" "go.signoz.io/signoz/pkg/types/authtypes" "golang.org/x/exp/maps" ) @@ -150,10 +151,10 @@ func makeTestSignozLog( return testLog } -func createTestUser() (*model.User, *model.ApiError) { +func createTestUser() (*types.User, *model.ApiError) { // Create a test user for auth ctx := context.Background() - org, apiErr := dao.DB().CreateOrg(ctx, &model.Organization{ + org, apiErr := dao.DB().CreateOrg(ctx, &types.Organization{ Name: "test", }) if apiErr != nil { @@ -170,20 +171,20 @@ func createTestUser() (*model.User, *model.ApiError) { userId := uuid.NewString() return dao.DB().CreateUser( ctx, - &model.User{ - Id: userId, + &types.User{ + ID: userId, Name: "test", Email: userId[:8] + "test@test.com", Password: "test", - OrgId: org.Id, - GroupId: group.ID, + OrgID: org.ID, + GroupID: group.ID, }, true, ) } func AuthenticatedRequestForTest( - user *model.User, + user *types.User, path string, postData interface{}, ) (*http.Request, error) { diff --git a/pkg/query-service/utils/testutils.go b/pkg/query-service/utils/testutils.go index cf4a5882d9..fd0678c6a3 100644 --- a/pkg/query-service/utils/testutils.go +++ b/pkg/query-service/utils/testutils.go @@ -45,6 +45,9 @@ func NewTestSqliteDB(t *testing.T) (sqlStore sqlstore.SQLStore, testDBFilePath s sqlmigration.NewAddIntegrationsFactory(), sqlmigration.NewAddLicensesFactory(), sqlmigration.NewAddPatsFactory(), + sqlmigration.NewModifyDatetimeFactory(), + sqlmigration.NewModifyOrgDomainFactory(), + sqlmigration.NewUpdateOrganizationFactory(sqlStore), ), ) if err != nil { @@ -62,7 +65,10 @@ func NewTestSqliteDB(t *testing.T) (sqlStore sqlstore.SQLStore, testDBFilePath s func NewQueryServiceDBForTests(t *testing.T) sqlstore.SQLStore { sqlStore, _ := NewTestSqliteDB(t) - dao.InitDao(sqlStore) + err := dao.InitDao(sqlStore) + if err != nil { + t.Fatalf("could not initialize dao: %v", err) + } dashboards.InitDB(sqlStore.SQLxDB()) return sqlStore diff --git a/pkg/signoz/signoz.go b/pkg/signoz/signoz.go index c4fe9957e0..8dee40a4e5 100644 --- a/pkg/signoz/signoz.go +++ b/pkg/signoz/signoz.go @@ -74,6 +74,9 @@ func New( return nil, err } + // add the org migration here since we need to pass the sqlstore + providerConfig.SQLMigrationProviderFactories.Add(sqlmigration.NewUpdateOrganizationFactory(sqlstore)) + // Initialize telemetrystore from the available telemetrystore provider factories telemetrystore, err := factory.NewProviderFromNamedMap( ctx, diff --git a/pkg/sqlmigration/009_add_pats.go b/pkg/sqlmigration/009_add_pats.go index 3190f5153a..3486fd17eb 100644 --- a/pkg/sqlmigration/009_add_pats.go +++ b/pkg/sqlmigration/009_add_pats.go @@ -35,7 +35,7 @@ func (migration *addPats) Up(ctx context.Context, db *bun.DB) error { OrgID string `bun:"org_id,type:text,notnull"` Name string `bun:"name,type:varchar(50),notnull,unique"` CreatedAt int `bun:"created_at,notnull"` - UpdatedAt int `bun:"updated_at,type:timestamp"` + UpdatedAt int `bun:"updated_at"` Data string `bun:"data,type:text,notnull"` }{}). ForeignKey(`("org_id") REFERENCES "organizations" ("id")`). diff --git a/pkg/sqlmigration/012_modify_org_domain.go b/pkg/sqlmigration/012_modify_org_domain.go index 8c83dcb344..3bf910f60e 100644 --- a/pkg/sqlmigration/012_modify_org_domain.go +++ b/pkg/sqlmigration/012_modify_org_domain.go @@ -4,6 +4,7 @@ import ( "context" "github.com/uptrace/bun" + "github.com/uptrace/bun/dialect" "github.com/uptrace/bun/migrate" "go.signoz.io/signoz/pkg/factory" ) @@ -27,6 +28,11 @@ func (migration *modifyOrgDomain) Register(migrations *migrate.Migrations) error } func (migration *modifyOrgDomain) Up(ctx context.Context, db *bun.DB) error { + // only run this for old sqlite db + if db.Dialect().Name() != dialect.SQLite { + return nil + } + // begin transaction tx, err := db.BeginTx(ctx, nil) if err != nil { diff --git a/pkg/sqlmigration/013_update_organization.go b/pkg/sqlmigration/013_update_organization.go new file mode 100644 index 0000000000..ffa919961d --- /dev/null +++ b/pkg/sqlmigration/013_update_organization.go @@ -0,0 +1,152 @@ +package sqlmigration + +import ( + "context" + "database/sql" + "errors" + + "github.com/uptrace/bun" + "github.com/uptrace/bun/migrate" + "go.signoz.io/signoz/pkg/factory" + "go.signoz.io/signoz/pkg/sqlstore" +) + +type updateOrganization struct { + store sqlstore.SQLStore +} + +func NewUpdateOrganizationFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] { + return factory.NewProviderFactory(factory.MustNewName("update_organization"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) { + return newUpdateOrganization(ctx, ps, c, sqlstore) + }) +} + +func newUpdateOrganization(_ context.Context, _ factory.ProviderSettings, _ Config, store sqlstore.SQLStore) (SQLMigration, error) { + return &updateOrganization{ + store: store, + }, nil +} + +func (migration *updateOrganization) Register(migrations *migrate.Migrations) error { + if err := migrations.Register(migration.Up, migration.Down); err != nil { + return err + } + + return nil +} + +func (migration *updateOrganization) Up(ctx context.Context, db *bun.DB) error { + + // begin transaction + tx, err := db.BeginTx(ctx, nil) + if err != nil { + return err + } + defer tx.Rollback() + + // update apdex settings table + if err := updateApdexSettings(ctx, tx); err != nil { + return err + } + + // drop user_flags table + if _, err := tx.NewDropTable().IfExists().Table("user_flags").Exec(ctx); err != nil { + return err + } + + // add org id to groups table + if exists, err := migration.store.Dialect().ColumnExists(ctx, tx, "groups", "org_id"); err != nil { + return err + } else if !exists { + if _, err := tx.NewAddColumn().Table("groups").ColumnExpr("org_id TEXT").Exec(ctx); err != nil { + return err + } + } + + // add created_at to groups table + for _, table := range []string{"groups"} { + if exists, err := migration.store.Dialect().ColumnExists(ctx, tx, table, "created_at"); err != nil { + return err + } else if !exists { + if _, err := tx.NewAddColumn().Table(table).ColumnExpr("created_at TIMESTAMP").Exec(ctx); err != nil { + return err + } + } + } + + // add updated_at to organizations, users, groups table + for _, table := range []string{"organizations", "users", "groups"} { + if exists, err := migration.store.Dialect().ColumnExists(ctx, tx, table, "updated_at"); err != nil { + return err + } else if !exists { + if _, err := tx.NewAddColumn().Table(table).ColumnExpr("updated_at TIMESTAMP").Exec(ctx); err != nil { + return err + } + } + } + + // since organizations, users has created_at as integer instead of timestamp + for _, table := range []string{"organizations", "users", "invites"} { + if err := migration.store.Dialect().MigrateIntToTimestamp(ctx, tx, table, "created_at"); err != nil { + return err + } + } + + // migrate is_anonymous and has_opted_updates to boolean from int + for _, column := range []string{"is_anonymous", "has_opted_updates"} { + if err := migration.store.Dialect().MigrateIntToBoolean(ctx, tx, "organizations", column); err != nil { + return err + } + } + + if err := tx.Commit(); err != nil { + return err + } + + return nil +} + +func (migration *updateOrganization) Down(ctx context.Context, db *bun.DB) error { + return nil +} + +func updateApdexSettings(ctx context.Context, tx bun.Tx) error { + if _, err := tx.NewCreateTable(). + Model(&struct { + bun.BaseModel `bun:"table:apdex_settings_new"` + OrgID string `bun:"org_id,pk,type:text"` + ServiceName string `bun:"service_name,pk,type:text"` + Threshold float64 `bun:"threshold,type:float,notnull"` + ExcludeStatusCodes string `bun:"exclude_status_codes,type:text,notnull"` + }{}). + ForeignKey(`("org_id") REFERENCES "organizations" ("id")`). + IfNotExists(). + Exec(ctx); err != nil { + return err + } + + // get org id from organizations table + var orgID string + if err := tx.QueryRowContext(ctx, `SELECT id FROM organizations LIMIT 1`).Scan(&orgID); err != nil && !errors.Is(err, sql.ErrNoRows) { + return err + } + + if orgID != "" { + // copy old data + if _, err := tx.ExecContext(ctx, `INSERT INTO apdex_settings_new (org_id, service_name, threshold, exclude_status_codes) SELECT ?, service_name, threshold, exclude_status_codes FROM apdex_settings`, orgID); err != nil { + return err + } + } + + // drop old table + if _, err := tx.NewDropTable().IfExists().Table("apdex_settings").Exec(ctx); err != nil { + return err + } + + // rename new table to old table + if _, err := tx.ExecContext(ctx, `ALTER TABLE apdex_settings_new RENAME TO apdex_settings`); err != nil { + return err + } + + return nil +} diff --git a/pkg/sqlmigration/sqlmigration.go b/pkg/sqlmigration/sqlmigration.go index d0b121f6ac..978148d0b5 100644 --- a/pkg/sqlmigration/sqlmigration.go +++ b/pkg/sqlmigration/sqlmigration.go @@ -86,3 +86,29 @@ func WrapIfNotExists(ctx context.Context, db *bun.DB, table string, column strin return q.Err(ErrNoExecute) } } + +func GetColumnType(ctx context.Context, bun bun.IDB, table string, column string) (string, error) { + var columnType string + var err error + + if bun.Dialect().Name() == dialect.SQLite { + err = bun.NewSelect(). + ColumnExpr("type"). + TableExpr("pragma_table_info(?)", table). + Where("name = ?", column). + Scan(ctx, &columnType) + } else { + err = bun.NewSelect(). + ColumnExpr("data_type"). + TableExpr("information_schema.columns"). + Where("table_name = ?", table). + Where("column_name = ?", column). + Scan(ctx, &columnType) + } + + if err != nil { + return "", err + } + + return columnType, nil +} diff --git a/pkg/sqlstore/postgressqlstore/dialect.go b/pkg/sqlstore/postgressqlstore/dialect.go new file mode 100644 index 0000000000..3f4744ddac --- /dev/null +++ b/pkg/sqlstore/postgressqlstore/dialect.go @@ -0,0 +1,116 @@ +package postgressqlstore + +import ( + "context" + + "github.com/uptrace/bun" +) + +type PGDialect struct { +} + +func (dialect *PGDialect) MigrateIntToTimestamp(ctx context.Context, bun bun.IDB, table string, column string) error { + columnType, err := dialect.GetColumnType(ctx, bun, table, column) + if err != nil { + return err + } + + // bigint for postgres and INTEGER for sqlite + if columnType != "bigint" { + return nil + } + + // if the columns is integer then do this + if _, err := bun.ExecContext(ctx, `ALTER TABLE `+table+` RENAME COLUMN `+column+` TO `+column+`_old`); err != nil { + return err + } + + // add new timestamp column + if _, err := bun.NewAddColumn().Table(table).ColumnExpr(column + " TIMESTAMP").Exec(ctx); err != nil { + return err + } + + if _, err := bun.NewUpdate(). + Table(table). + Set(column + " = to_timestamp(cast(" + column + "_old as INTEGER))"). + Where("1=1"). + Exec(ctx); err != nil { + return err + } + + // drop old column + if _, err := bun.NewDropColumn().Table(table).Column(column + "_old").Exec(ctx); err != nil { + return err + } + + return nil +} + +func (dialect *PGDialect) MigrateIntToBoolean(ctx context.Context, bun bun.IDB, table string, column string) error { + columnType, err := dialect.GetColumnType(ctx, bun, table, column) + if err != nil { + return err + } + + if columnType != "bigint" { + return nil + } + + if _, err := bun.ExecContext(ctx, `ALTER TABLE `+table+` RENAME COLUMN `+column+` TO `+column+`_old`); err != nil { + return err + } + + // add new boolean column + if _, err := bun.NewAddColumn().Table(table).ColumnExpr(column + " BOOLEAN").Exec(ctx); err != nil { + return err + } + + // copy data from old column to new column, converting from int to boolean + if _, err := bun.NewUpdate(). + Table(table). + Set(column + " = CASE WHEN " + column + "_old = 1 THEN true ELSE false END"). + Where("1=1"). + Exec(ctx); err != nil { + return err + } + + // drop old column + if _, err := bun.NewDropColumn().Table(table).Column(column + "_old").Exec(ctx); err != nil { + return err + } + + return nil +} + +func (dialect *PGDialect) GetColumnType(ctx context.Context, bun bun.IDB, table string, column string) (string, error) { + var columnType string + var err error + err = bun.NewSelect(). + ColumnExpr("data_type"). + TableExpr("information_schema.columns"). + Where("table_name = ?", table). + Where("column_name = ?", column). + Scan(ctx, &columnType) + + if err != nil { + return "", err + } + + return columnType, nil +} + +func (dialect *PGDialect) ColumnExists(ctx context.Context, bun bun.IDB, table string, column string) (bool, error) { + var count int + err := bun.NewSelect(). + ColumnExpr("COUNT(*)"). + TableExpr("information_schema.columns"). + Where("table_name = ?", table). + Where("column_name = ?", column). + Scan(ctx, &count) + + if err != nil { + return false, err + } + + return count > 0, nil +} diff --git a/pkg/sqlstore/postgressqlstore/provider.go b/pkg/sqlstore/postgressqlstore/provider.go index 6dcc43ff3e..7b06d40716 100644 --- a/pkg/sqlstore/postgressqlstore/provider.go +++ b/pkg/sqlstore/postgressqlstore/provider.go @@ -18,6 +18,7 @@ type provider struct { sqldb *sql.DB bundb *sqlstore.BunDB sqlxdb *sqlx.DB + dialect *PGDialect } func NewFactory(hookFactories ...factory.ProviderFactory[sqlstore.SQLStoreHook, sqlstore.Config]) factory.ProviderFactory[sqlstore.SQLStore, sqlstore.Config] { @@ -59,6 +60,7 @@ func New(ctx context.Context, providerSettings factory.ProviderSettings, config sqldb: sqldb, bundb: sqlstore.NewBunDB(settings, sqldb, pgdialect.New(), hooks), sqlxdb: sqlx.NewDb(sqldb, "postgres"), + dialect: &PGDialect{}, }, nil } @@ -74,6 +76,10 @@ func (provider *provider) SQLxDB() *sqlx.DB { return provider.sqlxdb } +func (provider *provider) Dialect() sqlstore.SQLDialect { + return provider.dialect +} + func (provider *provider) BunDBCtx(ctx context.Context) bun.IDB { return provider.bundb.BunDBCtx(ctx) } diff --git a/pkg/sqlstore/sqlitesqlstore/dialect.go b/pkg/sqlstore/sqlitesqlstore/dialect.go new file mode 100644 index 0000000000..14ac7d1d45 --- /dev/null +++ b/pkg/sqlstore/sqlitesqlstore/dialect.go @@ -0,0 +1,115 @@ +package sqlitesqlstore + +import ( + "context" + + "github.com/uptrace/bun" +) + +type SQLiteDialect struct { +} + +func (dialect *SQLiteDialect) MigrateIntToTimestamp(ctx context.Context, bun bun.IDB, table string, column string) error { + columnType, err := dialect.GetColumnType(ctx, bun, table, column) + if err != nil { + return err + } + + if columnType != "INTEGER" { + return nil + } + + // if the columns is integer then do this + if _, err := bun.ExecContext(ctx, `ALTER TABLE `+table+` RENAME COLUMN `+column+` TO `+column+`_old`); err != nil { + return err + } + + // add new timestamp column + if _, err := bun.NewAddColumn().Table(table).ColumnExpr(column + " TIMESTAMP").Exec(ctx); err != nil { + return err + } + + // copy data from old column to new column, converting from int (unix timestamp) to timestamp + if _, err := bun.NewUpdate(). + Table(table). + Set(column + " = datetime(" + column + "_old, 'unixepoch')"). + Where("1=1"). + Exec(ctx); err != nil { + return err + } + + // drop old column + if _, err := bun.NewDropColumn().Table(table).Column(column + "_old").Exec(ctx); err != nil { + return err + } + + return nil +} + +func (dialect *SQLiteDialect) MigrateIntToBoolean(ctx context.Context, bun bun.IDB, table string, column string) error { + columnType, err := dialect.GetColumnType(ctx, bun, table, column) + if err != nil { + return err + } + + if columnType != "INTEGER" { + return nil + } + + if _, err := bun.ExecContext(ctx, `ALTER TABLE `+table+` RENAME COLUMN `+column+` TO `+column+`_old`); err != nil { + return err + } + + // add new boolean column + if _, err := bun.NewAddColumn().Table(table).ColumnExpr(column + " BOOLEAN").Exec(ctx); err != nil { + return err + } + + // copy data from old column to new column, converting from int to boolean + if _, err := bun.NewUpdate(). + Table(table). + Set(column + " = CASE WHEN " + column + "_old = 1 THEN true ELSE false END"). + Where("1=1"). + Exec(ctx); err != nil { + return err + } + + // drop old column + if _, err := bun.NewDropColumn().Table(table).Column(column + "_old").Exec(ctx); err != nil { + return err + } + + return nil +} + +func (dialect *SQLiteDialect) GetColumnType(ctx context.Context, bun bun.IDB, table string, column string) (string, error) { + var columnType string + var err error + + err = bun.NewSelect(). + ColumnExpr("type"). + TableExpr("pragma_table_info(?)", table). + Where("name = ?", column). + Scan(ctx, &columnType) + + if err != nil { + return "", err + } + + return columnType, nil +} + +func (dialect *SQLiteDialect) ColumnExists(ctx context.Context, bun bun.IDB, table string, column string) (bool, error) { + var count int + err := bun.NewSelect(). + ColumnExpr("COUNT(*)"). + TableExpr("pragma_table_info(?)", table). + Where("name = ?", column). + Scan(ctx, &count) + + if err != nil { + return false, err + } + + return count > 0, nil +} diff --git a/pkg/sqlstore/sqlitesqlstore/provider.go b/pkg/sqlstore/sqlitesqlstore/provider.go index daf7361877..af617d40f8 100644 --- a/pkg/sqlstore/sqlitesqlstore/provider.go +++ b/pkg/sqlstore/sqlitesqlstore/provider.go @@ -17,6 +17,7 @@ type provider struct { sqldb *sql.DB bundb *sqlstore.BunDB sqlxdb *sqlx.DB + dialect *SQLiteDialect } func NewFactory(hookFactories ...factory.ProviderFactory[sqlstore.SQLStoreHook, sqlstore.Config]) factory.ProviderFactory[sqlstore.SQLStore, sqlstore.Config] { @@ -49,6 +50,7 @@ func New(ctx context.Context, providerSettings factory.ProviderSettings, config sqldb: sqldb, bundb: sqlstore.NewBunDB(settings, sqldb, sqlitedialect.New(), hooks), sqlxdb: sqlx.NewDb(sqldb, "sqlite3"), + dialect: &SQLiteDialect{}, }, nil } @@ -64,6 +66,10 @@ func (provider *provider) SQLxDB() *sqlx.DB { return provider.sqlxdb } +func (provider *provider) Dialect() sqlstore.SQLDialect { + return provider.dialect +} + func (provider *provider) BunDBCtx(ctx context.Context) bun.IDB { return provider.bundb.BunDBCtx(ctx) } diff --git a/pkg/sqlstore/sqlstore.go b/pkg/sqlstore/sqlstore.go index c9831d6b92..2e57da3ec8 100644 --- a/pkg/sqlstore/sqlstore.go +++ b/pkg/sqlstore/sqlstore.go @@ -20,6 +20,9 @@ type SQLStore interface { // SQLxDB returns an instance of sqlx.DB. This is the legacy ORM used. SQLxDB() *sqlx.DB + // Returns the dialect of the database. + Dialect() SQLDialect + // RunInTxCtx runs the given callback in a transaction. It creates and injects a new context with the transaction. // If a transaction is present in the context, it will be used. RunInTxCtx(ctx context.Context, opts *SQLStoreTxOptions, cb func(ctx context.Context) error) error @@ -32,3 +35,10 @@ type SQLStore interface { type SQLStoreHook interface { bun.QueryHook } + +type SQLDialect interface { + MigrateIntToTimestamp(ctx context.Context, bun bun.IDB, table string, column string) error + MigrateIntToBoolean(ctx context.Context, bun bun.IDB, table string, column string) error + GetColumnType(ctx context.Context, bun bun.IDB, table string, column string) (string, error) + ColumnExists(ctx context.Context, bun bun.IDB, table string, column string) (bool, error) +} diff --git a/pkg/sqlstore/sqlstoretest/dialect.go b/pkg/sqlstore/sqlstoretest/dialect.go new file mode 100644 index 0000000000..bc4228ef7d --- /dev/null +++ b/pkg/sqlstore/sqlstoretest/dialect.go @@ -0,0 +1,26 @@ +package sqlstoretest + +import ( + "context" + + "github.com/uptrace/bun" +) + +type TestDialect struct { +} + +func (dialect *TestDialect) MigrateIntToTimestamp(ctx context.Context, bun bun.IDB, table string, column string) error { + return nil +} + +func (dialect *TestDialect) MigrateIntToBoolean(ctx context.Context, bun bun.IDB, table string, column string) error { + return nil +} + +func (dialect *TestDialect) GetColumnType(ctx context.Context, bun bun.IDB, table string, column string) (string, error) { + return "", nil +} + +func (dialect *TestDialect) ColumnExists(ctx context.Context, bun bun.IDB, table string, column string) (bool, error) { + return false, nil +} diff --git a/pkg/sqlstore/sqlstoretest/provider.go b/pkg/sqlstore/sqlstoretest/provider.go index 471592980b..8ed1c3fdf8 100644 --- a/pkg/sqlstore/sqlstoretest/provider.go +++ b/pkg/sqlstore/sqlstoretest/provider.go @@ -15,10 +15,11 @@ import ( var _ sqlstore.SQLStore = (*Provider)(nil) type Provider struct { - db *sql.DB - mock sqlmock.Sqlmock - bunDB *bun.DB - sqlxDB *sqlx.DB + db *sql.DB + mock sqlmock.Sqlmock + bunDB *bun.DB + sqlxDB *sqlx.DB + dialect *TestDialect } func New(config sqlstore.Config, matcher sqlmock.QueryMatcher) *Provider { @@ -38,10 +39,11 @@ func New(config sqlstore.Config, matcher sqlmock.QueryMatcher) *Provider { } return &Provider{ - db: db, - mock: mock, - bunDB: bunDB, - sqlxDB: sqlxDB, + db: db, + mock: mock, + bunDB: bunDB, + sqlxDB: sqlxDB, + dialect: &TestDialect{}, } } @@ -61,6 +63,10 @@ func (provider *Provider) Mock() sqlmock.Sqlmock { return provider.mock } +func (provider *Provider) Dialect() sqlstore.SQLDialect { + return provider.dialect +} + func (provider *Provider) BunDBCtx(ctx context.Context) bun.IDB { return provider.bunDB } diff --git a/pkg/types/auditable.go b/pkg/types/auditable.go new file mode 100644 index 0000000000..29c2021575 --- /dev/null +++ b/pkg/types/auditable.go @@ -0,0 +1,13 @@ +package types + +import "time" + +type TimeAuditable struct { + CreatedAt time.Time `bun:"created_at" json:"createdAt"` + UpdatedAt time.Time `bun:"updated_at" json:"updatedAt"` +} + +type UserAuditable struct { + CreatedBy string `bun:"created_by" json:"createdBy"` + UpdatedBy string `bun:"updated_by" json:"updatedBy"` +} diff --git a/pkg/types/organization.go b/pkg/types/organization.go index 32da1a2601..3bbf92f331 100644 --- a/pkg/types/organization.go +++ b/pkg/types/organization.go @@ -1,8 +1,6 @@ package types import ( - "time" - "github.com/uptrace/bun" ) @@ -10,69 +8,16 @@ import ( type Organization struct { bun.BaseModel `bun:"table:organizations"` - ID string `bun:"id,pk,type:text"` - Name string `bun:"name,type:text,notnull"` - CreatedAt int `bun:"created_at,notnull"` - IsAnonymous int `bun:"is_anonymous,notnull,default:0,CHECK(is_anonymous IN (0,1))"` - HasOptedUpdates int `bun:"has_opted_updates,notnull,default:1,CHECK(has_opted_updates IN (0,1))"` -} - -type Invite struct { - bun.BaseModel `bun:"table:invites"` - - ID int `bun:"id,pk,autoincrement"` - Name string `bun:"name,type:text,notnull"` - Email string `bun:"email,type:text,notnull,unique"` - Token string `bun:"token,type:text,notnull"` - CreatedAt int `bun:"created_at,notnull"` - Role string `bun:"role,type:text,notnull"` - OrgID string `bun:"org_id,type:text,notnull"` -} - -type Group struct { - bun.BaseModel `bun:"table:groups"` - ID string `bun:"id,pk,type:text" json:"id"` - Name string `bun:"name,type:text,notnull,unique" json:"name"` -} - -type User struct { - bun.BaseModel `bun:"table:users"` - ID string `bun:"id,pk,type:text"` - Name string `bun:"name,type:text,notnull"` - Email string `bun:"email,type:text,notnull,unique"` - Password string `bun:"password,type:text,notnull"` - CreatedAt int `bun:"created_at,notnull"` - ProfilePictureURL string `bun:"profile_picture_url,type:text"` - GroupID string `bun:"group_id,type:text,notnull"` - OrgID string `bun:"org_id,type:text,notnull"` -} - -type ResetPasswordRequest struct { - bun.BaseModel `bun:"table:reset_password_request"` - ID int `bun:"id,pk,autoincrement"` - Token string `bun:"token,type:text,notnull"` - UserID string `bun:"user_id,type:text,notnull"` -} - -type UserFlags struct { - bun.BaseModel `bun:"table:user_flags"` - UserID string `bun:"user_id,pk,type:text,notnull"` - Flags string `bun:"flags,type:text"` + TimeAuditable + ID string `bun:"id,pk,type:text" json:"id"` + Name string `bun:"name,type:text,notnull" json:"name"` + IsAnonymous bool `bun:"is_anonymous,notnull,default:0,CHECK(is_anonymous IN (0,1))" json:"isAnonymous"` + HasOptedUpdates bool `bun:"has_opted_updates,notnull,default:1,CHECK(has_opted_updates IN (0,1))" json:"hasOptedUpdates"` } type ApdexSettings struct { - bun.BaseModel `bun:"table:apdex_settings"` - ServiceName string `bun:"service_name,pk,type:text"` - Threshold float64 `bun:"threshold,type:float,notnull"` - ExcludeStatusCodes string `bun:"exclude_status_codes,type:text,notnull"` -} - -type IngestionKey struct { - bun.BaseModel `bun:"table:ingestion_keys"` - KeyId string `bun:"key_id,pk,type:text"` - Name string `bun:"name,type:text"` - CreatedAt time.Time `bun:"created_at,default:current_timestamp"` - IngestionKey string `bun:"ingestion_key,type:text,notnull"` - IngestionURL string `bun:"ingestion_url,type:text,notnull"` - DataRegion string `bun:"data_region,type:text,notnull"` + OrgID string `bun:"org_id,pk,type:text" json:"orgId"` + ServiceName string `bun:"service_name,pk,type:text" json:"serviceName"` + Threshold float64 `bun:"threshold,type:float,notnull" json:"threshold"` + ExcludeStatusCodes string `bun:"exclude_status_codes,type:text,notnull" json:"excludeStatusCodes"` } diff --git a/pkg/types/user.go b/pkg/types/user.go new file mode 100644 index 0000000000..bf6fd59df1 --- /dev/null +++ b/pkg/types/user.go @@ -0,0 +1,54 @@ +package types + +import ( + "time" + + "github.com/uptrace/bun" +) + +type Invite struct { + bun.BaseModel `bun:"table:invites"` + + OrgID string `bun:"org_id,type:text,notnull" json:"orgId"` + ID int `bun:"id,pk,autoincrement" json:"id"` + Name string `bun:"name,type:text,notnull" json:"name"` + Email string `bun:"email,type:text,notnull,unique" json:"email"` + Token string `bun:"token,type:text,notnull" json:"token"` + CreatedAt time.Time `bun:"created_at,notnull" json:"createdAt"` + Role string `bun:"role,type:text,notnull" json:"role"` +} + +type Group struct { + bun.BaseModel `bun:"table:groups"` + + TimeAuditable + OrgID string `bun:"org_id,type:text"` + ID string `bun:"id,pk,type:text" json:"id"` + Name string `bun:"name,type:text,notnull,unique" json:"name"` +} + +type GettableUser struct { + User + Role string `json:"role"` + Organization string `json:"organization"` +} + +type User struct { + bun.BaseModel `bun:"table:users"` + + TimeAuditable + ID string `bun:"id,pk,type:text" json:"id"` + Name string `bun:"name,type:text,notnull" json:"name"` + Email string `bun:"email,type:text,notnull,unique" json:"email"` + Password string `bun:"password,type:text,notnull" json:"-"` + ProfilePictureURL string `bun:"profile_picture_url,type:text" json:"profilePictureURL"` + GroupID string `bun:"group_id,type:text,notnull" json:"groupId"` + OrgID string `bun:"org_id,type:text,notnull" json:"orgId"` +} + +type ResetPasswordRequest struct { + bun.BaseModel `bun:"table:reset_password_request"` + ID int `bun:"id,pk,autoincrement" json:"id"` + Token string `bun:"token,type:text,notnull" json:"token"` + UserID string `bun:"user_id,type:text,notnull" json:"userId"` +}