feat(organization): schema changes for the organizations entity (#7684)

* feat(organization): add hname and alias for organization

* fix: boolean values are not shown in the list panel's column

* fix: moved logic to component level

* fix: added type

* fix: added test cases

* fix: added test cases

* chore: update copy webpack plugin

* Revert "fix: display same key with multiple data types in filter suggestions by enhancing the deduping logic (#7255)"

This reverts commit 1e85981a17a8e715e948308d3e85072d976907d3.

* fix: use query search v2 for traces data source to handle multiple data types for the same key

* fix(QueryBuilderSearchV2): add user typed option if it doesn't exist in the payload

* fix(QueryBuilderSearchV2): increase the height of search dropdown for non-logs data sources

* fix: display span scope selector for trace data source

* chore: remove the span scope selector from qb search v1 and move the component to search v2

* fix: write test to ensure that we display span scope selector for traces data source

* fix: limit converting  ->   only to log data source

* fix: don't display empty suggestion if only spaces are typed

* chore: tests for span scope selector

* chore: qb search flow (key, operator, value) test cases

* refactor: fix the Maximum update depth reached issue while running tests

* chore: overall improvements to span scope selector tests

Resource attr filter: style fix and quick filter changes (#7691)

* chore: resource attr filter init

* chore: resource attr filter api integration

* chore: operator config updated

* chore: fliter show hide logic and styles

* chore: add support for custom operator list to qb

* chore: minor refactor

* chore: minor code refactor

* test: quick filters test suite added

* test: quick filters test suite added

* test: all errors test suite added

* chore: style fix

* test: all errors mock fix

* chore: test case fix and mixpanel update

* chore: color update

* chore: minor refactor

* chore: style fix

* chore: set default query in exceptions tab

* chore: style fix

* chore: minor refactor

* chore: minor refactor

* chore: minor refactor

* chore: test update

* chore: fix filter header with no query name

* fix: scroll fix

* chore: add data source traces to quick filters

* chore: replace div with fragment

---------

Co-authored-by: Aditya Singh <adityasingh@Adityas-MacBook-Pro.local>

fix: handle rate operators for table panel (#7695)

* fix: handle rate operators for table panel

chore: fix error rate (#7701)

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* feat(organization): minor cleanups

* feat(organization): better naming for api and usecase

* feat(organization): better packaging for modules

* feat(organization): change hname to displayName

* feat(organization): update the migration to use dialect

* feat(organization): update the migration to use dialect

* feat(organization): update the migration to use dialect

* feat(organization): revert back to impl

* feat(organization): remove DI from organization

* feat(organization): address review comments

* feat(organization): address review comments

* feat(organization): address review comments

---------

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
This commit is contained in:
Vikrant Gupta 2025-04-25 19:38:15 +05:30 committed by GitHub
parent a1846c008a
commit c322657666
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
51 changed files with 690 additions and 378 deletions

View File

@ -12,6 +12,7 @@ import (
"github.com/SigNoz/signoz/ee/query-service/usage"
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/apis/fields"
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
"github.com/SigNoz/signoz/pkg/modules/preference"
preferencecore "github.com/SigNoz/signoz/pkg/modules/preference/core"
baseapp "github.com/SigNoz/signoz/pkg/query-service/app"
@ -59,6 +60,8 @@ type APIHandler struct {
// NewAPIHandler returns an APIHandler
func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz) (*APIHandler, error) {
preference := preference.NewAPI(preferencecore.NewPreference(preferencecore.NewStore(signoz.SQLStore), preferencetypes.NewDefaultPreferenceMap()))
organizationAPI := implorganization.NewAPI(implorganization.NewModule(implorganization.NewStore(signoz.SQLStore)))
organizationModule := implorganization.NewModule(implorganization.NewStore(signoz.SQLStore))
baseHandler, err := baseapp.NewAPIHandler(baseapp.APIHandlerOpts{
Reader: opts.DataConnector,
@ -78,6 +81,8 @@ func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz) (*APIHandler,
FieldsAPI: fields.NewAPI(signoz.TelemetryStore),
Signoz: signoz,
Preference: preference,
OrganizationAPI: organizationAPI,
OrganizationModule: organizationModule,
})
if err != nil {

View File

@ -134,7 +134,7 @@ func (ah *APIHandler) registerUser(w http.ResponseWriter, r *http.Request) {
return
}
_, registerError := baseauth.Register(ctx, req, ah.Signoz.Alertmanager)
_, registerError := baseauth.Register(ctx, req, ah.Signoz.Alertmanager, ah.OrganizationModule)
if !registerError.IsNil() {
RespondError(w, apierr, nil)
return
@ -151,9 +151,8 @@ func (ah *APIHandler) registerUser(w http.ResponseWriter, r *http.Request) {
func (ah *APIHandler) getInvite(w http.ResponseWriter, r *http.Request) {
token := mux.Vars(r)["token"]
sourceUrl := r.URL.Query().Get("ref")
ctx := context.Background()
inviteObject, err := baseauth.GetInvite(context.Background(), token)
inviteObject, err := baseauth.GetInvite(r.Context(), token, ah.OrganizationModule)
if err != nil {
RespondError(w, model.BadRequest(err), nil)
return
@ -163,7 +162,7 @@ func (ah *APIHandler) getInvite(w http.ResponseWriter, r *http.Request) {
InvitationResponseObject: inviteObject,
}
precheck, apierr := ah.AppDao().PrecheckLogin(ctx, inviteObject.Email, sourceUrl)
precheck, apierr := ah.AppDao().PrecheckLogin(r.Context(), inviteObject.Email, sourceUrl)
resp.Precheck = precheck
if apierr != nil {

View File

@ -9,10 +9,9 @@ import (
"net/http"
"time"
"github.com/pkg/errors"
"github.com/SigNoz/signoz/ee/query-service/constants"
"github.com/SigNoz/signoz/ee/query-service/model"
"github.com/pkg/errors"
)
var C *Client

View File

@ -138,15 +138,6 @@ func (lm *Manager) UploadUsage() {
zap.L().Info("uploading usage data")
orgName := ""
orgNames, orgError := lm.modelDao.GetOrgs(ctx)
if orgError != nil {
zap.L().Error("failed to get org data: %v", zap.Error(orgError))
}
if len(orgNames) == 1 {
orgName = orgNames[0].Name
}
usagesPayload := []model.Usage{}
for _, usage := range usages {
usageDataBytes, err := encryption.Decrypt([]byte(usage.ExporterID[:32]), []byte(usage.Data))
@ -166,7 +157,7 @@ func (lm *Manager) UploadUsage() {
usageData.ExporterID = usage.ExporterID
usageData.Type = usage.Type
usageData.Tenant = "default"
usageData.OrgName = orgName
usageData.OrgName = "default"
usageData.TenantId = lm.tenantID
usagesPayload = append(usagesPayload, usageData)
}

View File

@ -79,6 +79,14 @@ func (dialect *dialect) MigrateIntToTimestamp(ctx context.Context, bun bun.IDB,
}
func (dialect *dialect) MigrateIntToBoolean(ctx context.Context, bun bun.IDB, table string, column string) error {
columnExists, err := dialect.ColumnExists(ctx, bun, table, column)
if err != nil {
return err
}
if !columnExists {
return nil
}
columnType, err := dialect.GetColumnType(ctx, bun, table, column)
if err != nil {
return err
@ -151,6 +159,26 @@ func (dialect *dialect) ColumnExists(ctx context.Context, bun bun.IDB, table str
return count > 0, nil
}
func (dialect *dialect) AddColumn(ctx context.Context, bun bun.IDB, table string, column string, columnExpr string) error {
exists, err := dialect.ColumnExists(ctx, bun, table, column)
if err != nil {
return err
}
if !exists {
_, err = bun.
NewAddColumn().
Table(table).
ColumnExpr(column + " " + columnExpr).
Exec(ctx)
if err != nil {
return err
}
}
return nil
}
func (dialect *dialect) RenameColumn(ctx context.Context, bun bun.IDB, table string, oldColumnName string, newColumnName string) (bool, error) {
oldColumnExists, err := dialect.ColumnExists(ctx, bun, table, oldColumnName)
if err != nil {
@ -162,10 +190,14 @@ func (dialect *dialect) RenameColumn(ctx context.Context, bun bun.IDB, table str
return false, err
}
if !oldColumnExists && newColumnExists {
if newColumnExists {
return true, nil
}
if !oldColumnExists {
return false, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, fmt.Sprintf("old column: %s doesn't exist", oldColumnName))
}
_, err = bun.
ExecContext(ctx, "ALTER TABLE "+table+" RENAME COLUMN "+oldColumnName+" TO "+newColumnName)
if err != nil {
@ -174,6 +206,26 @@ func (dialect *dialect) RenameColumn(ctx context.Context, bun bun.IDB, table str
return true, nil
}
func (dialect *dialect) DropColumn(ctx context.Context, bun bun.IDB, table string, column string) error {
exists, err := dialect.ColumnExists(ctx, bun, table, column)
if err != nil {
return err
}
if exists {
_, err = bun.
NewDropColumn().
Table(table).
Column(column).
Exec(ctx)
if err != nil {
return err
}
}
return nil
}
func (dialect *dialect) TableExists(ctx context.Context, bun bun.IDB, table interface{}) (bool, error) {
count := 0

View File

@ -64,7 +64,7 @@ function App(): JSX.Element {
// wait for the required data to be loaded before doing init for anything!
if (!isFetchingActiveLicenseV3 && activeLicenseV3 && org) {
const orgName =
org && Array.isArray(org) && org.length > 0 ? org[0].name : '';
org && Array.isArray(org) && org.length > 0 ? org[0].displayName : '';
const { name, email, role } = user;

View File

@ -1,4 +1,4 @@
import axios from 'api';
import { ApiV2Instance as axios } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
@ -8,14 +8,12 @@ const editOrg = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.put(`/org/${props.orgId}`, {
name: props.name,
isAnonymous: props.isAnonymous,
hasOptedUpdates: props.hasOptedUpdates,
const response = await axios.put(`/orgs/me`, {
displayName: props.displayName,
});
return {
statusCode: 200,
statusCode: 204,
error: null,
message: response.data.status,
payload: response.data,

View File

@ -160,7 +160,7 @@ export default function CustomDomainSettings(): JSX.Element {
{!isLoadingDeploymentsData && (
<Card className="custom-domain-settings-card">
<div className="custom-domain-settings-content-header">
Team {org?.[0]?.name} Information
Team {org?.[0]?.displayName} Information
</div>
<div className="custom-domain-settings-content-body">

View File

@ -13,8 +13,7 @@ import { useTranslation } from 'react-i18next';
export interface OrgData {
id: string;
isAnonymous: boolean;
name: string;
displayName: string;
}
export interface OrgDetails {
@ -110,15 +109,14 @@ function OrgQuestions({
try {
setIsLoading(true);
const { statusCode, error } = await editOrg({
isAnonymous: currentOrgData.isAnonymous,
name: organisationName,
displayName: organisationName,
orgId: currentOrgData.id,
});
if (statusCode === 200) {
updateOrg(currentOrgData?.id, orgDetails.organisationName);
if (statusCode === 204) {
updateOrg(currentOrgData?.id, organisationName);
logEvent('Org Onboarding: Org Name Updated', {
organisationName: orgDetails.organisationName,
organisationName,
});
logEvent('Org Onboarding: Answered', {

View File

@ -94,7 +94,7 @@ function OnboardingQuestionaire(): JSX.Element {
setOrgDetails({
...orgDetails,
organisationName: org[0].name,
organisationName: org[0].displayName,
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps

View File

@ -390,7 +390,7 @@ function OnboardingAddDataSource(): JSX.Element {
setSetupStepItems([
{
...setupStepItemsBase[0],
description: org?.[0]?.name || '',
description: org?.[0]?.displayName || '',
},
...setupStepItemsBase.slice(1),
]);
@ -403,7 +403,7 @@ function OnboardingAddDataSource(): JSX.Element {
setSetupStepItems([
{
...setupStepItemsBase[0],
description: org?.[0]?.name || '',
description: org?.[0]?.displayName || '',
},
{
...setupStepItemsBase[1],
@ -415,7 +415,7 @@ function OnboardingAddDataSource(): JSX.Element {
setSetupStepItems([
{
...setupStepItemsBase[0],
description: org?.[0]?.name || '',
description: org?.[0]?.displayName || '',
},
{
...setupStepItemsBase[1],

View File

@ -7,27 +7,22 @@ import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { requireErrorMessage } from 'utils/form/requireErrorMessage';
function DisplayName({
index,
id: orgId,
isAnonymous,
}: DisplayNameProps): JSX.Element {
function DisplayName({ index, id: orgId }: DisplayNameProps): JSX.Element {
const [form] = Form.useForm<FormValues>();
const orgName = Form.useWatch('name', form);
const orgName = Form.useWatch('displayName', form);
const { t } = useTranslation(['organizationsettings', 'common']);
const { org, updateOrg } = useAppContext();
const { name } = (org || [])[index];
const { displayName } = (org || [])[index];
const [isLoading, setIsLoading] = useState<boolean>(false);
const { notifications } = useNotifications();
const onSubmit = async (values: FormValues): Promise<void> => {
try {
setIsLoading(true);
const { name } = values;
const { displayName } = values;
const { statusCode, error } = await editOrg({
isAnonymous,
name,
displayName,
orgId,
});
if (statusCode === 200) {
@ -36,7 +31,7 @@ function DisplayName({
ns: 'common',
}),
});
updateOrg(orgId, name);
updateOrg(orgId, displayName);
} else {
notifications.error({
message:
@ -61,18 +56,18 @@ function DisplayName({
return <div />;
}
const isDisabled = isLoading || orgName === name || !orgName;
const isDisabled = isLoading || orgName === displayName || !orgName;
return (
<Form
initialValues={{ name }}
initialValues={{ displayName }}
form={form}
layout="vertical"
onFinish={onSubmit}
autoComplete="off"
>
<Form.Item
name="name"
name="displayName"
label="Display name"
rules={[{ required: true, message: requireErrorMessage('Display name') }]}
>
@ -95,11 +90,10 @@ function DisplayName({
interface DisplayNameProps {
index: number;
id: IUser['id'];
isAnonymous: boolean;
}
interface FormValues {
name: string;
displayName: string;
}
export default DisplayName;

View File

@ -23,12 +23,7 @@ function OrganizationSettings(): JSX.Element {
<>
<Space direction="vertical">
{org.map((e, index) => (
<DisplayName
isAnonymous={e.isAnonymous}
key={e.id}
id={e.id}
index={index}
/>
<DisplayName key={e.id} id={e.id} index={index} />
))}
</Space>
<Divider />

View File

@ -1,4 +1,4 @@
import { Button, Form, Input, Space, Switch, Typography } from 'antd';
import { Button, Form, Input, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import getInviteDetails from 'api/user/getInviteDetails';
import loginApi from 'api/user/login';
@ -14,13 +14,7 @@ import { useQuery } from 'react-query';
import { useLocation } from 'react-router-dom';
import { PayloadProps as LoginPrecheckPayloadProps } from 'types/api/user/loginPrecheck';
import {
ButtonContainer,
FormContainer,
FormWrapper,
Label,
MarginTop,
} from './styles';
import { ButtonContainer, FormContainer, FormWrapper, Label } from './styles';
import { isPasswordNotValidMessage, isPasswordValid } from './utils';
const { Title } = Typography;
@ -111,24 +105,15 @@ function SignUp({ version }: SignUpProps): JSX.Element {
const isPreferenceVisible = token === null;
const commonHandler = async (
values: FormValues,
isPreferenceVisible: boolean,
): Promise<void> => {
const commonHandler = async (values: FormValues): Promise<void> => {
try {
const { organizationName, password, firstName, email } = values;
const response = await signUpApi({
email,
name: firstName,
orgName: organizationName,
orgDisplayName: organizationName,
password,
token: params.get('token') || undefined,
...(isPreferenceVisible
? {
isAnonymous: values.isAnonymous,
hasOptedUpdates: values.hasOptedUpdates,
}
: {}),
});
if (response.statusCode === 200) {
@ -171,7 +156,7 @@ function SignUp({ version }: SignUpProps): JSX.Element {
const response = await signUpApi({
email: values.email,
name: values.firstName,
orgName: values.organizationName,
orgDisplayName: values.organizationName,
password: values.password,
token: params.get('token') || undefined,
sourceUrl: encodeURIComponent(window.location.href),
@ -221,14 +206,14 @@ function SignUp({ version }: SignUpProps): JSX.Element {
}
if (isPreferenceVisible) {
await commonHandler(values, true);
await commonHandler(values);
} else {
logEvent('Account Created Successfully', {
email: values.email,
name: values.firstName,
});
await commonHandler(values, false);
await commonHandler(values);
}
setLoading(false);
@ -278,7 +263,6 @@ function SignUp({ version }: SignUpProps): JSX.Element {
<FormContainer
onFinish={!precheck.sso ? handleSubmit : handleSubmitSSO}
onValuesChange={handleValuesChange}
initialValues={{ hasOptedUpdates: true, isAnonymous: false }}
form={form}
>
<Title level={4}>Create your account</Title>
@ -359,34 +343,6 @@ function SignUp({ version }: SignUpProps): JSX.Element {
)}
</div>
)}
{isPreferenceVisible && (
<>
<MarginTop marginTop="2.4375rem">
<Space>
<FormContainer.Item
noStyle
name="hasOptedUpdates"
valuePropName="checked"
>
<Switch />
</FormContainer.Item>
<Typography>{t('prompt_keepme_posted')} </Typography>
</Space>
</MarginTop>
<MarginTop marginTop="0.5rem">
<Space>
<FormContainer.Item noStyle name="isAnonymous" valuePropName="checked">
<Switch />
</FormContainer.Item>
<Typography>{t('prompt_anonymise')}</Typography>
</Space>
</MarginTop>
</>
)}
{isPreferenceVisible && (
<Typography.Paragraph
italic

View File

@ -82,10 +82,8 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
return [
{
createdAt: 0,
hasOptedUpdates: false,
id: userData.payload.orgId,
isAnonymous: false,
name: userData.payload.organization,
displayName: userData.payload.organization,
},
];
}
@ -95,10 +93,8 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
...prev.slice(0, orgIndex),
{
createdAt: 0,
hasOptedUpdates: false,
id: userData.payload.orgId,
isAnonymous: false,
name: userData.payload.organization,
displayName: userData.payload.organization,
},
...prev.slice(orgIndex + 1, prev.length),
];
@ -209,10 +205,8 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
...org.slice(0, orgIndex),
{
createdAt: 0,
hasOptedUpdates: false,
id: orgId,
isAnonymous: false,
name: updatedOrgName,
displayName: updatedOrgName,
},
...org.slice(orgIndex + 1, org.length),
];

View File

@ -156,10 +156,8 @@ export function getAppContextMock(
org: [
{
createdAt: 0,
hasOptedUpdates: false,
id: 'does-not-matter-id',
isAnonymous: false,
name: 'Pentagon',
displayName: 'Pentagon',
},
],
isFetchingUser: false,

View File

@ -1,8 +1,6 @@
export interface Props {
name: string;
isAnonymous: boolean;
displayName: string;
orgId: string;
hasOptedUpdates?: boolean;
}
export interface PayloadProps {

View File

@ -14,6 +14,6 @@ export interface PayloadProps {
name: User['name'];
role: ROLES;
token: string;
organization: Organization['name'];
organization: Organization['displayName'];
precheck?: LoginPrecheckPayloadProps;
}

View File

@ -1,9 +1,7 @@
export interface Organization {
createdAt: number;
hasOptedUpdates: boolean;
id: string;
isAnonymous: boolean;
name: string;
displayName: string;
}
export type PayloadProps = Organization[];

View File

@ -1,10 +1,8 @@
export interface Props {
name: string;
orgName: string;
orgDisplayName: string;
email: string;
password: string;
token?: string;
sourceUrl?: string;
isAnonymous?: boolean;
hasOptedUpdates?: boolean;
}

View File

@ -2,6 +2,8 @@ export type Created = 201;
export type Success = 200;
export type SuccessNoContent = 204;
export type Forbidden = 403;
export type BadRequest = 400;
@ -14,7 +16,7 @@ export type Conflict = 409;
export type ServerError = 500;
export type SuccessStatusCode = Created | Success;
export type SuccessStatusCode = Created | Success | SuccessNoContent;
export type ErrorStatusCode =
| Forbidden

View File

@ -33,7 +33,7 @@ export const loginApi = async (page: Page): Promise<void> => {
body: JSON.stringify(loginApiResponse),
}),
),
page.route(`**/org/${userLoginResponse.orgId}`, (route) =>
page.route(`**/orgs/me`, (route) =>
route.fulfill({
status: 200,
body: JSON.stringify(updateOrgResponse),

View File

@ -0,0 +1,80 @@
package implorganization
import (
"encoding/json"
"net/http"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type organizationAPI struct {
module organization.Module
}
func NewAPI(module organization.Module) organization.API {
return &organizationAPI{module: module}
}
func (api *organizationAPI) Get(rw http.ResponseWriter, r *http.Request) {
claims, ok := authtypes.ClaimsFromContext(r.Context())
if !ok {
render.Error(rw, errors.Newf(errors.TypeUnauthenticated, errors.CodeUnauthenticated, "unauthenticated"))
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid org id"))
return
}
organization, err := api.module.Get(r.Context(), orgID)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, organization)
}
func (api *organizationAPI) GetAll(rw http.ResponseWriter, r *http.Request) {
organizations, err := api.module.GetAll(r.Context())
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, organizations)
}
func (api *organizationAPI) Update(rw http.ResponseWriter, r *http.Request) {
claims, ok := authtypes.ClaimsFromContext(r.Context())
if !ok {
render.Error(rw, errors.Newf(errors.TypeUnauthenticated, errors.CodeUnauthenticated, "unauthenticated"))
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid org id"))
return
}
var req *types.Organization
err = json.NewDecoder(r.Body).Decode(&req)
if err != nil {
render.Error(rw, err)
}
req.ID = orgID
err = api.module.Update(r.Context(), req)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusNoContent, nil)
}

View File

@ -0,0 +1,33 @@
package implorganization
import (
"context"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/valuer"
)
type organizationModule struct {
store types.OrganizationStore
}
func NewModule(organizationStore types.OrganizationStore) organization.Module {
return &organizationModule{store: organizationStore}
}
func (o *organizationModule) Create(ctx context.Context, organization *types.Organization) error {
return o.store.Create(ctx, organization)
}
func (o *organizationModule) Get(ctx context.Context, id valuer.UUID) (*types.Organization, error) {
return o.store.Get(ctx, id)
}
func (o *organizationModule) GetAll(ctx context.Context) ([]*types.Organization, error) {
return o.store.GetAll(ctx)
}
func (o *organizationModule) Update(ctx context.Context, updatedOrganization *types.Organization) error {
return o.store.Update(ctx, updatedOrganization)
}

View File

@ -0,0 +1,102 @@
package implorganization
import (
"context"
"database/sql"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/valuer"
)
type store struct {
store sqlstore.SQLStore
}
func NewStore(db sqlstore.SQLStore) types.OrganizationStore {
return &store{store: db}
}
func (s *store) Create(ctx context.Context, organization *types.Organization) error {
_, err := s.
store.
BunDB().
NewInsert().
Model(organization).
Exec(ctx)
if err != nil {
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to create organization")
}
return nil
}
func (s *store) Get(ctx context.Context, id valuer.UUID) (*types.Organization, error) {
organization := new(types.Organization)
err := s.
store.
BunDB().
NewSelect().
Model(organization).
Where("id = ?", id.StringValue()).
Scan(ctx)
if err != nil {
if err == sql.ErrNoRows {
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "no organization found with id: %s", id.StringValue())
}
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to get organization with id: %s", id.StringValue())
}
return organization, nil
}
func (s *store) GetAll(ctx context.Context) ([]*types.Organization, error) {
organizations := make([]*types.Organization, 0)
err := s.
store.
BunDB().
NewSelect().
Model(&organizations).
Scan(ctx)
if err != nil {
if err == sql.ErrNoRows {
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "no organizations found")
}
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to get all organizations")
}
return organizations, nil
}
func (s *store) Update(ctx context.Context, organization *types.Organization) error {
_, err := s.
store.
BunDB().
NewUpdate().
Model(organization).
Set("display_name = ?", organization.DisplayName).
Set("updated_at = ?", time.Now()).
Where("id = ?", organization.ID.StringValue()).
Exec(ctx)
if err != nil {
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to update organization with id: %s", organization.ID.StringValue())
}
return nil
}
func (s *store) Delete(ctx context.Context, id valuer.UUID) error {
_, err := s.
store.
BunDB().
NewDelete().
Model(new(types.Organization)).
Where("id = ?", id.StringValue()).
Exec(ctx)
if err != nil {
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to delete organization with id: %s", id.StringValue())
}
return nil
}

View File

@ -0,0 +1,34 @@
package organization
import (
"context"
"net/http"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/valuer"
)
type Module interface {
// Create creates the given organization
Create(context.Context, *types.Organization) error
// Get gets the organization based on the given id
Get(context.Context, valuer.UUID) (*types.Organization, error)
// GetAll gets all the organizations
GetAll(context.Context) ([]*types.Organization, error)
// Update updates the given organization based on id present
Update(context.Context, *types.Organization) error
}
type API interface {
// Get gets the organization based on the id in claims
Get(http.ResponseWriter, *http.Request)
// GetAll gets all the organizations
GetAll(http.ResponseWriter, *http.Request)
// Update updates the organization based on the id in claims
Update(http.ResponseWriter, *http.Request)
}

View File

@ -15,7 +15,6 @@ type API interface {
GetOrgPreference(http.ResponseWriter, *http.Request)
UpdateOrgPreference(http.ResponseWriter, *http.Request)
GetAllOrgPreferences(http.ResponseWriter, *http.Request)
GetUserPreference(http.ResponseWriter, *http.Request)
UpdateUserPreference(http.ResponseWriter, *http.Request)
GetAllUserPreferences(http.ResponseWriter, *http.Request)

View File

@ -10,7 +10,6 @@ type Usecase interface {
GetOrgPreference(ctx context.Context, preferenceId string, orgId string) (*preferencetypes.GettablePreference, error)
UpdateOrgPreference(ctx context.Context, preferenceId string, preferenceValue interface{}, orgId string) error
GetAllOrgPreferences(ctx context.Context, orgId string) ([]*preferencetypes.PreferenceWithValue, error)
GetUserPreference(ctx context.Context, preferenceId string, orgId string, userId string) (*preferencetypes.GettablePreference, error)
UpdateUserPreference(ctx context.Context, preferenceId string, preferenceValue interface{}, userId string) error
GetAllUserPreferences(ctx context.Context, orgId string, userId string) ([]*preferencetypes.PreferenceWithValue, error)

View File

@ -4,6 +4,8 @@ import (
"context"
"testing"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
"github.com/SigNoz/signoz/pkg/query-service/auth"
"github.com/SigNoz/signoz/pkg/query-service/constants"
"github.com/SigNoz/signoz/pkg/query-service/dao"
@ -20,7 +22,8 @@ func TestRegenerateConnectionUrlWithUpdatedConfig(t *testing.T) {
controller, err := NewController(sqlStore)
require.NoError(err)
user, apiErr := createTestUser()
organizationModule := implorganization.NewModule(implorganization.NewStore(sqlStore))
user, apiErr := createTestUser(organizationModule)
require.Nil(apiErr)
// should be able to generate connection url for
@ -66,8 +69,8 @@ func TestAgentCheckIns(t *testing.T) {
sqlStore := utils.NewQueryServiceDBForTests(t)
controller, err := NewController(sqlStore)
require.NoError(err)
user, apiErr := createTestUser()
organizationModule := implorganization.NewModule(implorganization.NewStore(sqlStore))
user, apiErr := createTestUser(organizationModule)
require.Nil(apiErr)
// An agent should be able to check in from a cloud account even
@ -118,7 +121,7 @@ func TestAgentCheckIns(t *testing.T) {
// After disconnecting existing account record, the agent should be able to
// connected for a particular cloud account id
_, apiErr = controller.DisconnectAccount(
_, _ = controller.DisconnectAccount(
context.TODO(), user.OrgID, "aws", testAccountId1,
)
@ -153,7 +156,8 @@ func TestCantDisconnectNonExistentAccount(t *testing.T) {
controller, err := NewController(sqlStore)
require.NoError(err)
user, apiErr := createTestUser()
organizationModule := implorganization.NewModule(implorganization.NewStore(sqlStore))
user, apiErr := createTestUser(organizationModule)
require.Nil(apiErr)
// Attempting to disconnect a non-existent account should return error
@ -171,7 +175,8 @@ func TestConfigureService(t *testing.T) {
controller, err := NewController(sqlStore)
require.NoError(err)
user, apiErr := createTestUser()
organizationModule := implorganization.NewModule(implorganization.NewStore(sqlStore))
user, apiErr := createTestUser(organizationModule)
require.Nil(apiErr)
// create a connected account
@ -286,19 +291,18 @@ func makeTestConnectedAccount(t *testing.T, orgId string, controller *Controller
return acc
}
func createTestUser() (*types.User, *model.ApiError) {
func createTestUser(organizationModule organization.Module) (*types.User, *model.ApiError) {
// Create a test user for auth
ctx := context.Background()
org, apiErr := dao.DB().CreateOrg(ctx, &types.Organization{
Name: "test",
})
if apiErr != nil {
return nil, apiErr
organization := types.NewOrganization("test")
err := organizationModule.Create(ctx, organization)
if err != nil {
return nil, model.InternalError(err)
}
group, apiErr := dao.DB().GetGroupByName(ctx, constants.AdminGroup)
if apiErr != nil {
return nil, apiErr
return nil, model.InternalError(apiErr)
}
auth.InitAuthCache(ctx)
@ -311,7 +315,7 @@ func createTestUser() (*types.User, *model.ApiError) {
Name: "test",
Email: userId[:8] + "test@test.com",
Password: "test",
OrgID: org.ID,
OrgID: organization.ID.StringValue(),
GroupID: group.ID,
},
true,

View File

@ -22,6 +22,7 @@ import (
"github.com/SigNoz/signoz/pkg/apis/fields"
errorsV2 "github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/preference"
"github.com/SigNoz/signoz/pkg/query-service/app/integrations"
"github.com/SigNoz/signoz/pkg/query-service/app/metricsexplorer"
@ -148,6 +149,9 @@ type APIHandler struct {
Signoz *signoz.SigNoz
Preference preference.API
OrganizationAPI organization.API
OrganizationModule organization.Module
}
type APIHandlerOpts struct {
@ -196,7 +200,9 @@ type APIHandlerOpts struct {
Signoz *signoz.SigNoz
Preference preference.API
Preference preference.API
OrganizationAPI organization.API
OrganizationModule organization.Module
}
// NewAPIHandler returns an APIHandler
@ -267,6 +273,8 @@ func NewAPIHandler(opts APIHandlerOpts) (*APIHandler, error) {
Signoz: opts.Signoz,
Preference: opts.Preference,
FieldsAPI: opts.FieldsAPI,
OrganizationAPI: opts.OrganizationAPI,
OrganizationModule: opts.OrganizationModule,
}
logsQueryBuilder := logsv3.PrepareLogsQuery
@ -623,11 +631,12 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *AuthMiddleware) {
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)
router.HandleFunc("/api/v1/org", am.AdminAccess(aH.getOrgs)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/org/{id}", am.AdminAccess(aH.getOrg)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/org/{id}", am.AdminAccess(aH.editOrg)).Methods(http.MethodPut)
router.HandleFunc("/api/v1/orgUsers/{id}", am.AdminAccess(aH.getOrgUsers)).Methods(http.MethodGet)
router.HandleFunc("/api/v2/orgs", am.AdminAccess(aH.getOrgs)).Methods(http.MethodGet)
router.HandleFunc("/api/v2/orgs/me", am.AdminAccess(aH.getOrg)).Methods(http.MethodGet)
router.HandleFunc("/api/v2/orgs/me", am.AdminAccess(aH.updateOrg)).Methods(http.MethodPut)
router.HandleFunc("/api/v1/getResetPasswordToken/{id}", am.AdminAccess(aH.getResetPasswordToken)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/resetPassword", am.OpenAccess(aH.resetPassword)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/changePassword/{id}", am.SelfAccess(aH.changePassword)).Methods(http.MethodPost)
@ -2058,7 +2067,7 @@ func (aH *APIHandler) inviteUsers(w http.ResponseWriter, r *http.Request) {
func (aH *APIHandler) getInvite(w http.ResponseWriter, r *http.Request) {
token := mux.Vars(r)["token"]
resp, err := auth.GetInvite(context.Background(), token)
resp, err := auth.GetInvite(context.Background(), token, aH.OrganizationModule)
if err != nil {
RespondError(w, &model.ApiError{Err: err, Typ: model.ErrorNotFound}, nil)
return
@ -2096,10 +2105,13 @@ func (aH *APIHandler) listPendingInvites(w http.ResponseWriter, r *http.Request)
// we should include org name field in the invite table, or do a join query.
var resp []*model.InvitationResponseObject
for _, inv := range invites {
org, apiErr := dao.DB().GetOrg(ctx, inv.OrgID)
if apiErr != nil {
RespondError(w, apiErr, nil)
orgID, err := valuer.NewUUID(inv.OrgID)
if err != nil {
render.Error(w, errorsV2.Newf(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, "invalid org_id in the invite"))
}
org, err := aH.OrganizationModule.Get(ctx, orgID)
if err != nil {
render.Error(w, errorsV2.Newf(errorsV2.TypeInternal, errorsV2.CodeInternal, err.Error()))
}
resp = append(resp, &model.InvitationResponseObject{
Name: inv.Name,
@ -2124,7 +2136,7 @@ func (aH *APIHandler) registerUser(w http.ResponseWriter, r *http.Request) {
return
}
_, apiErr := auth.Register(context.Background(), req, aH.Signoz.Alertmanager)
_, apiErr := auth.Register(context.Background(), req, aH.Signoz.Alertmanager, aH.OrganizationModule)
if apiErr != nil {
RespondError(w, apiErr, nil)
return
@ -2398,49 +2410,15 @@ func (aH *APIHandler) editRole(w http.ResponseWriter, r *http.Request) {
}
func (aH *APIHandler) getOrgs(w http.ResponseWriter, r *http.Request) {
orgs, apiErr := dao.DB().GetOrgs(context.Background())
if apiErr != nil {
RespondError(w, apiErr, "Failed to fetch orgs from the DB")
return
}
aH.WriteJSON(w, r, orgs)
aH.OrganizationAPI.GetAll(w, r)
}
func (aH *APIHandler) getOrg(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
org, apiErr := dao.DB().GetOrg(context.Background(), id)
if apiErr != nil {
RespondError(w, apiErr, "Failed to fetch org from the DB")
return
}
aH.WriteJSON(w, r, org)
aH.OrganizationAPI.Get(w, r)
}
func (aH *APIHandler) editOrg(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
req, err := parseEditOrgRequest(r)
if aH.HandleError(w, err, http.StatusBadRequest) {
return
}
req.ID = id
if apiErr := dao.DB().EditOrg(context.Background(), req); apiErr != nil {
RespondError(w, apiErr, "Failed to update org in the DB")
return
}
data := map[string]interface{}{
"hasOptedUpdates": req.HasOptedUpdates,
"isAnonymous": req.IsAnonymous,
"organizationName": req.Name,
}
claims, ok := authtypes.ClaimsFromContext(r.Context())
if !ok {
zap.L().Error("failed to get user email from jwt")
}
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_ORG_SETTINGS, data, claims.Email, true, false)
aH.WriteJSON(w, r, map[string]string{"data": "org updated successfully"})
func (aH *APIHandler) updateOrg(w http.ResponseWriter, r *http.Request) {
aH.OrganizationAPI.Update(w, r)
}
func (aH *APIHandler) getOrgUsers(w http.ResponseWriter, r *http.Request) {

View File

@ -4,6 +4,7 @@ import (
"context"
"testing"
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
_ "github.com/mattn/go-sqlite3"
"github.com/stretchr/testify/require"
)
@ -11,10 +12,11 @@ import (
func TestIntegrationLifecycle(t *testing.T) {
require := require.New(t)
mgr := NewTestIntegrationsManager(t)
mgr, store := NewTestIntegrationsManager(t)
ctx := context.Background()
user, apiErr := createTestUser()
organizationModule := implorganization.NewModule(implorganization.NewStore(store))
user, apiErr := createTestUser(organizationModule)
if apiErr != nil {
t.Fatalf("could not create test user: %v", apiErr)
}

View File

@ -5,19 +5,21 @@ import (
"slices"
"testing"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/query-service/auth"
"github.com/SigNoz/signoz/pkg/query-service/constants"
"github.com/SigNoz/signoz/pkg/query-service/dao"
"github.com/SigNoz/signoz/pkg/query-service/model"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
"github.com/SigNoz/signoz/pkg/query-service/utils"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/pipelinetypes"
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/google/uuid"
)
func NewTestIntegrationsManager(t *testing.T) *Manager {
func NewTestIntegrationsManager(t *testing.T) (*Manager, sqlstore.SQLStore) {
testDB := utils.NewQueryServiceDBForTests(t)
installedIntegrationsRepo, err := NewInstalledIntegrationsSqliteRepo(testDB)
@ -28,22 +30,21 @@ func NewTestIntegrationsManager(t *testing.T) *Manager {
return &Manager{
availableIntegrationsRepo: &TestAvailableIntegrationsRepo{},
installedIntegrationsRepo: installedIntegrationsRepo,
}
}, testDB
}
func createTestUser() (*types.User, *model.ApiError) {
func createTestUser(organizationModule organization.Module) (*types.User, *model.ApiError) {
// Create a test user for auth
ctx := context.Background()
org, apiErr := dao.DB().CreateOrg(ctx, &types.Organization{
Name: "test",
})
if apiErr != nil {
return nil, apiErr
organization := types.NewOrganization("test")
err := organizationModule.Create(ctx, organization)
if err != nil {
return nil, model.InternalError(err)
}
group, apiErr := dao.DB().GetGroupByName(ctx, constants.AdminGroup)
if apiErr != nil {
return nil, apiErr
return nil, model.InternalError(apiErr)
}
auth.InitAuthCache(ctx)
@ -56,7 +57,7 @@ func createTestUser() (*types.User, *model.ApiError) {
Name: "test",
Email: userId[:8] + "test@test.com",
Password: "test",
OrgID: org.ID,
OrgID: organization.ID.StringValue(),
GroupID: group.ID,
},
true,

View File

@ -142,7 +142,7 @@ func (r *Repo) GetDefaultOrgID(ctx context.Context) (string, *model.ApiError) {
if len(orgs) == 0 {
return "", model.InternalError(errors.New("no orgs found"))
}
return orgs[0].ID, nil
return orgs[0].ID.StringValue(), nil
}
// GetPipelines returns pipeline and errors (if any)

View File

@ -536,14 +536,6 @@ func parseSetApdexScoreRequest(r *http.Request) (*types.ApdexSettings, error) {
return &req, nil
}
func parseInsertIngestionKeyRequest(r *http.Request) (*model.IngestionKey, error) {
var req model.IngestionKey
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, err
}
return &req, nil
}
func parseRegisterRequest(r *http.Request) (*auth.RegisterRequest, error) {
var req auth.RegisterRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {

View File

@ -15,6 +15,7 @@ import (
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/apis/fields"
"github.com/SigNoz/signoz/pkg/http/middleware"
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
"github.com/SigNoz/signoz/pkg/modules/preference"
preferencecore "github.com/SigNoz/signoz/pkg/modules/preference/core"
"github.com/SigNoz/signoz/pkg/prometheus"
@ -185,7 +186,9 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
}
telemetry.GetInstance().SetReader(reader)
preferenceModule := preference.NewAPI(preferencecore.NewPreference(preferencecore.NewStore(serverOptions.SigNoz.SQLStore), preferencetypes.NewDefaultPreferenceMap()))
preferenceAPI := preference.NewAPI(preferencecore.NewPreference(preferencecore.NewStore(serverOptions.SigNoz.SQLStore), preferencetypes.NewDefaultPreferenceMap()))
organizationAPI := implorganization.NewAPI(implorganization.NewModule(implorganization.NewStore(serverOptions.SigNoz.SQLStore)))
organizationModule := implorganization.NewModule(implorganization.NewStore(serverOptions.SigNoz.SQLStore))
apiHandler, err := NewAPIHandler(APIHandlerOpts{
Reader: reader,
SkipConfig: skipConfig,
@ -204,7 +207,9 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
AlertmanagerAPI: alertmanager.NewAPI(serverOptions.SigNoz.Alertmanager),
FieldsAPI: fields.NewAPI(serverOptions.SigNoz.TelemetryStore),
Signoz: serverOptions.SigNoz,
Preference: preferenceModule,
Preference: preferenceAPI,
OrganizationAPI: organizationAPI,
OrganizationModule: organizationModule,
})
if err != nil {
return nil, err

View File

@ -12,6 +12,7 @@ import (
"github.com/pkg/errors"
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/query-service/constants"
"github.com/SigNoz/signoz/pkg/query-service/dao"
"github.com/SigNoz/signoz/pkg/query-service/model"
@ -277,7 +278,7 @@ func RevokeInvite(ctx context.Context, email string) error {
}
// GetInvite returns an invitation object for the given token.
func GetInvite(ctx context.Context, token string) (*model.InvitationResponseObject, error) {
func GetInvite(ctx context.Context, token string, organizationModule organization.Module) (*model.InvitationResponseObject, error) {
zap.L().Debug("GetInvite method invoked for token", zap.String("token", token))
inv, apiErr := dao.DB().GetInviteFromToken(ctx, token)
@ -289,11 +290,13 @@ func GetInvite(ctx context.Context, token string) (*model.InvitationResponseObje
return nil, errors.New("user is not invited")
}
// 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)
if apiErr != nil {
return nil, errors.Wrap(apiErr.Err, "failed to query the DB")
orgID, err := valuer.NewUUID(inv.OrgID)
if err != nil {
return nil, err
}
org, err := organizationModule.Get(ctx, orgID)
if err != nil {
return nil, errors.Wrap(err, "failed to query the DB")
}
return &model.InvitationResponseObject{
Name: inv.Name,
@ -301,7 +304,7 @@ func GetInvite(ctx context.Context, token string) (*model.InvitationResponseObje
Token: inv.Token,
CreatedAt: inv.CreatedAt.Unix(),
Role: inv.Role,
Organization: org.Name,
Organization: org.DisplayName,
}, nil
}
@ -390,21 +393,19 @@ 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"`
InviteToken string `json:"token"`
IsAnonymous bool `json:"isAnonymous"`
HasOptedUpdates bool `json:"hasOptedUpdates"`
Name string `json:"name"`
OrgID string `json:"orgId"`
OrgDisplayName string `json:"orgDisplayName"`
OrgName string `json:"orgName"`
Email string `json:"email"`
Password string `json:"password"`
InviteToken string `json:"token"`
// reference URL to track where the register request is coming from
SourceUrl string `json:"sourceUrl"`
}
func RegisterFirstUser(ctx context.Context, req *RegisterRequest) (*types.User, *model.ApiError) {
func RegisterFirstUser(ctx context.Context, req *RegisterRequest, organizationModule organization.Module) (*types.User, *model.ApiError) {
if req.Email == "" {
return nil, model.BadRequest(model.ErrEmailRequired{})
}
@ -414,13 +415,10 @@ func RegisterFirstUser(ctx context.Context, req *RegisterRequest) (*types.User,
}
groupName := constants.AdminGroup
// modify this to use bun
org, apierr := dao.DB().CreateOrg(ctx,
&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
organization := types.NewOrganization(req.OrgDisplayName)
err := organizationModule.Create(ctx, organization)
if err != nil {
return nil, model.InternalError(err)
}
group, apiErr := dao.DB().GetGroupByName(ctx, groupName)
@ -430,8 +428,6 @@ func RegisterFirstUser(ctx context.Context, req *RegisterRequest) (*types.User,
}
var hash string
var err error
hash, err = PasswordHash(req.Password)
if err != nil {
zap.L().Error("failed to generate password hash when registering a user", zap.Error(err))
@ -448,7 +444,7 @@ func RegisterFirstUser(ctx context.Context, req *RegisterRequest) (*types.User,
},
ProfilePictureURL: "", // Currently unused
GroupID: group.ID,
OrgID: org.ID,
OrgID: organization.ID.StringValue(),
}
return dao.DB().CreateUser(ctx, user, true)
@ -553,7 +549,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, alertmanager alertmanager.Alertmanager) (*types.User, *model.ApiError) {
func Register(ctx context.Context, req *RegisterRequest, alertmanager alertmanager.Alertmanager, organizationModule organization.Module) (*types.User, *model.ApiError) {
users, err := dao.DB().GetUsers(ctx)
if err != nil {
return nil, model.InternalError(fmt.Errorf("failed to get user count"))
@ -561,7 +557,7 @@ func Register(ctx context.Context, req *RegisterRequest, alertmanager alertmanag
switch len(users) {
case 0:
user, err := RegisterFirstUser(ctx, req)
user, err := RegisterFirstUser(ctx, req, organizationModule)
if err != nil {
return nil, err
}

View File

@ -26,10 +26,6 @@ type Queries interface {
GetGroupByName(ctx context.Context, name string) (*types.Group, *model.ApiError)
GetGroups(ctx context.Context) ([]types.Group, *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) (*types.ResetPasswordRequest, *model.ApiError)
GetUsersByOrg(ctx context.Context, orgId string) ([]types.GettableUser, *model.ApiError)
GetUsersByGroup(ctx context.Context, groupId string) ([]types.GettableUser, *model.ApiError)
@ -50,10 +46,6 @@ type Mutations interface {
CreateGroup(ctx context.Context, group *types.Group) (*types.Group, *model.ApiError)
DeleteGroup(ctx context.Context, id string) *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 *types.ResetPasswordRequest) *model.ApiError
DeleteResetPasswordEntry(ctx context.Context, token string) *model.ApiError

View File

@ -65,7 +65,7 @@ func (mds *ModelDaoSqlite) initializeOrgPreferences(ctx context.Context) error {
}
// set telemetry fields from userPreferences
telemetry.GetInstance().SetDistinctId(org.ID)
telemetry.GetInstance().SetDistinctId(org.ID.StringValue())
users, _ := mds.GetUsers(ctx)
countUsers := len(users)

View File

@ -3,7 +3,6 @@ package sqlite
import (
"context"
"fmt"
"time"
"github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/SigNoz/signoz/pkg/query-service/telemetry"
@ -95,71 +94,6 @@ func (mds *ModelDaoSqlite) GetInvites(ctx context.Context, orgID string) ([]type
return invites, nil
}
func (mds *ModelDaoSqlite) CreateOrg(ctx context.Context,
org *types.Organization) (*types.Organization, *model.ApiError) {
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}
}
return org, nil
}
func (mds *ModelDaoSqlite) GetOrg(ctx context.Context,
id string) (*types.Organization, *model.ApiError) {
orgs := []types.Organization{}
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,
Err: errors.New("Found multiple org with same ID"),
}
}
if len(orgs) == 0 {
return nil, nil
}
return &orgs[0], nil
}
func (mds *ModelDaoSqlite) GetOrgByName(ctx context.Context,
name string) (*types.Organization, *model.ApiError) {
orgs := []types.Organization{}
if err := mds.bundb.NewSelect().
Model(&orgs).
Where("name = ?", name).
Scan(ctx); err != nil {
return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err}
}
if len(orgs) > 1 {
return nil, &model.ApiError{
Typ: model.ErrorInternal,
Err: errors.New("Multiple orgs with same ID found"),
}
}
if len(orgs) == 0 {
return nil, nil
}
return &orgs[0], nil
}
func (mds *ModelDaoSqlite) GetOrgs(ctx context.Context) ([]types.Organization, *model.ApiError) {
var orgs []types.Organization
err := mds.bundb.NewSelect().
@ -172,37 +106,6 @@ func (mds *ModelDaoSqlite) GetOrgs(ctx context.Context) ([]types.Organization, *
return orgs, nil
}
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)
if err != nil {
return &model.ApiError{Typ: model.ErrorInternal, Err: err}
}
telemetry.GetInstance().SetTelemetryAnonymous(org.IsAnonymous)
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)
if err != nil {
return &model.ApiError{Typ: model.ErrorInternal, Err: err}
}
return nil
}
func (mds *ModelDaoSqlite) CreateUser(ctx context.Context,
user *types.User, isFirstUser bool) (*types.User, *model.ApiError) {
_, err := mds.bundb.NewInsert().
@ -306,7 +209,7 @@ func (mds *ModelDaoSqlite) GetUser(ctx context.Context,
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").
ColumnExpr("o.display_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)
@ -343,7 +246,7 @@ func (mds *ModelDaoSqlite) GetUserByEmail(ctx context.Context,
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").
ColumnExpr("o.display_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)
@ -378,7 +281,7 @@ func (mds *ModelDaoSqlite) GetUsersWithOpts(ctx context.Context, limit int) ([]t
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").
ColumnExpr("o.display_name as organization").
Join("JOIN groups g ON g.id = users.group_id").
Join("JOIN organizations o ON o.id = users.org_id")
@ -402,7 +305,7 @@ func (mds *ModelDaoSqlite) GetUsersByOrg(ctx context.Context,
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").
ColumnExpr("o.display_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)
@ -423,7 +326,7 @@ func (mds *ModelDaoSqlite) GetUsersByGroup(ctx context.Context,
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").
ColumnExpr("o.display_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)

View File

@ -10,6 +10,7 @@ import (
"testing"
"github.com/SigNoz/signoz/pkg/http/middleware"
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
"github.com/SigNoz/signoz/pkg/query-service/app"
"github.com/SigNoz/signoz/pkg/query-service/auth"
"github.com/SigNoz/signoz/pkg/query-service/constants"
@ -313,7 +314,8 @@ func NewFilterSuggestionsTestBed(t *testing.T) *FilterSuggestionsTestBed {
apiHandler.RegisterRoutes(router, am)
apiHandler.RegisterQueryRangeV3Routes(router, am)
user, apiErr := createTestUser()
organizationModule := implorganization.NewModule(implorganization.NewStore(testDB))
user, apiErr := createTestUser(organizationModule)
if apiErr != nil {
t.Fatalf("could not create a test user: %v", apiErr)
}

View File

@ -9,6 +9,7 @@ import (
"strings"
"testing"
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
"github.com/SigNoz/signoz/pkg/query-service/agentConf"
"github.com/SigNoz/signoz/pkg/query-service/app"
"github.com/SigNoz/signoz/pkg/query-service/app/integrations"
@ -479,7 +480,8 @@ func NewTestbedWithoutOpamp(t *testing.T, sqlStore sqlstore.SQLStore) *LogPipeli
t.Fatalf("could not create a new ApiHandler: %v", err)
}
user, apiErr := createTestUser()
organizationModule := implorganization.NewModule(implorganization.NewStore(sqlStore))
user, apiErr := createTestUser(organizationModule)
if apiErr != nil {
t.Fatalf("could not create a test user: %v", apiErr)
}

View File

@ -9,6 +9,8 @@ import (
"time"
"github.com/SigNoz/signoz/pkg/http/middleware"
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
"github.com/SigNoz/signoz/pkg/query-service/app"
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations"
"github.com/SigNoz/signoz/pkg/query-service/auth"
@ -375,7 +377,8 @@ func NewCloudIntegrationsTestBed(t *testing.T, testDB sqlstore.SQLStore) *CloudI
apiHandler.RegisterRoutes(router, am)
apiHandler.RegisterCloudIntegrationsRoutes(router, am)
user, apiErr := createTestUser()
organizationModule := implorganization.NewModule(implorganization.NewStore(testDB))
user, apiErr := createTestUser(organizationModule)
if apiErr != nil {
t.Fatalf("could not create a test user: %v", apiErr)
}

View File

@ -9,6 +9,7 @@ import (
"time"
"github.com/SigNoz/signoz/pkg/http/middleware"
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
"github.com/SigNoz/signoz/pkg/query-service/app"
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations"
"github.com/SigNoz/signoz/pkg/query-service/app/integrations"
@ -583,7 +584,8 @@ func NewIntegrationsTestBed(t *testing.T, testDB sqlstore.SQLStore) *Integration
apiHandler.RegisterRoutes(router, am)
apiHandler.RegisterIntegrationRoutes(router, am)
user, apiErr := createTestUser()
organizationModule := implorganization.NewModule(implorganization.NewStore(testDB))
user, apiErr := createTestUser(organizationModule)
if apiErr != nil {
t.Fatalf("could not create a test user: %v", apiErr)
}

View File

@ -14,6 +14,7 @@ import (
"github.com/DATA-DOG/go-sqlmock"
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/prometheus"
"github.com/SigNoz/signoz/pkg/prometheus/prometheustest"
"github.com/SigNoz/signoz/pkg/query-service/app"
@ -148,19 +149,18 @@ func makeTestSignozLog(
return testLog
}
func createTestUser() (*types.User, *model.ApiError) {
func createTestUser(organizationModule organization.Module) (*types.User, *model.ApiError) {
// Create a test user for auth
ctx := context.Background()
org, apiErr := dao.DB().CreateOrg(ctx, &types.Organization{
Name: "test",
})
if apiErr != nil {
return nil, apiErr
organization := types.NewOrganization("test")
err := organizationModule.Create(ctx, organization)
if err != nil {
return nil, model.InternalError(err)
}
group, apiErr := dao.DB().GetGroupByName(ctx, constants.AdminGroup)
if apiErr != nil {
return nil, apiErr
return nil, model.InternalError(apiErr)
}
auth.InitAuthCache(ctx)
@ -174,7 +174,7 @@ func createTestUser() (*types.User, *model.ApiError) {
Name: "test",
Email: userId[:8] + "test@test.com",
Password: "test",
OrgID: org.ID,
OrgID: organization.ID.StringValue(),
GroupID: group.ID,
},
true,

View File

@ -57,6 +57,7 @@ func NewTestSqliteDB(t *testing.T) (sqlStore sqlstore.SQLStore, testDBFilePath s
sqlmigration.NewUpdatePatFactory(sqlStore),
sqlmigration.NewAddVirtualFieldsFactory(),
sqlmigration.NewUpdateIntegrationsFactory(sqlStore),
sqlmigration.NewUpdateOrganizationsFactory(sqlStore),
),
)
if err != nil {

View File

@ -72,6 +72,7 @@ func NewSQLMigrationProviderFactories(sqlstore sqlstore.SQLStore) factory.NamedM
sqlmigration.NewUpdateRulesFactory(sqlstore),
sqlmigration.NewAddVirtualFieldsFactory(),
sqlmigration.NewUpdateIntegrationsFactory(sqlstore),
sqlmigration.NewUpdateOrganizationsFactory(sqlstore),
)
}

View File

@ -0,0 +1,119 @@
package sqlmigration
import (
"context"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/uptrace/bun"
"github.com/uptrace/bun/migrate"
)
type updateOrganizations struct {
store sqlstore.SQLStore
}
func NewUpdateOrganizationsFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(
factory.MustNewName("update_organizations"),
func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
return newUpdateOrganizations(ctx, ps, c, sqlstore)
})
}
func newUpdateOrganizations(_ context.Context, _ factory.ProviderSettings, _ Config, store sqlstore.SQLStore) (SQLMigration, error) {
return &updateOrganizations{store: store}, nil
}
func (migration *updateOrganizations) Register(migrations *migrate.Migrations) error {
if err := migrations.
Register(migration.Up, migration.Down); err != nil {
return err
}
return nil
}
func (migration *updateOrganizations) Up(ctx context.Context, db *bun.DB) error {
tx, err := db.
BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
err = migration.
store.
Dialect().
DropColumn(ctx, tx, "organizations", "is_anonymous")
if err != nil {
return err
}
err = migration.
store.
Dialect().
DropColumn(ctx, tx, "organizations", "has_opted_updates")
if err != nil {
return err
}
_, err = migration.
store.
Dialect().
RenameColumn(ctx, tx, "organizations", "name", "display_name")
if err != nil {
return err
}
err = migration.
store.
Dialect().
AddColumn(ctx, tx, "organizations", "name", "TEXT")
if err != nil {
return err
}
_, err = tx.
NewCreateIndex().
Unique().
IfNotExists().
Index("idx_unique_name").
Table("organizations").
Column("name").
Exec(ctx)
if err != nil {
return err
}
err = migration.
store.
Dialect().
AddColumn(ctx, tx, "organizations", "alias", "TEXT")
if err != nil {
return err
}
_, err = tx.
NewCreateIndex().
Unique().
IfNotExists().
Index("idx_unique_alias").
Table("organizations").
Column("alias").
Exec(ctx)
if err != nil {
return err
}
err = tx.Commit()
if err != nil {
return err
}
return nil
}
func (migration *updateOrganizations) Down(context.Context, *bun.DB) error {
return nil
}

View File

@ -74,6 +74,14 @@ func (dialect *dialect) MigrateIntToTimestamp(ctx context.Context, bun bun.IDB,
}
func (dialect *dialect) MigrateIntToBoolean(ctx context.Context, bun bun.IDB, table string, column string) error {
columnExists, err := dialect.ColumnExists(ctx, bun, table, column)
if err != nil {
return err
}
if !columnExists {
return nil
}
columnType, err := dialect.GetColumnType(ctx, bun, table, column)
if err != nil {
return err
@ -141,6 +149,26 @@ func (dialect *dialect) ColumnExists(ctx context.Context, bun bun.IDB, table str
return count > 0, nil
}
func (dialect *dialect) AddColumn(ctx context.Context, bun bun.IDB, table string, column string, columnExpr string) error {
exists, err := dialect.ColumnExists(ctx, bun, table, column)
if err != nil {
return err
}
if !exists {
_, err = bun.
NewAddColumn().
Table(table).
ColumnExpr(column + " " + columnExpr).
Exec(ctx)
if err != nil {
return err
}
}
return nil
}
func (dialect *dialect) RenameColumn(ctx context.Context, bun bun.IDB, table string, oldColumnName string, newColumnName string) (bool, error) {
oldColumnExists, err := dialect.ColumnExists(ctx, bun, table, oldColumnName)
if err != nil {
@ -152,10 +180,14 @@ func (dialect *dialect) RenameColumn(ctx context.Context, bun bun.IDB, table str
return false, err
}
if !oldColumnExists && newColumnExists {
if newColumnExists {
return true, nil
}
if !oldColumnExists {
return false, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "old column: %s doesn't exist", oldColumnName)
}
_, err = bun.
ExecContext(ctx, "ALTER TABLE "+table+" RENAME COLUMN "+oldColumnName+" TO "+newColumnName)
if err != nil {
@ -164,6 +196,26 @@ func (dialect *dialect) RenameColumn(ctx context.Context, bun bun.IDB, table str
return true, nil
}
func (dialect *dialect) DropColumn(ctx context.Context, bun bun.IDB, table string, column string) error {
exists, err := dialect.ColumnExists(ctx, bun, table, column)
if err != nil {
return err
}
if exists {
_, err = bun.
NewDropColumn().
Table(table).
Column(column).
Exec(ctx)
if err != nil {
return err
}
}
return nil
}
func (dialect *dialect) TableExists(ctx context.Context, bun bun.IDB, table interface{}) (bool, error) {
count := 0

View File

@ -42,7 +42,9 @@ type SQLDialect interface {
AddNotNullDefaultToColumn(context.Context, bun.IDB, string, string, string, string) error
GetColumnType(context.Context, bun.IDB, string, string) (string, error)
ColumnExists(context.Context, bun.IDB, string, string) (bool, error)
AddColumn(context.Context, bun.IDB, string, string, string) error
RenameColumn(context.Context, bun.IDB, string, string, string) (bool, error)
DropColumn(context.Context, bun.IDB, string, string) error
RenameTableAndModifyModel(context.Context, bun.IDB, interface{}, interface{}, []string, func(context.Context) error) error
UpdatePrimaryKey(context.Context, bun.IDB, interface{}, interface{}, string, func(context.Context) error) error
AddPrimaryKey(context.Context, bun.IDB, interface{}, interface{}, string, func(context.Context) error) error

View File

@ -25,10 +25,18 @@ func (dialect *dialect) ColumnExists(ctx context.Context, bun bun.IDB, table str
return false, nil
}
func (dialect *dialect) AddColumn(ctx context.Context, bun bun.IDB, table string, column string, columnExpr string) error {
return nil
}
func (dialect *dialect) RenameColumn(ctx context.Context, bun bun.IDB, table string, oldColumnName string, newColumnName string) (bool, error) {
return true, nil
}
func (dialect *dialect) DropColumn(ctx context.Context, bun bun.IDB, table string, column string) error {
return nil
}
func (dialect *dialect) RenameTableAndModifyModel(ctx context.Context, bun bun.IDB, oldModel interface{}, newModel interface{}, references []string, cb func(context.Context) error) error {
return nil
}

View File

@ -1,17 +1,34 @@
package types
import (
"context"
"time"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/uptrace/bun"
)
// TODO: check constraints are not working
type Organization struct {
bun.BaseModel `bun:"table:organizations"`
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"`
Identifiable
Name string `bun:"name,type:text,nullzero" json:"name"`
Alias string `bun:"alias,type:text,nullzero" json:"alias"`
DisplayName string `bun:"display_name,type:text,notnull" json:"displayName"`
}
func NewOrganization(displayName string) *Organization {
return &Organization{
Identifiable: Identifiable{
ID: valuer.GenerateUUID(),
},
TimeAuditable: TimeAuditable{
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
// Name: "default/main", TODO: take the call and uncomment this later
DisplayName: displayName,
}
}
type ApdexSettings struct {
@ -22,3 +39,11 @@ type ApdexSettings struct {
Threshold float64 `bun:"threshold,type:float,notnull" json:"threshold"`
ExcludeStatusCodes string `bun:"exclude_status_codes,type:text,notnull" json:"excludeStatusCodes"`
}
type OrganizationStore interface {
Create(context.Context, *Organization) error
Get(context.Context, valuer.UUID) (*Organization, error)
GetAll(context.Context) ([]*Organization, error)
Update(context.Context, *Organization) error
Delete(context.Context, valuer.UUID) error
}