Compare commits

..

2 Commits

Author SHA1 Message Date
Vinicius Lourenço
df30852296 perf(bundle-size): move antd-table-saveas-excel out of main bundle (#10229)
Some checks are pending
build-staging / prepare (push) Waiting to run
build-staging / js-build (push) Blocked by required conditions
build-staging / go-build (push) Blocked by required conditions
build-staging / staging (push) Blocked by required conditions
Release Drafter / update_release_draft (push) Waiting to run
2026-02-27 17:27:30 +00:00
Srikanth Chekuri
b9beabb425 chore: add guide for packages (#10443) 2026-02-27 13:50:44 +00:00
20 changed files with 550 additions and 665 deletions

View File

@@ -0,0 +1,126 @@
# Packages
All shared Go code in SigNoz lives under `pkg/`. Each package represents a distinct domain concept and exposes a clear public interface. This guide covers the conventions for creating, naming, and organising packages so the codebase stays consistent as it grows.
## How should I name a package?
Use short, lowercase, single-word names. No underscores or camelCase (`querier`, `cache`, `authz`, not `query_builder` or `dataStore`).
Names must be **domain-specific**. A package name should tell you what problem domain it deals with, not what data structure it wraps. Prefer `alertmanager` over `manager`, `licensing` over `checker`.
Avoid generic names like `util`, `helpers`, `common`, `misc`, or `base`. If you can't name it, the code probably belongs in an existing package.
## When should I create a new package?
Create a new package when:
- The functionality represents a **distinct domain concept** (e.g., `authz`, `licensing`, `cache`).
- Two or more other packages would import it; it serves as shared infrastructure.
- The code has a clear public interface that can stand on its own.
Do **not** create a new package when:
- There is already a package that covers the same domain. Extend the existing package instead.
- The code is only used in one place. Keep it local to the caller.
- You are splitting purely for file size. Use multiple files within the same package instead.
## How should I lay out a package?
A typical package looks like:
```
pkg/cache/
├── cache.go # Public interface + exported types
├── config.go # Configuration types if needed
├── memorycache/ # Implementation sub-package
├── rediscache/ # Another implementation
└── cachetest/ # Test helpers for consumers
```
Follow these rules:
1. **Interface-first file**: The file matching the package name (e.g., `cache.go` in `pkg/cache/`) should define the public interface and core exported types. Keep implementation details out of this file.
2. **One responsibility per file**: Name files after what they contain (`config.go`, `handler.go`, `service.go`), not after the package name. If a package merges two concerns, prefix files to group them (e.g., `memory_store.go`, `redis_store.go` in a storage package).
3. **Sub-packages for implementations**: When a package defines an interface with multiple implementations, put each implementation in its own sub-package (`memorycache/`, `rediscache/`). This keeps the parent package import-free of implementation dependencies.
4. **Test helpers in `{pkg}test/`**: If consumers need test mocks or builders, put them in a `{pkg}test/` sub-package (e.g., `cachetest/`, `sqlstoretest/`). This avoids polluting the main package with test-only code.
5. **Test files stay alongside source**: Unit tests go in `_test.go` files next to the code they test, in the same package.
## How should I name symbols?
### Exported symbols
- **Interfaces**: For single-method interfaces, follow the standard `-er` suffix convention (`Reader`, `Writer`, `Closer`). For multi-method interfaces, use clear nouns (`Cache`, `Store`, `Provider`).
- **Constructors**: `New<Type>(...)` (e.g., `NewMemoryCache()`).
- **Avoid stutter**: Since callers qualify with the package name, don't repeat it. Write `cache.Cache`, not `cache.CacheInterface`. Write `authz.FromRole`, not `authz.AuthzFromRole`.
### Unexported symbols
- Struct receivers: one or two characters (`c`, `f`, `br`).
- Helper functions: descriptive lowercase names (`parseToken`, `buildQuery`).
### Constants
- Use `PascalCase` for exported constants.
- When merging files from different origins into one package, watch out for **name collisions** across files. Prefix to disambiguate when two types share a natural name.
## How should I organise imports?
Group imports in three blocks separated by blank lines:
```go
import (
// 1. Standard library
"fmt"
"net/http"
// 2. External dependencies
"github.com/gorilla/mux"
// 3. Internal
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types"
)
```
Never introduce circular imports. If package A needs package B and B needs A, extract the shared types into a third package (often under `pkg/types/`).
## Where do shared types go?
Most types belong in `pkg/types/` under a domain-specific sub-package (e.g., `pkg/types/ruletypes`, `pkg/types/authtypes`).
Do not put domain logic in `pkg/types/`. Only data structures, constants, and simple methods.
## How do I merge or move packages?
When two packages are tightly coupled (one imports the other's constants, they cover the same domain), merge them:
1. Pick a domain-specific name for the combined package.
2. Prefix files to preserve origin (e.g., `memory_store.go`, `redis_store.go`).
3. Resolve symbol conflicts explicitly; rename with a prefix rather than silently shadowing.
4. Update all consumers in a single change.
5. Delete the old packages. Do not leave behind re-export shims.
6. Verify with `go build ./...`, `go test ./<new-pkg>/...`, and `go vet ./...`.
## When should I add documentation?
Add a `doc.go` with a package-level comment for any package that is non-trivial or has multiple consumers. Keep it to 13 sentences:
```go
// Package cache provides a caching interface with pluggable backends
// for in-memory and Redis-based storage.
package cache
```
## What should I remember?
- Package names are domain-specific and lowercase. Never generic names like `util` or `common`.
- The file matching the package name (e.g., `cache.go`) defines the public interface. Implementation details go elsewhere.
- Never introduce circular imports. Extract shared types into `pkg/types/` when needed.
- Watch for symbol name collisions when merging packages, prefix to disambiguate.
- Put test helpers in a `{pkg}test/` sub-package, not in the main package.
- Before submitting, verify with `go build ./...`, `go test ./<your-pkg>/...`, and `go vet ./...`.
- Update all consumers when you rename or move symbols.

View File

@@ -8,4 +8,13 @@ We adhere to three primary style guides as our foundation:
- [Code Review Comments](https://go.dev/wiki/CodeReviewComments) - For understanding common comments in code reviews
- [Google Style Guide](https://google.github.io/styleguide/go/) - Additional practices from Google
We **recommend** (almost enforce) reviewing these guides before contributing to the codebase. They provide valuable insights into writing idiomatic Go code and will help you understand our approach to backend development. In addition, we have a few additional rules that make certain areas stricter than the above which can be found in area-specific files in this package.
We **recommend** (almost enforce) reviewing these guides before contributing to the codebase. They provide valuable insights into writing idiomatic Go code and will help you understand our approach to backend development. In addition, we have a few additional rules that make certain areas stricter than the above which can be found in area-specific files in this package:
- [Packages](packages.md) — Naming, layout, and conventions for `pkg/` packages
- [Errors](errors.md) — Structured error handling
- [Handler](handler.md) — Writing HTTP handlers and OpenAPI integration
- [Endpoint](endpoint.md) — Endpoint conventions
- [SQL](sql.md) — Database query patterns
- [Provider](provider.md) — Provider pattern
- [Integration](integration.md) — Integration conventions
- [Flagger](flagger.md) — Feature flag conventions

View File

@@ -170,7 +170,7 @@ func (ah *APIHandler) getOrCreateCloudIntegrationUser(
cloudIntegrationUserName := fmt.Sprintf("%s-integration", cloudProvider)
email := valuer.MustNewEmail(fmt.Sprintf("%s@signoz.io", cloudIntegrationUserName))
cloudIntegrationUser, err := types.NewUser(cloudIntegrationUserName, email, types.RoleViewer, valuer.MustNewUUID(orgId), types.UserStatusActive)
cloudIntegrationUser, err := types.NewUser(cloudIntegrationUserName, email, types.RoleViewer, valuer.MustNewUUID(orgId))
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf("couldn't create cloud integration user: %w", err))
}

View File

@@ -1,6 +1,6 @@
import { useState } from 'react';
import { CloudDownloadOutlined } from '@ant-design/icons';
import { Button, Dropdown, MenuProps } from 'antd';
import { Excel } from 'antd-table-saveas-excel';
import { unparse } from 'papaparse';
import { DownloadProps } from './Download.types';
@@ -8,25 +8,36 @@ import { DownloadProps } from './Download.types';
import './Download.styles.scss';
function Download({ data, isLoading, fileName }: DownloadProps): JSX.Element {
const downloadExcelFile = (): void => {
const headers = Object.keys(Object.assign({}, ...data)).map((item) => {
const updatedTitle = item
.split('_')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
return {
title: updatedTitle,
dataIndex: item,
};
});
const excel = new Excel();
excel
.addSheet(fileName)
.addColumns(headers)
.addDataSource(data, {
str2Percent: true,
})
.saveAs(`${fileName}.xlsx`);
const [isDownloading, setIsDownloading] = useState(false);
const downloadExcelFile = async (): Promise<void> => {
setIsDownloading(true);
try {
const headers = Object.keys(Object.assign({}, ...data)).map((item) => {
const updatedTitle = item
.split('_')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
return {
title: updatedTitle,
dataIndex: item,
};
});
const excelLib = await import('antd-table-saveas-excel');
const excel = new excelLib.Excel();
excel
.addSheet(fileName)
.addColumns(headers)
.addDataSource(data, {
str2Percent: true,
})
.saveAs(`${fileName}.xlsx`);
} finally {
setIsDownloading(false);
}
};
const downloadCsvFile = (): void => {
@@ -59,7 +70,7 @@ function Download({ data, isLoading, fileName }: DownloadProps): JSX.Element {
<Dropdown menu={menu} trigger={['click']}>
<Button
className="download-button"
loading={isLoading}
loading={isLoading || isDownloading}
size="small"
type="link"
>

View File

@@ -1,5 +1,5 @@
import { useState } from 'react';
import { Button, Popover, Typography } from 'antd';
import { Excel } from 'antd-table-saveas-excel';
import { FileDigit, FileDown, Sheet } from 'lucide-react';
import { unparse } from 'papaparse';
@@ -8,25 +8,34 @@ import { DownloadProps } from './DownloadV2.types';
import './DownloadV2.styles.scss';
function Download({ data, isLoading, fileName }: DownloadProps): JSX.Element {
const downloadExcelFile = (): void => {
const headers = Object.keys(Object.assign({}, ...data)).map((item) => {
const updatedTitle = item
.split('_')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
return {
title: updatedTitle,
dataIndex: item,
};
});
const excel = new Excel();
excel
.addSheet(fileName)
.addColumns(headers)
.addDataSource(data, {
str2Percent: true,
})
.saveAs(`${fileName}.xlsx`);
const [isDownloading, setIsDownloading] = useState(false);
const downloadExcelFile = async (): Promise<void> => {
setIsDownloading(true);
try {
const headers = Object.keys(Object.assign({}, ...data)).map((item) => {
const updatedTitle = item
.split('_')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
return {
title: updatedTitle,
dataIndex: item,
};
});
const excelLib = await import('antd-table-saveas-excel');
const excel = new excelLib.Excel();
excel
.addSheet(fileName)
.addColumns(headers)
.addDataSource(data, {
str2Percent: true,
})
.saveAs(`${fileName}.xlsx`);
} finally {
setIsDownloading(false);
}
};
const downloadCsvFile = (): void => {
@@ -54,6 +63,7 @@ function Download({ data, isLoading, fileName }: DownloadProps): JSX.Element {
type="text"
onClick={downloadExcelFile}
className="action-btns"
loading={isDownloading}
>
Excel (.xlsx)
</Button>

View File

@@ -17,7 +17,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
Description: "This endpoint creates an invite for a user",
Request: new(types.PostableInvite),
RequestContentType: "application/json",
Response: new(types.User),
Response: new(types.Invite),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
@@ -43,73 +43,73 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
return err
}
// if err := router.Handle("/api/v1/invite/{token}", handler.New(provider.authZ.OpenAccess(provider.userHandler.GetInvite), handler.OpenAPIDef{
// ID: "GetInvite",
// Tags: []string{"users"},
// Summary: "Get invite",
// Description: "This endpoint gets an invite by token",
// Request: nil,
// RequestContentType: "",
// Response: new(types.Invite),
// ResponseContentType: "application/json",
// SuccessStatusCode: http.StatusOK,
// ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
// Deprecated: false,
// SecuritySchemes: []handler.OpenAPISecurityScheme{},
// })).Methods(http.MethodGet).GetError(); err != nil {
// return err
// }
if err := router.Handle("/api/v1/invite/{token}", handler.New(provider.authZ.OpenAccess(provider.userHandler.GetInvite), handler.OpenAPIDef{
ID: "GetInvite",
Tags: []string{"users"},
Summary: "Get invite",
Description: "This endpoint gets an invite by token",
Request: nil,
RequestContentType: "",
Response: new(types.Invite),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: []handler.OpenAPISecurityScheme{},
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
// if err := router.Handle("/api/v1/invite/{id}", handler.New(provider.authZ.AdminAccess(provider.userHandler.DeleteInvite), handler.OpenAPIDef{
// ID: "DeleteInvite",
// Tags: []string{"users"},
// Summary: "Delete invite",
// Description: "This endpoint deletes an invite by id",
// Request: nil,
// RequestContentType: "",
// Response: nil,
// ResponseContentType: "",
// SuccessStatusCode: http.StatusNoContent,
// ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
// Deprecated: false,
// SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
// })).Methods(http.MethodDelete).GetError(); err != nil {
// return err
// }
if err := router.Handle("/api/v1/invite/{id}", handler.New(provider.authZ.AdminAccess(provider.userHandler.DeleteInvite), handler.OpenAPIDef{
ID: "DeleteInvite",
Tags: []string{"users"},
Summary: "Delete invite",
Description: "This endpoint deletes an invite by id",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
// if err := router.Handle("/api/v1/invite", handler.New(provider.authZ.AdminAccess(provider.userHandler.ListInvite), handler.OpenAPIDef{
// ID: "ListInvite",
// Tags: []string{"users"},
// Summary: "List invites",
// Description: "This endpoint lists all invites",
// Request: nil,
// RequestContentType: "",
// Response: make([]*types.Invite, 0),
// ResponseContentType: "application/json",
// SuccessStatusCode: http.StatusOK,
// ErrorStatusCodes: []int{},
// Deprecated: false,
// SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
// })).Methods(http.MethodGet).GetError(); err != nil {
// return err
// }
if err := router.Handle("/api/v1/invite", handler.New(provider.authZ.AdminAccess(provider.userHandler.ListInvite), handler.OpenAPIDef{
ID: "ListInvite",
Tags: []string{"users"},
Summary: "List invites",
Description: "This endpoint lists all invites",
Request: nil,
RequestContentType: "",
Response: make([]*types.Invite, 0),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
// if err := router.Handle("/api/v1/invite/accept", handler.New(provider.authZ.OpenAccess(provider.userHandler.AcceptInvite), handler.OpenAPIDef{
// ID: "AcceptInvite",
// Tags: []string{"users"},
// Summary: "Accept invite",
// Description: "This endpoint accepts an invite by token",
// Request: new(types.PostableAcceptInvite),
// RequestContentType: "application/json",
// Response: new(types.User),
// ResponseContentType: "application/json",
// SuccessStatusCode: http.StatusCreated,
// ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
// Deprecated: false,
// SecuritySchemes: []handler.OpenAPISecurityScheme{},
// })).Methods(http.MethodPost).GetError(); err != nil {
// return err
// }
if err := router.Handle("/api/v1/invite/accept", handler.New(provider.authZ.OpenAccess(provider.userHandler.AcceptInvite), handler.OpenAPIDef{
ID: "AcceptInvite",
Tags: []string{"users"},
Summary: "Accept invite",
Description: "This endpoint accepts an invite by token",
Request: new(types.PostableAcceptInvite),
RequestContentType: "application/json",
Response: new(types.User),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: []handler.OpenAPISecurityScheme{},
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/pats", handler.New(provider.authZ.AdminAccess(provider.userHandler.CreateAPIKey), handler.OpenAPIDef{
ID: "CreateAPIKey",

View File

@@ -3,10 +3,9 @@ package flagger
import "github.com/SigNoz/signoz/pkg/types/featuretypes"
var (
FeatureUseSpanMetrics = featuretypes.MustNewName("use_span_metrics")
FeatureKafkaSpanEval = featuretypes.MustNewName("kafka_span_eval")
FeatureHideRootUser = featuretypes.MustNewName("hide_root_user")
FeatureSoftDeleteUsers = featuretypes.MustNewName("soft_delete_users")
FeatureUseSpanMetrics = featuretypes.MustNewName("use_span_metrics")
FeatureKafkaSpanEval = featuretypes.MustNewName("kafka_span_eval")
FeatureHideRootUser = featuretypes.MustNewName("hide_root_user")
)
func MustNewRegistry() featuretypes.Registry {
@@ -35,14 +34,6 @@ func MustNewRegistry() featuretypes.Registry {
DefaultVariant: featuretypes.MustNewName("disabled"),
Variants: featuretypes.NewBooleanVariants(),
},
&featuretypes.Feature{
Name: FeatureSoftDeleteUsers,
Kind: featuretypes.KindBoolean,
Stage: featuretypes.StageStable,
Description: "Controls whether users are soft deleted or not",
DefaultVariant: featuretypes.MustNewName("disabled"),
Variants: featuretypes.NewBooleanVariants(),
},
)
if err != nil {
panic(err)

View File

@@ -141,7 +141,7 @@ func (module *module) CreateCallbackAuthNSession(ctx context.Context, authNProvi
roleMapping := authDomain.AuthDomainConfig().RoleMapping
role := roleMapping.NewRoleFromCallbackIdentity(callbackIdentity)
user, err := types.NewUser(callbackIdentity.Name, callbackIdentity.Email, role, callbackIdentity.OrgID, types.UserStatusActive)
user, err := types.NewUser(callbackIdentity.Name, callbackIdentity.Email, role, callbackIdentity.OrgID)
if err != nil {
return "", err
}

View File

@@ -26,24 +26,24 @@ func NewHandler(module root.Module, getter root.Getter) root.Handler {
return &handler{module: module, getter: getter}
}
// func (h *handler) AcceptInvite(w http.ResponseWriter, r *http.Request) {
// ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
// defer cancel()
func (h *handler) AcceptInvite(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
// req := new(types.PostableAcceptInvite)
// if err := binding.JSON.BindBody(r.Body, req); err != nil {
// render.Error(w, err)
// return
// }
req := new(types.PostableAcceptInvite)
if err := binding.JSON.BindBody(r.Body, req); err != nil {
render.Error(w, err)
return
}
// user, err := h.module.AcceptInvite(ctx, req.InviteToken, req.Password)
// if err != nil {
// render.Error(w, err)
// return
// }
user, err := h.module.AcceptInvite(ctx, req.InviteToken, req.Password)
if err != nil {
render.Error(w, err)
return
}
// render.Success(w, http.StatusCreated, user)
// }
render.Success(w, http.StatusCreated, user)
}
func (h *handler) CreateInvite(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
@@ -61,7 +61,7 @@ func (h *handler) CreateInvite(rw http.ResponseWriter, r *http.Request) {
return
}
invitedUsers, err := h.module.CreateBulkInvite(ctx, valuer.MustNewUUID(claims.OrgID), valuer.MustNewUUID(claims.UserID), &types.PostableBulkInviteRequest{
invites, err := h.module.CreateBulkInvite(ctx, valuer.MustNewUUID(claims.OrgID), valuer.MustNewUUID(claims.UserID), &types.PostableBulkInviteRequest{
Invites: []types.PostableInvite{req},
})
if err != nil {
@@ -69,7 +69,7 @@ func (h *handler) CreateInvite(rw http.ResponseWriter, r *http.Request) {
return
}
render.Success(rw, http.StatusCreated, invitedUsers[0])
render.Success(rw, http.StatusCreated, invites[0])
}
func (h *handler) CreateBulkInvite(rw http.ResponseWriter, r *http.Request) {
@@ -103,63 +103,63 @@ func (h *handler) CreateBulkInvite(rw http.ResponseWriter, r *http.Request) {
render.Success(rw, http.StatusCreated, nil)
}
// func (h *handler) GetInvite(w http.ResponseWriter, r *http.Request) {
// ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
// defer cancel()
func (h *handler) GetInvite(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
// token := mux.Vars(r)["token"]
// invite, err := h.module.GetInviteByToken(ctx, token)
// if err != nil {
// render.Error(w, err)
// return
// }
token := mux.Vars(r)["token"]
invite, err := h.module.GetInviteByToken(ctx, token)
if err != nil {
render.Error(w, err)
return
}
// render.Success(w, http.StatusOK, invite)
// }
render.Success(w, http.StatusOK, invite)
}
// func (h *handler) ListInvite(w http.ResponseWriter, r *http.Request) {
// ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
// defer cancel()
func (h *handler) ListInvite(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
// claims, err := authtypes.ClaimsFromContext(ctx)
// if err != nil {
// render.Error(w, err)
// return
// }
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(w, err)
return
}
// invites, err := h.module.ListInvite(ctx, claims.OrgID)
// if err != nil {
// render.Error(w, err)
// return
// }
invites, err := h.module.ListInvite(ctx, claims.OrgID)
if err != nil {
render.Error(w, err)
return
}
// render.Success(w, http.StatusOK, invites)
// }
render.Success(w, http.StatusOK, invites)
}
// func (h *handler) DeleteInvite(w http.ResponseWriter, r *http.Request) {
// ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
// defer cancel()
func (h *handler) DeleteInvite(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
// id := mux.Vars(r)["id"]
id := mux.Vars(r)["id"]
// claims, err := authtypes.ClaimsFromContext(ctx)
// if err != nil {
// render.Error(w, err)
// return
// }
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(w, err)
return
}
// uuid, err := valuer.NewUUID(id)
// if err != nil {
// render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is invalid"))
// return
// }
uuid, err := valuer.NewUUID(id)
if err != nil {
render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is invalid"))
return
}
// if err := h.module.DeleteInvite(ctx, claims.OrgID, uuid); err != nil {
// render.Error(w, err)
// return
// }
// render.Success(w, http.StatusNoContent, nil)
// }
if err := h.module.DeleteInvite(ctx, claims.OrgID, uuid); err != nil {
render.Error(w, err)
return
}
render.Success(w, http.StatusNoContent, nil)
}
func (h *handler) GetUser(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)

View File

@@ -12,7 +12,6 @@ import (
"github.com/SigNoz/signoz/pkg/emailing"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/flagger"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/user"
root "github.com/SigNoz/signoz/pkg/modules/user"
@@ -20,7 +19,6 @@ import (
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/emailtypes"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/dustin/go-humanize"
@@ -35,11 +33,10 @@ type Module struct {
authz authz.AuthZ
analytics analytics.Analytics
config user.Config
flagger flagger.Flagger
}
// This module is a WIP, don't take inspiration from this.
func NewModule(store types.UserStore, tokenizer tokenizer.Tokenizer, emailing emailing.Emailing, providerSettings factory.ProviderSettings, orgSetter organization.Setter, authz authz.AuthZ, analytics analytics.Analytics, config user.Config, flagger flagger.Flagger) root.Module {
func NewModule(store types.UserStore, tokenizer tokenizer.Tokenizer, emailing emailing.Emailing, providerSettings factory.ProviderSettings, orgSetter organization.Setter, authz authz.AuthZ, analytics analytics.Analytics, config user.Config) root.Module {
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/modules/user/impluser")
return &Module{
store: store,
@@ -50,59 +47,57 @@ func NewModule(store types.UserStore, tokenizer tokenizer.Tokenizer, emailing em
analytics: analytics,
authz: authz,
config: config,
flagger: flagger,
}
}
// func (m *Module) AcceptInvite(ctx context.Context, token string, password string) (*types.User, error) {
// invite, err := m.store.GetInviteByToken(ctx, token)
// if err != nil {
// return nil, err
// }
func (m *Module) AcceptInvite(ctx context.Context, token string, password string) (*types.User, error) {
invite, err := m.store.GetInviteByToken(ctx, token)
if err != nil {
return nil, err
}
// user, err := types.NewUser(invite.Name, invite.Email, invite.Role, invite.OrgID)
// if err != nil {
// return nil, err
// }
user, err := types.NewUser(invite.Name, invite.Email, invite.Role, invite.OrgID)
if err != nil {
return nil, err
}
// factorPassword, err := types.NewFactorPassword(password, user.ID.StringValue())
// if err != nil {
// return nil, err
// }
factorPassword, err := types.NewFactorPassword(password, user.ID.StringValue())
if err != nil {
return nil, err
}
// err = m.CreateUser(ctx, user, root.WithFactorPassword(factorPassword))
// if err != nil {
// return nil, err
// }
err = m.CreateUser(ctx, user, root.WithFactorPassword(factorPassword))
if err != nil {
return nil, err
}
// if err := m.DeleteInvite(ctx, invite.OrgID.String(), invite.ID); err != nil {
// return nil, err
// }
if err := m.DeleteInvite(ctx, invite.OrgID.String(), invite.ID); err != nil {
return nil, err
}
// return user, nil
// }
return user, nil
}
// func (m *Module) GetInviteByToken(ctx context.Context, token string) (*types.Invite, error) {
// invite, err := m.store.GetInviteByToken(ctx, token)
// if err != nil {
// return nil, err
// }
func (m *Module) GetInviteByToken(ctx context.Context, token string) (*types.Invite, error) {
invite, err := m.store.GetInviteByToken(ctx, token)
if err != nil {
return nil, err
}
// return invite, nil
// }
return invite, nil
}
// CreateBulk implements invite.Module.
func (m *Module) CreateBulkInvite(ctx context.Context, orgID valuer.UUID, userID valuer.UUID, bulkInvites *types.PostableBulkInviteRequest) ([]*types.User, error) {
//! TODO this is an incomplete implementation - will fix this
func (m *Module) CreateBulkInvite(ctx context.Context, orgID valuer.UUID, userID valuer.UUID, bulkInvites *types.PostableBulkInviteRequest) ([]*types.Invite, error) {
creator, err := m.store.GetUser(ctx, userID)
if err != nil {
return nil, err
}
invitedUsers := make([]*types.User, 0, len(bulkInvites.Invites))
invites := make([]*types.Invite, 0, len(bulkInvites.Invites))
for _, invite := range bulkInvites.Invites {
// check and active user already exists with this email
// check if user exists
existingUser, err := m.store.GetUserByEmailAndOrgID(ctx, invite.Email, orgID)
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
return nil, err
@@ -110,91 +105,70 @@ func (m *Module) CreateBulkInvite(ctx context.Context, orgID valuer.UUID, userID
if existingUser != nil {
if err := existingUser.ErrIfRoot(); err != nil {
return nil, errors.WithAdditionalf(err, "Cannot send invite to root user")
return nil, errors.WithAdditionalf(err, "cannot send invite to root user")
}
}
// check if a pending invite already exists
if existingUser.Status == types.UserStatusPendingInvite {
return nil, errors.New(errors.TypeAlreadyExists, errors.CodeAlreadyExists, "An invite already exists for this email")
}
// if user is in soft deleted state, we reinitiate that
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
if existingUser != nil {
return nil, errors.New(errors.TypeAlreadyExists, errors.CodeAlreadyExists, "User already exists with the same email")
}
// Check if an invite already exists
existingInvite, err := m.store.GetInviteByEmailAndOrgID(ctx, invite.Email, orgID)
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
return nil, err
}
if existingInvite != nil {
return nil, errors.New(errors.TypeAlreadyExists, errors.CodeAlreadyExists, "An invite already exists for this email")
}
role, err := types.NewRole(invite.Role.String())
if err != nil {
return nil, err
}
// create a new user with pending invite status
newUser, err := types.NewUser(invite.Name, invite.Email, role, orgID, types.UserStatusPendingInvite)
newInvite, err := types.NewInvite(invite.Name, role, orgID, invite.Email)
if err != nil {
return nil, err
}
// generate a temp password
password, err := types.GenerateFactorPassword(newUser.ID.StringValue())
if err != nil {
return nil, err
}
// store the user and password in db
err = m.createUserWithoutGrant(ctx, newUser, root.WithFactorPassword(password))
if err != nil {
return nil, err
}
invitedUsers = append(invitedUsers, newUser)
newInvite.InviteLink = fmt.Sprintf("%s/signup?token=%s", invite.FrontendBaseUrl, newInvite.Token)
invites = append(invites, newInvite)
}
// send password reset emails to all the invited users
for i, invitedUser := range invitedUsers {
m.analytics.TrackUser(ctx, orgID.String(), creator.ID.String(), "Invite Sent", map[string]any{
"invitee_email": invitedUser.Email,
"invitee_role": invitedUser.Role,
})
err = m.store.CreateBulkInvite(ctx, invites)
if err != nil {
return nil, err
}
frontendBaseUrl := bulkInvites.Invites[i].FrontendBaseUrl
if frontendBaseUrl == "" {
m.settings.Logger().InfoContext(ctx, "frontend base url is not provided, skipping email", "invitee_email", invitedUser.Email)
for i := 0; i < len(invites); i++ {
m.analytics.TrackUser(ctx, orgID.String(), creator.ID.String(), "Invite Sent", map[string]any{"invitee_email": invites[i].Email, "invitee_role": invites[i].Role})
// if the frontend base url is not provided, we don't send the email
if bulkInvites.Invites[i].FrontendBaseUrl == "" {
m.settings.Logger().InfoContext(ctx, "frontend base url is not provided, skipping email", "invitee_email", invites[i].Email)
continue
}
// generate reset password token
resetPasswordToken, err := m.GetOrCreateResetPasswordToken(ctx, invitedUser.ID)
if err != nil {
m.settings.Logger().ErrorContext(ctx, "failed to create reset password token for invited user", "error", err)
continue
}
resetLink := m.resetLink(frontendBaseUrl, resetPasswordToken.Token)
tokenLifetime := m.config.Password.Reset.MaxTokenLifetime
humanizedTokenLifetime := strings.TrimSpace(humanize.RelTime(time.Now(), time.Now().Add(tokenLifetime), "", ""))
// TODO! improve invitation email text and add expiry details too
if err := m.emailing.SendHTML(ctx, invitedUser.Email.String(), "You're Invited to Join SigNoz", emailtypes.TemplateNameInvitationEmail, map[string]any{
if err := m.emailing.SendHTML(ctx, invites[i].Email.String(), "You're Invited to Join SigNoz", emailtypes.TemplateNameInvitationEmail, map[string]any{
"inviter_email": creator.Email,
"link": resetLink,
"Expiry": humanizedTokenLifetime,
"link": fmt.Sprintf("%s/signup?token=%s", bulkInvites.Invites[i].FrontendBaseUrl, invites[i].Token),
}); err != nil {
m.settings.Logger().ErrorContext(ctx, "failed to send invite email", "error", err)
m.settings.Logger().ErrorContext(ctx, "failed to send email", "error", err)
}
}
return invitedUsers, nil
return invites, nil
}
// func (m *Module) ListInvite(ctx context.Context, orgID string) ([]*types.User, error) {
// return m.store.ListPendingInviteUsers(ctx, orgID)
// }
func (m *Module) ListInvite(ctx context.Context, orgID string) ([]*types.Invite, error) {
return m.store.ListInvite(ctx, orgID)
}
// func (m *Module) DeleteInvite(ctx context.Context, orgID string, id valuer.UUID) error {
// return m.store.DeleteInvite(ctx, orgID, id)
// }
func (m *Module) DeleteInvite(ctx context.Context, orgID string, id valuer.UUID) error {
return m.store.DeleteInvite(ctx, orgID, id)
}
func (module *Module) CreateUser(ctx context.Context, input *types.User, opts ...root.CreateUserOption) error {
createUserOpts := root.NewCreateUserOptions(opts...)
@@ -325,23 +299,12 @@ func (module *Module) DeleteUser(ctx context.Context, orgID valuer.UUID, id stri
return err
}
evalCtx := featuretypes.NewFlaggerEvaluationContext(orgID)
softDeleteUsers := module.flagger.BooleanOrEmpty(ctx, flagger.FeatureSoftDeleteUsers, evalCtx)
if softDeleteUsers {
user.UpdateStatus(types.UserStatusDeleted)
if err := module.store.UpdateUser(ctx, orgID, user); err != nil {
return err
}
} else {
if err := module.store.DeleteUser(ctx, orgID.String(), user.ID.StringValue()); err != nil {
return err
}
if err := module.store.DeleteUser(ctx, orgID.String(), user.ID.StringValue()); err != nil {
return err
}
module.analytics.TrackUser(ctx, user.OrgID.String(), user.ID.String(), "User Deleted", map[string]any{
"deleted_by": deletedBy,
"is_soft_delete": softDeleteUsers,
"deleted_by": deletedBy,
})
return nil
@@ -429,7 +392,7 @@ func (module *Module) ForgotPassword(ctx context.Context, orgID valuer.UUID, ema
return err
}
resetLink := module.resetLink(frontendBaseURL, token.Token)
resetLink := fmt.Sprintf("%s/password-reset?token=%s", frontendBaseURL, token.Token)
tokenLifetime := module.config.Password.Reset.MaxTokenLifetime
humanizedTokenLifetime := strings.TrimSpace(humanize.RelTime(time.Now(), time.Now().Add(tokenLifetime), "", ""))
@@ -479,25 +442,6 @@ func (module *Module) UpdatePasswordByResetPasswordToken(ctx context.Context, to
return err
}
// update the status of user if this a newly invited user and also grant authz
if user.Status == types.UserStatusPendingInvite {
err = module.authz.Grant(
ctx,
user.OrgID,
roletypes.MustGetSigNozManagedRoleFromExistingRole(user.Role),
authtypes.MustNewSubject(authtypes.TypeableUser, user.ID.StringValue(), user.OrgID, nil),
)
if err != nil {
return err
}
user.UpdateStatus(types.UserStatusActive)
err = module.store.UpdateUser(ctx, user.OrgID, user)
if err != nil {
return err
}
}
return module.store.UpdatePassword(ctx, password)
}
@@ -653,7 +597,3 @@ func (module *Module) createUserWithoutGrant(ctx context.Context, input *types.U
return nil
}
func (module *Module) resetLink(frontendBaseUrl string, token string) string {
return fmt.Sprintf("%s/password-reset?token=%s", frontendBaseUrl, token)
}

View File

@@ -26,75 +26,75 @@ func NewStore(sqlstore sqlstore.SQLStore, settings factory.ProviderSettings) typ
}
// CreateBulkInvite implements types.InviteStore.
// func (store *store) CreateBulkInvite(ctx context.Context, invites []*types.Invite) error {
// _, err := store.sqlstore.BunDB().NewInsert().
// Model(&invites).
// Exec(ctx)
func (store *store) CreateBulkInvite(ctx context.Context, invites []*types.Invite) error {
_, err := store.sqlstore.BunDB().NewInsert().
Model(&invites).
Exec(ctx)
// if err != nil {
// return store.sqlstore.WrapAlreadyExistsErrf(err, types.ErrInviteAlreadyExists, "invite with email: %s already exists in org: %s", invites[0].Email, invites[0].OrgID)
// }
// return nil
// }
if err != nil {
return store.sqlstore.WrapAlreadyExistsErrf(err, types.ErrInviteAlreadyExists, "invite with email: %s already exists in org: %s", invites[0].Email, invites[0].OrgID)
}
return nil
}
// Delete implements types.InviteStore.
// func (store *store) DeleteInvite(ctx context.Context, orgID string, id valuer.UUID) error {
// _, err := store.sqlstore.BunDB().NewDelete().
// Model(&types.Invite{}).
// Where("org_id = ?", orgID).
// Where("id = ?", id).
// Exec(ctx)
// if err != nil {
// return store.sqlstore.WrapNotFoundErrf(err, types.ErrInviteNotFound, "invite with id: %s does not exist in org: %s", id.StringValue(), orgID)
// }
// return nil
// }
func (store *store) DeleteInvite(ctx context.Context, orgID string, id valuer.UUID) error {
_, err := store.sqlstore.BunDB().NewDelete().
Model(&types.Invite{}).
Where("org_id = ?", orgID).
Where("id = ?", id).
Exec(ctx)
if err != nil {
return store.sqlstore.WrapNotFoundErrf(err, types.ErrInviteNotFound, "invite with id: %s does not exist in org: %s", id.StringValue(), orgID)
}
return nil
}
// func (store *store) GetInviteByEmailAndOrgID(ctx context.Context, email valuer.Email, orgID valuer.UUID) (*types.Invite, error) {
// invite := new(types.Invite)
func (store *store) GetInviteByEmailAndOrgID(ctx context.Context, email valuer.Email, orgID valuer.UUID) (*types.Invite, error) {
invite := new(types.Invite)
// err := store.
// sqlstore.
// BunDBCtx(ctx).NewSelect().
// Model(invite).
// Where("email = ?", email).
// Where("org_id = ?", orgID).
// Scan(ctx)
// if err != nil {
// return nil, store.sqlstore.WrapNotFoundErrf(err, types.ErrInviteNotFound, "invite with email %s does not exist in org %s", email, orgID)
// }
err := store.
sqlstore.
BunDBCtx(ctx).NewSelect().
Model(invite).
Where("email = ?", email).
Where("org_id = ?", orgID).
Scan(ctx)
if err != nil {
return nil, store.sqlstore.WrapNotFoundErrf(err, types.ErrInviteNotFound, "invite with email %s does not exist in org %s", email, orgID)
}
// return invite, nil
// }
return invite, nil
}
// func (store *store) GetInviteByToken(ctx context.Context, token string) (*types.GettableInvite, error) {
// invite := new(types.Invite)
func (store *store) GetInviteByToken(ctx context.Context, token string) (*types.GettableInvite, error) {
invite := new(types.Invite)
// err := store.
// sqlstore.
// BunDBCtx(ctx).
// NewSelect().
// Model(invite).
// Where("token = ?", token).
// Scan(ctx)
// if err != nil {
// return nil, store.sqlstore.WrapNotFoundErrf(err, types.ErrInviteNotFound, "invite does not exist", token)
// }
err := store.
sqlstore.
BunDBCtx(ctx).
NewSelect().
Model(invite).
Where("token = ?", token).
Scan(ctx)
if err != nil {
return nil, store.sqlstore.WrapNotFoundErrf(err, types.ErrInviteNotFound, "invite does not exist", token)
}
// return invite, nil
// }
return invite, nil
}
// func (store *store) ListInvite(ctx context.Context, orgID string) ([]*types.Invite, error) {
// invites := new([]*types.Invite)
// err := store.sqlstore.BunDB().NewSelect().
// Model(invites).
// Where("org_id = ?", orgID).
// Scan(ctx)
// if err != nil {
// return nil, store.sqlstore.WrapNotFoundErrf(err, types.ErrInviteNotFound, "invite with org id: %s does not exist", orgID)
// }
// return *invites, nil
// }
func (store *store) ListInvite(ctx context.Context, orgID string) ([]*types.Invite, error) {
invites := new([]*types.Invite)
err := store.sqlstore.BunDB().NewSelect().
Model(invites).
Where("org_id = ?", orgID).
Scan(ctx)
if err != nil {
return nil, store.sqlstore.WrapNotFoundErrf(err, types.ErrInviteNotFound, "invite with org id: %s does not exist", orgID)
}
return *invites, nil
}
func (store *store) CreatePassword(ctx context.Context, password *types.FactorPassword) error {
_, err := store.
@@ -202,7 +202,6 @@ func (store *store) GetUsersByRoleAndOrgID(ctx context.Context, role types.Role,
Model(&users).
Where("org_id = ?", orgID).
Where("role = ?", role).
Where("status = ?", types.UserStatusActive.StringValue()).
Scan(ctx)
if err != nil {
return nil, err
@@ -222,7 +221,6 @@ func (store *store) UpdateUser(ctx context.Context, orgID valuer.UUID, user *typ
Column("role").
Column("is_root").
Column("updated_at").
Column("status").
Where("org_id = ?", orgID).
Where("id = ?", user.ID).
Exec(ctx)
@@ -241,7 +239,6 @@ func (store *store) ListUsersByOrgID(ctx context.Context, orgID valuer.UUID) ([]
NewSelect().
Model(&users).
Where("org_id = ?", orgID).
Where("status = ?", types.UserStatusActive.StringValue()).
Scan(ctx)
if err != nil {
return nil, err
@@ -577,7 +574,6 @@ func (store *store) CountByOrgID(ctx context.Context, orgID valuer.UUID) (int64,
NewSelect().
Model(user).
Where("org_id = ?", orgID).
Where("status = ?", types.UserStatusActive.StringValue()).
Count(ctx)
if err != nil {
return 0, err
@@ -642,21 +638,3 @@ func (store *store) ListUsersByEmailAndOrgIDs(ctx context.Context, email valuer.
return users, nil
}
func (store *store) ListPendingInviteUsers(ctx context.Context, orgID valuer.UUID) ([]*types.User, error) {
users := []*types.User{}
err := store.
sqlstore.
BunDBCtx(ctx).
NewSelect().
Model(&users).
Where("org_id = ?", orgID).
Where("status = ?", types.UserStatusPendingInvite.StringValue()).
Scan(ctx)
if err != nil {
return nil, err
}
return users, nil
}

View File

@@ -40,11 +40,11 @@ type Module interface {
DeleteUser(ctx context.Context, orgID valuer.UUID, id string, deletedBy string) error
// invite
CreateBulkInvite(ctx context.Context, orgID valuer.UUID, userID valuer.UUID, bulkInvites *types.PostableBulkInviteRequest) ([]*types.User, error)
// ListInvite(ctx context.Context, orgID string) ([]*types.User, error)
// DeleteInvite(ctx context.Context, orgID string, id valuer.UUID) error
// AcceptInvite(ctx context.Context, token string, password string) (*types.User, error)
// GetInviteByToken(ctx context.Context, token string) (*types.Invite, error)
CreateBulkInvite(ctx context.Context, orgID valuer.UUID, userID valuer.UUID, bulkInvites *types.PostableBulkInviteRequest) ([]*types.Invite, error)
ListInvite(ctx context.Context, orgID string) ([]*types.Invite, error)
DeleteInvite(ctx context.Context, orgID string, id valuer.UUID) error
AcceptInvite(ctx context.Context, token string, password string) (*types.User, error)
GetInviteByToken(ctx context.Context, token string) (*types.Invite, error)
// API KEY
CreateAPIKey(ctx context.Context, apiKey *types.StorableAPIKey) error
@@ -85,10 +85,10 @@ type Getter interface {
type Handler interface {
// invite
CreateInvite(http.ResponseWriter, *http.Request)
// AcceptInvite(http.ResponseWriter, *http.Request)
// GetInvite(http.ResponseWriter, *http.Request) // public function
// ListInvite(http.ResponseWriter, *http.Request)
// DeleteInvite(http.ResponseWriter, *http.Request)
AcceptInvite(http.ResponseWriter, *http.Request)
GetInvite(http.ResponseWriter, *http.Request) // public function
ListInvite(http.ResponseWriter, *http.Request)
DeleteInvite(http.ResponseWriter, *http.Request)
CreateBulkInvite(http.ResponseWriter, *http.Request)
ListUsers(http.ResponseWriter, *http.Request)

View File

@@ -50,7 +50,7 @@ func TestNewHandlers(t *testing.T) {
userGetter := impluser.NewGetter(impluser.NewStore(sqlstore, providerSettings), flagger)
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, userGetter, flagger)
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, userGetter)
querierHandler := querier.NewHandler(providerSettings, nil, nil)
handlers := NewHandlers(modules, providerSettings, nil, querierHandler, nil, nil, nil, nil, nil, nil, nil)

View File

@@ -8,7 +8,6 @@ import (
"github.com/SigNoz/signoz/pkg/cache"
"github.com/SigNoz/signoz/pkg/emailing"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/flagger"
"github.com/SigNoz/signoz/pkg/modules/apdex"
"github.com/SigNoz/signoz/pkg/modules/apdex/implapdex"
"github.com/SigNoz/signoz/pkg/modules/authdomain"
@@ -87,11 +86,10 @@ func NewModules(
config Config,
dashboard dashboard.Module,
userGetter user.Getter,
flagger flagger.Flagger,
) Modules {
quickfilter := implquickfilter.NewModule(implquickfilter.NewStore(sqlstore))
orgSetter := implorganization.NewSetter(implorganization.NewStore(sqlstore), alertmanager, quickfilter)
user := impluser.NewModule(impluser.NewStore(sqlstore, providerSettings), tokenizer, emailing, providerSettings, orgSetter, authz, analytics, config.User, flagger)
user := impluser.NewModule(impluser.NewStore(sqlstore, providerSettings), tokenizer, emailing, providerSettings, orgSetter, authz, analytics, config.User)
ruleStore := sqlrulestore.NewRuleStore(sqlstore, queryParser, providerSettings)
return Modules{

View File

@@ -49,7 +49,7 @@ func TestNewModules(t *testing.T) {
userGetter := impluser.NewGetter(impluser.NewStore(sqlstore, providerSettings), flagger)
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, userGetter, flagger)
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, userGetter)
reflectVal := reflect.ValueOf(modules)
for i := 0; i < reflectVal.NumField(); i++ {

View File

@@ -388,7 +388,7 @@ func New(
}
// Initialize all modules
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, analytics, querier, telemetrystore, telemetryMetadataStore, authNs, authz, cache, queryParser, config, dashboard, userGetter, flagger)
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, analytics, querier, telemetrystore, telemetryMetadataStore, authNs, authz, cache, queryParser, config, dashboard, userGetter)
userService := impluser.NewService(providerSettings, impluser.NewStore(sqlstore, providerSettings), modules.User, orgGetter, authz, config.User.Root)

View File

@@ -1,88 +0,0 @@
package sqlmigration
import (
"context"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlschema"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/uptrace/bun"
"github.com/uptrace/bun/migrate"
)
type addStatusUser struct {
sqlstore sqlstore.SQLStore
sqlschema sqlschema.SQLSchema
}
func AddStatusUserFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(
factory.MustNewName("add_status_user"),
func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
return &addStatusUser{
sqlstore: sqlstore,
sqlschema: sqlschema,
}, nil
},
)
}
func (migration *addStatusUser) Register(migrations *migrate.Migrations) error {
if err := migrations.Register(migration.Up, migration.Down); err != nil {
return err
}
return nil
}
func (migration *addStatusUser) Up(ctx context.Context, db *bun.DB) error {
if err := migration.sqlschema.ToggleFKEnforcement(ctx, db, false); err != nil {
return err
}
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() {
_ = tx.Rollback()
}()
table, uniqueConstraints, err := migration.sqlschema.GetTable(ctx, sqlschema.TableName("users"))
if err != nil {
return err
}
column := &sqlschema.Column{
Name: sqlschema.ColumnName("status"),
DataType: sqlschema.DataTypeText,
Nullable: false,
}
sqls := migration.sqlschema.Operator().AddColumn(table, uniqueConstraints, column, false)
indexSqls := migration.sqlschema.Operator().CreateIndex(&sqlschema.UniqueIndex{TableName: "users", ColumnNames: []sqlschema.ColumnName{"email", "org_id"}})
sqls = append(sqls, indexSqls...)
for _, sql := range sqls {
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
return err
}
}
if err := tx.Commit(); err != nil {
return err
}
if err := migration.sqlschema.ToggleFKEnforcement(ctx, db, true); err != nil {
return err
}
return nil
}
func (migration *addStatusUser) Down(ctx context.Context, db *bun.DB) error {
return nil
}

View File

@@ -1,70 +0,0 @@
package sqlmigration
import (
"context"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlschema"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/uptrace/bun"
"github.com/uptrace/bun/migrate"
)
type deprecateUserInvite struct {
sqlstore sqlstore.SQLStore
sqlschema sqlschema.SQLSchema
}
func NewDeprecateUserInviteFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(
factory.MustNewName("deprecate_user_invite"),
func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
return &deprecateUserInvite{
sqlstore: sqlstore,
sqlschema: sqlschema,
}, nil
},
)
}
func (migration *deprecateUserInvite) Register(migrations *migrate.Migrations) error {
if err := migrations.Register(migration.Up, migration.Down); err != nil {
return err
}
return nil
}
func (migration *deprecateUserInvite) Up(ctx context.Context, db *bun.DB) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() {
_ = tx.Rollback()
}()
table, _, err := migration.sqlschema.GetTable(ctx, sqlschema.TableName("user_invite"))
if err != nil {
return err
}
dropTableSqls := migration.sqlschema.Operator().DropTable(table)
for _, sql := range dropTableSqls {
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
return err
}
}
if err := tx.Commit(); err != nil {
return err
}
return nil
}
func (migration *deprecateUserInvite) Down(ctx context.Context, db *bun.DB) error {
return nil
}

View File

@@ -1,12 +1,12 @@
package types
import (
// "encoding/json"
// "time"
"encoding/json"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/valuer"
// "github.com/uptrace/bun"
"github.com/uptrace/bun"
)
var (
@@ -14,38 +14,37 @@ var (
ErrInviteNotFound = errors.MustNewCode("invite_not_found")
)
// TODO - remove this commented lines
// type GettableInvite = Invite
type GettableInvite = Invite
// type Invite struct {
// bun.BaseModel `bun:"table:user_invite"`
type Invite struct {
bun.BaseModel `bun:"table:user_invite"`
// Identifiable
// TimeAuditable
// Name string `bun:"name,type:text" json:"name"`
// Email valuer.Email `bun:"email,type:text" json:"email"`
// Token string `bun:"token,type:text" json:"token"`
// Role Role `bun:"role,type:text" json:"role"`
// OrgID valuer.UUID `bun:"org_id,type:text" json:"orgId"`
Identifiable
TimeAuditable
Name string `bun:"name,type:text" json:"name"`
Email valuer.Email `bun:"email,type:text" json:"email"`
Token string `bun:"token,type:text" json:"token"`
Role Role `bun:"role,type:text" json:"role"`
OrgID valuer.UUID `bun:"org_id,type:text" json:"orgId"`
// InviteLink string `bun:"-" json:"inviteLink"`
// }
InviteLink string `bun:"-" json:"inviteLink"`
}
// type InviteEmailData struct {
// CustomerName string
// InviterName string
// InviterEmail string
// Link string
// }
type InviteEmailData struct {
CustomerName string
InviterName string
InviterEmail string
Link string
}
// type PostableAcceptInvite struct {
// DisplayName string `json:"displayName"`
// InviteToken string `json:"token"`
// Password string `json:"password"`
type PostableAcceptInvite struct {
DisplayName string `json:"displayName"`
InviteToken string `json:"token"`
Password string `json:"password"`
// // reference URL to track where the register request is coming from
// SourceURL string `json:"sourceUrl"`
// }
// reference URL to track where the register request is coming from
SourceURL string `json:"sourceUrl"`
}
type PostableInvite struct {
Name string `json:"name"`
@@ -58,45 +57,45 @@ type PostableBulkInviteRequest struct {
Invites []PostableInvite `json:"invites"`
}
// type GettableCreateInviteResponse struct {
// InviteToken string `json:"token"`
// }
type GettableCreateInviteResponse struct {
InviteToken string `json:"token"`
}
// func NewInvite(name string, role Role, orgID valuer.UUID, email valuer.Email) (*Invite, error) {
// invite := &Invite{
// Identifiable: Identifiable{
// ID: valuer.GenerateUUID(),
// },
// Name: name,
// Email: email,
// Token: valuer.GenerateUUID().String(),
// Role: role,
// OrgID: orgID,
// TimeAuditable: TimeAuditable{
// CreatedAt: time.Now(),
// UpdatedAt: time.Now(),
// },
// }
func NewInvite(name string, role Role, orgID valuer.UUID, email valuer.Email) (*Invite, error) {
invite := &Invite{
Identifiable: Identifiable{
ID: valuer.GenerateUUID(),
},
Name: name,
Email: email,
Token: valuer.GenerateUUID().String(),
Role: role,
OrgID: orgID,
TimeAuditable: TimeAuditable{
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
}
// return invite, nil
// }
return invite, nil
}
// func (request *PostableAcceptInvite) UnmarshalJSON(data []byte) error {
// type Alias PostableAcceptInvite
func (request *PostableAcceptInvite) UnmarshalJSON(data []byte) error {
type Alias PostableAcceptInvite
// var temp Alias
// if err := json.Unmarshal(data, &temp); err != nil {
// return err
// }
var temp Alias
if err := json.Unmarshal(data, &temp); err != nil {
return err
}
// if temp.InviteToken == "" {
// return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "invite token is required")
// }
if temp.InviteToken == "" {
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "invite token is required")
}
// if !IsPasswordValid(temp.Password) {
// return ErrInvalidPassword
// }
if !IsPasswordValid(temp.Password) {
return ErrInvalidPassword
}
// *request = PostableAcceptInvite(temp)
// return nil
// }
*request = PostableAcceptInvite(temp)
return nil
}

View File

@@ -23,25 +23,17 @@ var (
ErrCodeRootUserOperationUnsupported = errors.MustNewCode("root_user_operation_unsupported")
)
var (
UserStatusPendingInvite = valuer.NewString("pending_invite")
UserStatusActive = valuer.NewString("active")
UserStatusDeleted = valuer.NewString("deleted")
ValidUserStatus = []valuer.String{UserStatusPendingInvite, UserStatusActive, UserStatusDeleted}
)
type GettableUser = User
type User struct {
bun.BaseModel `bun:"table:users"`
Identifiable
DisplayName string `bun:"display_name" json:"displayName"`
Email valuer.Email `bun:"email" json:"email"`
Role Role `bun:"role" json:"role"`
OrgID valuer.UUID `bun:"org_id" json:"orgId"`
IsRoot bool `bun:"is_root" json:"isRoot"`
Status valuer.String `bun:"status" json:"status"`
DisplayName string `bun:"display_name" json:"displayName"`
Email valuer.Email `bun:"email" json:"email"`
Role Role `bun:"role" json:"role"`
OrgID valuer.UUID `bun:"org_id" json:"orgId"`
IsRoot bool `bun:"is_root" json:"isRoot"`
TimeAuditable
}
@@ -53,7 +45,7 @@ type PostableRegisterOrgAndAdmin struct {
OrgName string `json:"orgName"`
}
func NewUser(displayName string, email valuer.Email, role Role, orgID valuer.UUID, status valuer.String) (*User, error) {
func NewUser(displayName string, email valuer.Email, role Role, orgID valuer.UUID) (*User, error) {
if email.IsZero() {
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "email is required")
}
@@ -75,7 +67,6 @@ func NewUser(displayName string, email valuer.Email, role Role, orgID valuer.UUI
Role: role,
OrgID: orgID,
IsRoot: false,
Status: status,
TimeAuditable: TimeAuditable{
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
@@ -101,7 +92,6 @@ func NewRootUser(displayName string, email valuer.Email, orgID valuer.UUID) (*Us
Role: RoleAdmin,
OrgID: orgID,
IsRoot: true,
Status: UserStatusActive,
TimeAuditable: TimeAuditable{
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
@@ -121,11 +111,6 @@ func (u *User) Update(displayName string, role Role) {
u.UpdatedAt = time.Now()
}
func (u *User) UpdateStatus(status valuer.String) {
u.Status = status
u.UpdatedAt = time.Now()
}
// PromoteToRoot promotes the user to a root user with admin role.
func (u *User) PromoteToRoot() {
u.IsRoot = true
@@ -154,7 +139,6 @@ func NewTraitsFromUser(user *User) map[string]any {
"role": user.Role,
"email": user.Email.String(),
"display_name": user.DisplayName,
"status": user.Status,
"created_at": user.CreatedAt,
}
}
@@ -177,15 +161,15 @@ func (request *PostableRegisterOrgAndAdmin) UnmarshalJSON(data []byte) error {
type UserStore interface {
// invite
// CreateBulkInvite(ctx context.Context, invites []*Invite) error
// ListInvite(ctx context.Context, orgID string) ([]*Invite, error)
// DeleteInvite(ctx context.Context, orgID string, id valuer.UUID) error
CreateBulkInvite(ctx context.Context, invites []*Invite) error
ListInvite(ctx context.Context, orgID string) ([]*Invite, error)
DeleteInvite(ctx context.Context, orgID string, id valuer.UUID) error
// Get invite by token.
// GetInviteByToken(ctx context.Context, token string) (*Invite, error)
GetInviteByToken(ctx context.Context, token string) (*Invite, error)
// Get invite by email and org.
// GetInviteByEmailAndOrgID(ctx context.Context, email valuer.Email, orgID valuer.UUID) (*Invite, error)
GetInviteByEmailAndOrgID(ctx context.Context, email valuer.Email, orgID valuer.UUID) (*Invite, error)
// Creates a user.
CreateUser(ctx context.Context, user *User) error
@@ -211,9 +195,6 @@ type UserStore interface {
// List users by email and org ids.
ListUsersByEmailAndOrgIDs(ctx context.Context, email valuer.Email, orgIDs []valuer.UUID) ([]*User, error)
// List users in pending invite status
ListPendingInviteUsers(ctx context.Context, orgID valuer.UUID) ([]*User, error)
UpdateUser(ctx context.Context, orgID valuer.UUID, user *User) error
DeleteUser(ctx context.Context, orgID string, id string) error