Allow customizing colors, site name and logo (#1362)

Closes #1259

Signed-off-by: Sergio Castaño Arteaga <tegioz@icloud.com>
Signed-off-by: Cintia Sanchez Garcia <cynthiasg@icloud.com>
Co-authored-by: Sergio Castaño Arteaga <tegioz@icloud.com>
Co-authored-by: Cintia Sanchez Garcia <cynthiasg@icloud.com>
This commit is contained in:
Sergio C. Arteaga 2021-05-31 14:06:25 +02:00 committed by GitHub
parent 30fc83ace6
commit 012a983b9b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
214 changed files with 1425 additions and 1105 deletions

View File

@ -2,7 +2,7 @@ apiVersion: v2
name: artifact-hub
description: Artifact Hub is a web-based application that enables finding, installing, and publishing Kubernetes packages.
type: application
version: 0.19.5
version: 0.19.6
appVersion: 0.19.0
kubeVersion: ">= 1.14.0-0"
home: https://artifacthub.io

View File

@ -73,3 +73,14 @@ stringData:
xffIndex: {{ .Values.hub.server.xffIndex }}
analytics:
gaTrackingID: {{ .Values.hub.analytics.gaTrackingID }}
theme:
colors:
primary: {{ .Values.hub.theme.colors.primary }}
secondary: {{ .Values.hub.theme.colors.secondary }}
images:
appleTouchIcon192: {{ .Values.hub.theme.images.appleTouchIcon192 }}
appleTouchIcon512: {{ .Values.hub.theme.images.appleTouchIcon512 }}
openGraphImage: {{ .Values.hub.theme.images.openGraphImage }}
shortcutIcon: {{ .Values.hub.theme.images.shortcutIcon }}
websiteLogo: {{ .Values.hub.theme.images.websiteLogo }}
siteName: {{ .Values.hub.theme.siteName }}

View File

@ -507,9 +507,77 @@
}
},
"required": ["port", "type"]
},
"theme": {
"type": "object",
"properties": {
"colors": {
"title": "Colors used in the website",
"type": "object",
"properties": {
"primary": {
"title": "Primary color",
"description": "Primary color used in the website. For an optimal experience, it's better to use colors that play well with white fonts.",
"type": "string",
"default": "#417598"
},
"secondary": {
"title": "Secondary color",
"description": "Secondary color used in the website, usually a darker version of the primary color. For an optimal experience, it's better to use colors that play well with white fonts.",
"type": "string",
"default": "#2D4857"
}
},
"required": ["primary", "secondary"]
},
"images": {
"title": "Images used in the website",
"type": "object",
"properties": {
"appleTouchIcon192": {
"title": "Apple touch icon (192x192)",
"description": "URL of the image used for the Apple touch icon (192x192).",
"type": "string",
"default": "/static/media/logo192_v2.png"
},
"appleTouchIcon512": {
"title": "Apple touch icon (512x512)",
"description": "URL of the image used for the Apple touch icon (512x512).",
"type": "string",
"default": "/static/media/logo512_v2.png"
},
"openGraphImage": {
"title": "Open Graph image",
"description": "URL of the image used in the og:image tag. This image is displayed when an Artifact Hub link is shared in Twitter or Slack, for example. The URL must use `https`.",
"type": "string",
"default": "/static/media/artifactHub_v2.png"
},
"shortcutIcon": {
"title": "Shortcut icon",
"description": "URL of the image used for the shortcut icon (also known as favicon).",
"type": "string",
"default": "/static/media/logo_v2.png"
},
"websiteLogo": {
"title": "Website logo",
"description": "URL of the logo used in the website header. For an optimal experience, it's better to use a white logo with transparent background, with no margin around it. It'll be displayed using a maximum height of 20px and a maximum width of 185px.",
"type": "string",
"default": "/static/media/logo/artifacthub-brand-white.svg"
}
},
"required": ["appleTouchIcon192", "appleTouchIcon512", "openGraphImage", "shortcutIcon", "websiteLogo"]
},
"siteName": {
"title": "Name of the site",
"description": "This name is displayed in some places in the website and email templates. When a different value than the default one (Artifact Hub) is provided, the site enters `white label` mode. In this mode, some sections of the website are displayed in a more generic way, omitting certain parts that are unique to Artifact Hub.",
"type": "string",
"default": "Artifact Hub"
}
},
"required": ["colors", "images", "siteName"]
}
},
"required": ["ingress", "service", "deploy", "server"]
"required": ["ingress", "service", "deploy", "server", "theme"]
},
"imagePullSecrets": {
"type": "array",

View File

@ -110,6 +110,17 @@ hub:
xffIndex: 0
analytics:
gaTrackingID: ""
theme:
colors:
primary: "#417598"
secondary: "#2D4857"
images:
appleTouchIcon192: "/static/media/logo192_v2.png"
appleTouchIcon512: "/static/media/logo512_v2.png"
openGraphImage: "/static/media/artifactHub_v2.png"
shortcutIcon: "/static/media/logo_v2.png"
websiteLogo: "/static/media/logo/artifacthub-brand-white.svg"
siteName: "Artifact hub"
scanner:
cronjob:

View File

@ -59,8 +59,8 @@ func main() {
// Setup and launch http server
ctx, stop := context.WithCancel(context.Background())
hSvc := &handlers.Services{
OrganizationManager: org.NewManager(db, es, az),
UserManager: user.NewManager(db, es),
OrganizationManager: org.NewManager(cfg, db, es, az),
UserManager: user.NewManager(cfg, db, es),
RepositoryManager: repo.NewManager(cfg, db, az, hc),
PackageManager: pkg.NewManager(db),
SubscriptionManager: subscription.NewManager(db),
@ -114,6 +114,7 @@ func main() {
// Setup and launch notifications dispatcher
nSvc := &notification.Services{
Cfg: cfg,
DB: db,
ES: es,
NotificationManager: notification.NewManager(),
@ -122,7 +123,7 @@ func main() {
PackageManager: pkg.NewManager(db),
HTTPClient: hc,
}
notificationsDispatcher := notification.NewDispatcher(cfg, nSvc)
notificationsDispatcher := notification.NewDispatcher(nSvc)
wg.Add(1)
go notificationsDispatcher.Run(ctx, &wg)

View File

@ -101,7 +101,7 @@
}
.line {
border-top: 7px solid #417598;
border-top: 7px solid {{ .Theme.PrimaryColor }};
}
.line-danger {
@ -113,17 +113,17 @@
}
.AHlink {
color: #2d4857;
color: {{ .Theme.SecondaryColor }};
}
.AHbtn {
background-color: #2d4857;
border: solid 1px #2d4857;
background-color: {{ .Theme.SecondaryColor }};
border: solid 1px {{ .Theme.SecondaryColor }};
color: #ffffff;
}
.hr {
border-top: 1px solid #417598;
border-top: 1px solid {{ .Theme.PrimaryColor }};
}
.text-muted {

View File

@ -90,7 +90,7 @@ func Setup(ctx context.Context, cfg *viper.Viper, svc *Services) (*Handlers, err
Organizations: org.NewHandlers(svc.OrganizationManager, svc.Authorizer, cfg),
Users: userHandlers,
Repositories: repo.NewHandlers(svc.RepositoryManager),
Repositories: repo.NewHandlers(cfg, svc.RepositoryManager),
Packages: pkg.NewHandlers(svc.PackageManager, svc.RepositoryManager, cfg, svc.HTTPClient),
Subscriptions: subscription.NewHandlers(svc.SubscriptionManager),
Webhooks: webhook.NewHandlers(svc.WebhookManager, svc.HTTPClient),
@ -142,7 +142,7 @@ func (h *Handlers) setupRouter() {
if h.cfg.GetBool("server.basicAuth.enabled") {
r.Use(h.Users.BasicAuth)
}
r.NotFound(h.Static.ServeIndex)
r.NotFound(h.Static.Index)
// API
r.Route("/api/v1", func(r chi.Router) {
@ -384,8 +384,8 @@ func (h *Handlers) setupRouter() {
// Index special entry points
r.Route("/packages", func(r chi.Router) {
r.Route("/{^helm$|^falco$|^opa$|^olm|^tbaction|^krew|^helm-plugin|^tekton-task|^keda-scaler|^coredns$}/{repoName}/{packageName}", func(r chi.Router) {
r.With(h.Packages.InjectIndexMeta).Get("/{version}", h.Static.ServeIndex)
r.With(h.Packages.InjectIndexMeta).Get("/", h.Static.ServeIndex)
r.With(h.Packages.InjectIndexMeta).Get("/{version}", h.Static.Index)
r.With(h.Packages.InjectIndexMeta).Get("/", h.Static.Index)
})
})
@ -408,7 +408,7 @@ func (h *Handlers) setupRouter() {
w.Header().Set("Cache-Control", helpers.BuildCacheControlHeader(5*time.Minute))
http.ServeFile(w, r, path.Join(widgetBuildPath, "static/js/artifacthub-widget.js"))
})
r.Get("/", h.Static.ServeIndex)
r.Get("/", h.Static.Index)
h.Router = r
}

View File

@ -3,12 +3,14 @@ package repo
import (
"encoding/json"
"net/http"
"strings"
"github.com/artifacthub/hub/internal/handlers/helpers"
"github.com/artifacthub/hub/internal/hub"
"github.com/go-chi/chi"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/spf13/viper"
)
const (
@ -18,13 +20,15 @@ const (
// Handlers represents a group of http handlers in charge of handling
// repositories operations.
type Handlers struct {
cfg *viper.Viper
repoManager hub.RepositoryManager
logger zerolog.Logger
}
// NewHandlers creates a new Handlers instance.
func NewHandlers(repoManager hub.RepositoryManager) *Handlers {
func NewHandlers(cfg *viper.Viper, repoManager hub.RepositoryManager) *Handlers {
return &Handlers{
cfg: cfg,
repoManager: repoManager,
logger: log.With().Str("handlers", "repo").Logger(),
}
@ -51,9 +55,9 @@ func (h *Handlers) Add(w http.ResponseWriter, r *http.Request) {
// repository badge.
func (h *Handlers) Badge(w http.ResponseWriter, r *http.Request) {
data := map[string]interface{}{
"color": "2d4857",
"label": "Artifact Hub",
"labelColor": "417598",
"color": strings.TrimPrefix(h.cfg.GetString("theme.colors.secondary"), "#"),
"label": h.cfg.GetString("theme.siteName"),
"labelColor": strings.TrimPrefix(h.cfg.GetString("theme.colors.primary"), "#"),
"logoSvg": logoSVG,
"logoWidth": 18,
"message": chi.URLParam(r, "repoName"),

View File

@ -17,6 +17,7 @@ import (
"github.com/artifacthub/hub/internal/tests"
"github.com/go-chi/chi"
"github.com/rs/zerolog"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
@ -156,7 +157,7 @@ func TestBadge(t *testing.T) {
assert.Equal(t, http.StatusOK, resp.StatusCode)
assert.Equal(t, "application/json", h.Get("Content-Type"))
assert.Equal(t, helpers.BuildCacheControlHeader(helpers.DefaultAPICacheMaxAge), h.Get("Cache-Control"))
assert.Equal(t, []byte(`{"color":"2d4857","label":"Artifact Hub","labelColor":"417598","logoSvg":"\u003csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#ffffff\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-hexagon\"\u003e\u003cpath d=\"M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z\"\u003e\u003c/path\u003e\u003c/svg\u003e","logoWidth":18,"message":"artifact-hub","schemaVersion":1,"style":"flat"}`), data)
assert.Equal(t, []byte(`{"color":"2D4857","label":"Artifact Hub","labelColor":"417598","logoSvg":"\u003csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#ffffff\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-hexagon\"\u003e\u003cpath d=\"M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z\"\u003e\u003c/path\u003e\u003c/svg\u003e","logoWidth":18,"message":"artifact-hub","schemaVersion":1,"style":"flat"}`), data)
hw.rm.AssertExpectations(t)
})
}
@ -763,15 +764,21 @@ func TestUpdate(t *testing.T) {
}
type handlersWrapper struct {
rm *repo.ManagerMock
h *Handlers
cfg *viper.Viper
rm *repo.ManagerMock
h *Handlers
}
func newHandlersWrapper() *handlersWrapper {
cfg := viper.New()
cfg.Set("theme.colors.primary", "#417598")
cfg.Set("theme.colors.secondary", "#2D4857")
cfg.Set("theme.siteName", "Artifact Hub")
rm := &repo.ManagerMock{}
return &handlersWrapper{
rm: rm,
h: NewHandlers(rm),
cfg: cfg,
rm: rm,
h: NewHandlers(cfg, rm),
}
}

View File

@ -23,6 +23,16 @@ import (
)
const (
cspPolicy = `
default-src 'none';
connect-src 'self' https://play.openpolicyagent.org https://www.google-analytics.com https://kubernetesjsonschema.dev;
font-src 'self';
img-src 'self' data: https:;
manifest-src 'self';
script-src 'self' https://www.google-analytics.com;
style-src 'self' 'unsafe-inline'
`
indexCacheMaxAge = 5 * time.Minute
// DocsCacheMaxAge is the cache max age used when serving the docs.
@ -113,6 +123,50 @@ func (h *Handlers) Image(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write(data)
}
// Index is an http handler that serves the index.html file.
func (h *Handlers) Index(w http.ResponseWriter, r *http.Request) {
// Set headers
w.Header().Set("Cache-Control", helpers.BuildCacheControlHeader(indexCacheMaxAge))
w.Header().Set("Content-Security-Policy", cspPolicy)
// Execute index template
title, _ := r.Context().Value(hub.IndexMetaTitleKey).(string)
if title == "" {
title = h.cfg.GetString("theme.siteName")
}
description, _ := r.Context().Value(hub.IndexMetaDescriptionKey).(string)
if description == "" {
description = "Find, install and publish Kubernetes packages"
}
openGraphImage := h.cfg.GetString("theme.images.openGraphImage")
if !strings.HasPrefix(openGraphImage, "http") {
openGraphImage = h.cfg.GetString("server.baseURL") + openGraphImage
}
data := map[string]interface{}{
"allowPrivateRepositories": h.cfg.GetBool("server.allowPrivateRepositories"),
"appleTouchIcon192": h.cfg.GetString("theme.images.appleTouchIcon192"),
"appleTouchIcon512": h.cfg.GetString("theme.images.appleTouchIcon512"),
"description": description,
"gaTrackingID": h.cfg.GetString("analytics.gaTrackingID"),
"githubAuth": h.cfg.IsSet("server.oauth.github"),
"googleAuth": h.cfg.IsSet("server.oauth.google"),
"motd": h.cfg.GetString("server.motd"),
"motdSeverity": h.cfg.GetString("server.motdSeverity"),
"oidcAuth": h.cfg.IsSet("server.oauth.oidc"),
"openGraphImage": openGraphImage,
"primaryColor": h.cfg.GetString("theme.colors.primary"),
"secondaryColor": h.cfg.GetString("theme.colors.secondary"),
"shortcutIcon": h.cfg.GetString("theme.images.shortcutIcon"),
"siteName": h.cfg.GetString("theme.siteName"),
"title": title,
"websiteLogo": h.cfg.GetString("theme.images.websiteLogo"),
}
if err := h.indexTmpl.Execute(w, data); err != nil {
h.logger.Error().Err(err).Msg("error executing index template")
http.Error(w, "", http.StatusInternalServerError)
}
}
// SaveImage is an http handler that stores the provided image returning its id.
func (h *Handlers) SaveImage(w http.ResponseWriter, r *http.Request) {
data, err := ioutil.ReadAll(r.Body)
@ -131,45 +185,6 @@ func (h *Handlers) SaveImage(w http.ResponseWriter, r *http.Request) {
helpers.RenderJSON(w, dataJSON, 0, http.StatusOK)
}
// ServeIndex is an http handler that serves the index.html file.
func (h *Handlers) ServeIndex(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", helpers.BuildCacheControlHeader(indexCacheMaxAge))
w.Header().Set("Content-Security-Policy", `
default-src 'none';
connect-src 'self' https://play.openpolicyagent.org https://www.google-analytics.com https://kubernetesjsonschema.dev;
font-src 'self';
img-src 'self' data: https:;
manifest-src 'self';
script-src 'self' https://www.google-analytics.com;
style-src 'self' 'unsafe-inline'
`)
// Execute index template
title, _ := r.Context().Value(hub.IndexMetaTitleKey).(string)
if title == "" {
title = "Artifact Hub"
}
description, _ := r.Context().Value(hub.IndexMetaDescriptionKey).(string)
if description == "" {
description = "Find, install and publish Kubernetes packages"
}
data := map[string]interface{}{
"baseURL": h.cfg.GetString("server.baseURL"),
"title": title,
"description": description,
"gaTrackingID": h.cfg.GetString("analytics.gaTrackingID"),
"allowPrivateRepositories": h.cfg.GetBool("server.allowPrivateRepositories"),
"githubAuth": h.cfg.IsSet("server.oauth.github"),
"googleAuth": h.cfg.IsSet("server.oauth.google"),
"oidcAuth": h.cfg.IsSet("server.oauth.oidc"),
"motd": h.cfg.GetString("server.motd"),
"motdSeverity": h.cfg.GetString("server.motdSeverity"),
}
if err := h.indexTmpl.Execute(w, data); err != nil {
h.logger.Error().Err(err).Msg("Error executing index template")
}
}
// FileServer sets up a http.FileServer handler to serve static files.
func FileServer(r chi.Router, public, static string, cacheMaxAge time.Duration) {
if strings.ContainsAny(public, "{}*") {

View File

@ -104,6 +104,23 @@ func TestImage(t *testing.T) {
})
}
func TestIndex(t *testing.T) {
t.Parallel()
w := httptest.NewRecorder()
r, _ := http.NewRequest("GET", "/", nil)
hw := newHandlersWrapper()
hw.h.Index(w, r)
resp := w.Result()
defer resp.Body.Close()
h := resp.Header
data, _ := ioutil.ReadAll(resp.Body)
assert.Equal(t, http.StatusOK, resp.StatusCode)
assert.Equal(t, helpers.BuildCacheControlHeader(indexCacheMaxAge), h.Get("Cache-Control"))
assert.Equal(t, []byte("title:Artifact Hub\ndescription:Find, install and publish Kubernetes packages\ngaTrackingID:1234\n"), data)
}
func TestSaveImage(t *testing.T) {
fakeSaveImageError := errors.New("fake save image error")
@ -143,23 +160,6 @@ func TestSaveImage(t *testing.T) {
})
}
func TestServeIndex(t *testing.T) {
t.Parallel()
w := httptest.NewRecorder()
r, _ := http.NewRequest("GET", "/", nil)
hw := newHandlersWrapper()
hw.h.ServeIndex(w, r)
resp := w.Result()
defer resp.Body.Close()
h := resp.Header
data, _ := ioutil.ReadAll(resp.Body)
assert.Equal(t, http.StatusOK, resp.StatusCode)
assert.Equal(t, helpers.BuildCacheControlHeader(indexCacheMaxAge), h.Get("Cache-Control"))
assert.Equal(t, []byte("title:Artifact Hub\ndescription:Find, install and publish Kubernetes packages\ngaTrackingID:1234\n"), data)
}
func TestServeStaticFile(t *testing.T) {
hw := newHandlersWrapper()
r := chi.NewRouter()
@ -201,6 +201,7 @@ func newHandlersWrapper() *handlersWrapper {
cfg := viper.New()
cfg.Set("server.webBuildPath", "testdata")
cfg.Set("analytics.gaTrackingID", "1234")
cfg.Set("theme.siteName", "Artifact Hub")
is := &img.StoreMock{}
return &handlersWrapper{

View File

@ -171,7 +171,7 @@ func (h *Handlers) ApproveSession(w http.ResponseWriter, r *http.Request) {
func (h *Handlers) BasicAuth(next http.Handler) http.Handler {
validUser := []byte(h.cfg.GetString("server.basicAuth.username"))
validPass := []byte(h.cfg.GetString("server.basicAuth.password"))
realm := "Artifact Hub"
realm := h.cfg.GetString("theme.siteName")
areCredentialsValid := func(user, pass []byte) bool {
if subtle.ConstantTimeCompare(user, validUser) != 1 {

View File

@ -34,6 +34,7 @@ type PackageNotificationTemplateData struct {
BaseURL string `json:"base_url"`
Event map[string]interface{} `json:"event"`
Package map[string]interface{} `json:"package"`
Theme map[string]string `json:"theme"`
}
// RepositoryNotificationTemplateData represents some details of a notification
@ -42,4 +43,5 @@ type RepositoryNotificationTemplateData struct {
BaseURL string `json:"base_url"`
Event map[string]interface{} `json:"event"`
Repository map[string]interface{} `json:"repository"`
Theme map[string]string `json:"theme"`
}

View File

@ -50,6 +50,7 @@ var (
// Services is a wrapper around several internal services used to handle
// notifications deliveries.
type Services struct {
Cfg *viper.Viper
DB hub.DB
ES hub.EmailSender
NotificationManager hub.NotificationManager
@ -66,7 +67,7 @@ type Dispatcher struct {
}
// NewDispatcher creates a new Dispatcher instance.
func NewDispatcher(cfg *viper.Viper, svc *Services, opts ...func(d *Dispatcher)) *Dispatcher {
func NewDispatcher(svc *Services, opts ...func(d *Dispatcher)) *Dispatcher {
// Setup dispatcher
d := &Dispatcher{
numWorkers: defaultNumWorkers,
@ -86,7 +87,7 @@ func NewDispatcher(cfg *viper.Viper, svc *Services, opts ...func(d *Dispatcher))
// Setup and launch workers
c := cache.New(cacheDefaultExpiration, cacheCleanupInterval)
baseURL := cfg.GetString("server.baseURL")
baseURL := svc.Cfg.GetString("server.baseURL")
d.workers = make([]*Worker, 0, d.numWorkers)
for i := 0; i < d.numWorkers; i++ {
d.workers = append(d.workers, NewWorker(svc, c, baseURL, tmpl))

View File

@ -16,7 +16,7 @@ func TestDispatcher(t *testing.T) {
// Setup dispatcher
cfg := viper.New()
cfg.Set("server.baseURL", "http://localhost:8000")
d := NewDispatcher(cfg, &Services{}, WithNumWorkers(0))
d := NewDispatcher(&Services{Cfg: cfg}, WithNumWorkers(0))
// Run it
ctx, stopDispatcher := context.WithCancel(context.Background())

View File

@ -121,12 +121,12 @@
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
<tr>
<td class="content-block powered-by" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; font-size: 10px; text-align: center;">
<p class="text-muted" style="font-size: 10px; text-align: center; text-decoration: none;">Didn't subscribe to Artifact Hub notifications for {{ .Package.Name }} package? You can unsubscribe <a href="{{ .BaseURL }}/control-panel/settings/subscriptions" target="_blank" class="text-muted" style="text-decoration: underline;">here</a>.</p>
<p class="text-muted" style="font-size: 10px; text-align: center; text-decoration: none;">Didn't subscribe to {{ .Theme.SiteName }} notifications for {{ .Package.Name }} package? You can unsubscribe <a href="{{ .BaseURL }}/control-panel/settings/subscriptions" target="_blank" class="text-muted" style="text-decoration: underline;">here</a>.</p>
</td>
</tr>
<tr>
<td class="content-block powered-by" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; font-size: 12px; text-align: center;">
<a href="{{ .BaseURL }}" class="AHlink" style="font-size: 12px; text-align: center; text-decoration: none;">© Artifact Hub</a>
<a href="{{ .BaseURL }}" class="AHlink" style="font-size: 12px; text-align: center; text-decoration: none;">© {{ .Theme.SiteName }}</a>
</td>
</tr>
</table>

View File

@ -27,7 +27,7 @@
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
<tr>
<td class="content-block powered-by" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; font-size: 12px; text-align: center;">
<a href="{{ .BaseURL }}" class="AHlink" style="font-size: 12px; text-align: center; text-decoration: none;">© Artifact Hub</a>
<a href="{{ .BaseURL }}" class="AHlink" style="font-size: 12px; text-align: center; text-decoration: none;">© {{ .Theme.SiteName }}</a>
</td>
</tr>
</table>

View File

@ -13,9 +13,6 @@
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">
We encountered some errors while scanning the packages in repository <strong>{{ .Repository.Name }}</strong> for security vulnerabilities.
</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 30px;">
If you find something in them that doesn't make sense, or there is anything you need help with, please file an issue <a href="https://github.com/artifacthub/hub/issues" class="AHlink" style="text-decoration: none;">here</a>.
</p>
<h4 style="color: #921e12; font-family: sans-serif; margin: 0; Margin-bottom: 15px;">Errors log</h4>
<table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="Margin-bottom: 30px; border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; box-sizing: border-box; background: #1D1F21; border-radius: 3px;">
<tbody>
@ -70,7 +67,7 @@
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
<tr>
<td class="content-block powered-by" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; font-size: 12px; text-align: center;">
<a href="{{ .BaseURL }}" class="AHlink" style="font-size: 12px; text-align: center; text-decoration: none;">© Artifact Hub</a>
<a href="{{ .BaseURL }}" class="AHlink" style="font-size: 12px; text-align: center; text-decoration: none;">© {{ .Theme.SiteName }}</a>
</td>
</tr>
</table>

View File

@ -16,7 +16,7 @@
<h4 class="subtitle" style="font-family: sans-serif; margin: 0; Margin-bottom: 15px;">{{ .Package.repository.publisher }} </h4>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 30px; text-align: left;">
We found one or more potential security vulnerabilities in the images of the <b>{{ .Package.Name }}</b> package version <b>{{ .Package.Version }}</b>. For more information, please see the package's security report in Artifact Hub.
We found one or more potential security vulnerabilities in the images of the <b>{{ .Package.Name }}</b> package version <b>{{ .Package.Version }}</b>. For more information, please see the package's security report in {{ .Theme.SiteName }}.
</p>
</td>
</tr>
@ -46,7 +46,7 @@
<p class="text-muted" style="font-size: 11px; text-decoration: none; Margin-bottom: 30px;">Or you can copy-paste this link: <span class="copy-link">{{ .Package.URL }}?modal=security-report</span></p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 30px; text-align: left;">
Please note that security alerts only consider vulnerabilities of <b>high</b> and <b>critical</b> severity. Any time a new potential security vulnerability is detected youll be notified again.
Please note that security alerts only consider vulnerabilities of <b>high</b> and <b>critical</b> severity. Any time a new potential security vulnerability is detected you'll be notified again.
</p>
</td>
</tr>
@ -66,12 +66,12 @@
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
<tr>
<td class="content-block powered-by" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; font-size: 10px; text-align: center;">
<p class="text-muted" style="font-size: 10px; text-align: center; text-decoration: none;">Didn't subscribe to Artifact Hub notifications for {{ .Package.Name }} package? You can unsubscribe <a href="{{ .BaseURL }}/control-panel/settings/subscriptions" target="_blank" class="text-muted" style="text-decoration: underline;">here</a>.</p>
<p class="text-muted" style="font-size: 10px; text-align: center; text-decoration: none;">Didn't subscribe to {{ .Theme.SiteName }} notifications for {{ .Package.Name }} package? You can unsubscribe <a href="{{ .BaseURL }}/control-panel/settings/subscriptions" target="_blank" class="text-muted" style="text-decoration: underline;">here</a>.</p>
</td>
</tr>
<tr>
<td class="content-block powered-by" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; font-size: 12px; text-align: center;">
<a href="{{ .BaseURL }}" class="AHlink" style="font-size: 12px; text-align: center; text-decoration: none;">© Artifact Hub</a>
<a href="{{ .BaseURL }}" class="AHlink" style="font-size: 12px; text-align: center; text-decoration: none;">© {{ .Theme.SiteName }}</a>
</td>
</tr>
</table>

View File

@ -16,11 +16,7 @@
</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">
Some or all of these errors may be just warnings, and it's possible that your packages have been still indexed properly. However, it'd be great if you can take a look at them just in case there is something missing or failing in your repository that may affect how your content is displayed on Artifact Hub.
</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 30px;">
If you find something in them that doesn't make sense, or there is anything you need help with, please file an issue <a href="https://github.com/artifacthub/hub/issues" style="color: #2d4857; text-decoration: none;">here</a>.
Some or all of these errors may be just warnings, and it's possible that your packages have been still indexed properly. However, it'd be great if you can take a look at them just in case there is something missing or failing in your repository that may affect how your content is displayed on {{ .Theme.SiteName }}.
</p>
<h4 style="color: #921e12; font-family: sans-serif; margin: 0; Margin-bottom: 15px;">Errors log</h4>
@ -50,7 +46,7 @@
<table border="0" cellpadding="0" cellspacing="0" style="width: 100%; border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt;">
<tbody>
<tr>
<td style="font-family: sans-serif; font-size: 14px; border-radius: 5px; vertical-align: top;"><div style="text-align: center;"> <a href="{{ .BaseURL }}/control-panel/repositories?modal=tracking&user-alias={{ .Repository.UserAlias }}&org-name={{ .Repository.OrganizationName }}&repo-name={{ .Repository.Name }}" class="AHbtn" target="_blank" style="display: inline-block; border-radius: 5px; box-sizing: border-box; cursor: pointer; text-decoration: none; font-size: 14px; font-weight: bold; margin: 0; padding: 12px 25px;">View in Artifact Hub</a> </div></td>
<td style="font-family: sans-serif; font-size: 14px; border-radius: 5px; vertical-align: top;"><div style="text-align: center;"> <a href="{{ .BaseURL }}/control-panel/repositories?modal=tracking&user-alias={{ .Repository.UserAlias }}&org-name={{ .Repository.OrganizationName }}&repo-name={{ .Repository.Name }}" class="AHbtn" target="_blank" style="display: inline-block; border-radius: 5px; box-sizing: border-box; cursor: pointer; text-decoration: none; font-size: 14px; font-weight: bold; margin: 0; padding: 12px 25px;">View in {{ .Theme.SiteName }}</a> </div></td>
</tr>
</tbody>
</table>
@ -82,7 +78,7 @@
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
<tr>
<td class="content-block powered-by" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; font-size: 12px; text-align: center;">
<a href="{{ .BaseURL }}" class="AHlink" style="font-size: 12px; text-align: center; text-decoration: none;">© Artifact Hub</a>
<a href="{{ .BaseURL }}" class="AHlink" style="font-size: 12px; text-align: center; text-decoration: none;">© {{ .Theme.SiteName }}</a>
</td>
</tr>
</table>

View File

@ -308,6 +308,11 @@ func (w *Worker) preparePkgNotificationTemplateData(
"Publisher": publisher,
},
},
Theme: map[string]string{
"PrimaryColor": w.svc.Cfg.GetString("theme.colors.primary"),
"SecondaryColor": w.svc.Cfg.GetString("theme.colors.secondary"),
"SiteName": w.svc.Cfg.GetString("theme.siteName"),
},
}, nil
}
@ -357,6 +362,11 @@ func (w *Worker) prepareRepoNotificationTemplateData(
"LastScanningErrors": strings.Split(r.LastScanningErrors, "\n"),
"LastTrackingErrors": strings.Split(r.LastTrackingErrors, "\n"),
},
Theme: map[string]string{
"PrimaryColor": w.svc.Cfg.GetString("theme.colors.primary"),
"SecondaryColor": w.svc.Cfg.GetString("theme.colors.secondary"),
"SiteName": w.svc.Cfg.GetString("theme.siteName"),
},
}, nil
}

View File

@ -18,6 +18,7 @@ import (
"github.com/artifacthub/hub/internal/subscription"
"github.com/artifacthub/hub/internal/tests"
"github.com/patrickmn/go-cache"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
@ -349,6 +350,7 @@ type servicesWrapper struct {
ctx context.Context
stopWorker context.CancelFunc
wg *sync.WaitGroup
cfg *viper.Viper
db *tests.DBMock
tx *tests.TXMock
es *email.SenderMock
@ -367,6 +369,7 @@ func newServicesWrapper() *servicesWrapper {
var wg sync.WaitGroup
wg.Add(1)
cfg := viper.New()
db := &tests.DBMock{}
tx := &tests.TXMock{}
es := &email.SenderMock{}
@ -381,6 +384,7 @@ func newServicesWrapper() *servicesWrapper {
ctx: ctx,
stopWorker: stopWorker,
wg: &wg,
cfg: cfg,
db: db,
tx: tx,
es: es,
@ -391,6 +395,7 @@ func newServicesWrapper() *servicesWrapper {
cache: cache,
hc: hc,
svc: &Services{
Cfg: cfg,
DB: db,
ES: es,
NotificationManager: nm,

View File

@ -18,6 +18,7 @@ import (
"github.com/artifacthub/hub/internal/util"
"github.com/open-policy-agent/opa/ast"
"github.com/satori/uuid"
"github.com/spf13/viper"
)
const (
@ -52,6 +53,7 @@ var organizationNameRE = regexp.MustCompile(`^[a-z0-9-]+$`)
// Manager provides an API to manage organizations.
type Manager struct {
cfg *viper.Viper
db hub.DB
es hub.EmailSender
az hub.Authorizer
@ -59,11 +61,12 @@ type Manager struct {
}
// NewManager creates a new Manager instance.
func NewManager(db hub.DB, es hub.EmailSender, az hub.Authorizer) *Manager {
func NewManager(cfg *viper.Viper, db hub.DB, es hub.EmailSender, az hub.Authorizer) *Manager {
return &Manager{
db: db,
es: es,
az: az,
cfg: cfg,
db: db,
es: es,
az: az,
tmpl: map[templateID]*template.Template{
invitationEmail: template.Must(template.New("").Parse(email.BaseTmpl + invitationEmailTmpl)),
},
@ -131,9 +134,14 @@ func (m *Manager) AddMember(ctx context.Context, orgName, userAlias, baseURL str
if err := m.db.QueryRow(ctx, getUserEmailDBQ, userAlias).Scan(&userEmail); err != nil {
return err
}
templateData := map[string]string{
"link": fmt.Sprintf("%s/accept-invitation?org=%s", baseURL, orgName),
"orgName": orgName,
templateData := map[string]interface{}{
"Link": fmt.Sprintf("%s/accept-invitation?org=%s", baseURL, orgName),
"OrgName": orgName,
"Theme": map[string]string{
"PrimaryColor": m.cfg.GetString("theme.colors.primary"),
"SecondaryColor": m.cfg.GetString("theme.colors.secondary"),
"SiteName": m.cfg.GetString("theme.siteName"),
},
}
var emailBody bytes.Buffer
if err := m.tmpl[invitationEmail].Execute(&emailBody, templateData); err != nil {

View File

@ -11,16 +11,19 @@ import (
"github.com/artifacthub/hub/internal/hub"
"github.com/artifacthub/hub/internal/tests"
"github.com/artifacthub/hub/internal/util"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
var cfg = viper.New()
func TestAdd(t *testing.T) {
ctx := context.WithValue(context.Background(), hub.UserIDKey, "userID")
t.Run("user id not found in ctx", func(t *testing.T) {
t.Parallel()
m := NewManager(nil, nil, nil)
m := NewManager(cfg, nil, nil, nil)
assert.Panics(t, func() {
_ = m.Add(context.Background(), &hub.Organization{})
})
@ -61,7 +64,7 @@ func TestAdd(t *testing.T) {
tc := tc
t.Run(tc.errMsg, func(t *testing.T) {
t.Parallel()
m := NewManager(nil, nil, nil)
m := NewManager(cfg, nil, nil, nil)
err := m.Add(ctx, tc.org)
assert.True(t, errors.Is(err, hub.ErrInvalidInput))
assert.Contains(t, err.Error(), tc.errMsg)
@ -73,7 +76,7 @@ func TestAdd(t *testing.T) {
t.Parallel()
db := &tests.DBMock{}
db.On("Exec", ctx, addOrgDBQ, "userID", mock.Anything).Return(nil)
m := NewManager(db, nil, nil)
m := NewManager(cfg, db, nil, nil)
err := m.Add(ctx, &hub.Organization{Name: "org1"})
assert.NoError(t, err)
@ -84,7 +87,7 @@ func TestAdd(t *testing.T) {
t.Parallel()
db := &tests.DBMock{}
db.On("Exec", ctx, addOrgDBQ, "userID", mock.Anything).Return(tests.ErrFakeDB)
m := NewManager(db, nil, nil)
m := NewManager(cfg, db, nil, nil)
err := m.Add(ctx, &hub.Organization{Name: "org1"})
assert.Equal(t, tests.ErrFakeDB, err)
@ -97,7 +100,7 @@ func TestAddMember(t *testing.T) {
t.Run("user id not found in ctx", func(t *testing.T) {
t.Parallel()
m := NewManager(nil, nil, nil)
m := NewManager(cfg, nil, nil, nil)
assert.Panics(t, func() {
_ = m.AddMember(context.Background(), "orgName", "userAlias", "")
})
@ -139,7 +142,7 @@ func TestAddMember(t *testing.T) {
tc := tc
t.Run(tc.errMsg, func(t *testing.T) {
t.Parallel()
m := NewManager(nil, nil, nil)
m := NewManager(cfg, nil, nil, nil)
err := m.AddMember(ctx, tc.orgName, tc.userAlias, tc.baseURL)
assert.True(t, errors.Is(err, hub.ErrInvalidInput))
assert.Contains(t, err.Error(), tc.errMsg)
@ -155,7 +158,7 @@ func TestAddMember(t *testing.T) {
UserID: "userID",
Action: hub.AddOrganizationMember,
}).Return(tests.ErrFake)
m := NewManager(nil, nil, az)
m := NewManager(cfg, nil, nil, az)
err := m.AddMember(ctx, "orgName", "userAlias", "http://baseurl.com")
assert.Equal(t, tests.ErrFake, err)
@ -191,7 +194,7 @@ func TestAddMember(t *testing.T) {
UserID: "userID",
Action: hub.AddOrganizationMember,
}).Return(nil)
m := NewManager(db, es, az)
m := NewManager(cfg, db, es, az)
err := m.AddMember(ctx, "orgName", "userAlias", "http://baseurl.com")
assert.Equal(t, tc.emailSenderResponse, err)
@ -228,7 +231,7 @@ func TestAddMember(t *testing.T) {
UserID: "userID",
Action: hub.AddOrganizationMember,
}).Return(nil)
m := NewManager(db, nil, az)
m := NewManager(cfg, db, nil, az)
err := m.AddMember(ctx, "orgName", "userAlias", "http://baseurl.com")
assert.Equal(t, tc.expectedError, err)
@ -263,7 +266,7 @@ func TestCheckAvailability(t *testing.T) {
tc := tc
t.Run(tc.errMsg, func(t *testing.T) {
t.Parallel()
m := NewManager(nil, nil, nil)
m := NewManager(cfg, nil, nil, nil)
_, err := m.CheckAvailability(context.Background(), tc.resourceKind, tc.value)
assert.True(t, errors.Is(err, hub.ErrInvalidInput))
assert.Contains(t, err.Error(), tc.errMsg)
@ -290,7 +293,7 @@ func TestCheckAvailability(t *testing.T) {
tc.dbQuery = fmt.Sprintf("select not exists (%s)", tc.dbQuery)
db := &tests.DBMock{}
db.On("QueryRow", ctx, tc.dbQuery, "value").Return(tc.available, nil)
m := NewManager(db, nil, nil)
m := NewManager(cfg, db, nil, nil)
available, err := m.CheckAvailability(ctx, tc.resourceKind, "value")
assert.NoError(t, err)
@ -305,7 +308,7 @@ func TestCheckAvailability(t *testing.T) {
db := &tests.DBMock{}
dbQuery := fmt.Sprintf(`select not exists (%s)`, checkOrgNameAvailDBQ)
db.On("QueryRow", ctx, dbQuery, "value").Return(false, tests.ErrFakeDB)
m := NewManager(db, nil, nil)
m := NewManager(cfg, db, nil, nil)
available, err := m.CheckAvailability(context.Background(), "organizationName", "value")
assert.Equal(t, tests.ErrFakeDB, err)
@ -319,7 +322,7 @@ func TestConfirmMembership(t *testing.T) {
t.Run("user id not found in ctx", func(t *testing.T) {
t.Parallel()
m := NewManager(nil, nil, nil)
m := NewManager(cfg, nil, nil, nil)
assert.Panics(t, func() {
_ = m.ConfirmMembership(context.Background(), "orgName")
})
@ -327,7 +330,7 @@ func TestConfirmMembership(t *testing.T) {
t.Run("invalid input", func(t *testing.T) {
t.Parallel()
m := NewManager(nil, nil, nil)
m := NewManager(cfg, nil, nil, nil)
err := m.ConfirmMembership(ctx, "")
assert.True(t, errors.Is(err, hub.ErrInvalidInput))
})
@ -336,7 +339,7 @@ func TestConfirmMembership(t *testing.T) {
t.Parallel()
db := &tests.DBMock{}
db.On("Exec", ctx, confirmMembershipDBQ, "userID", "orgName").Return(nil)
m := NewManager(db, nil, nil)
m := NewManager(cfg, db, nil, nil)
err := m.ConfirmMembership(ctx, "orgName")
assert.NoError(t, err)
@ -347,7 +350,7 @@ func TestConfirmMembership(t *testing.T) {
t.Parallel()
db := &tests.DBMock{}
db.On("Exec", ctx, confirmMembershipDBQ, "userID", "orgName").Return(tests.ErrFakeDB)
m := NewManager(db, nil, nil)
m := NewManager(cfg, db, nil, nil)
err := m.ConfirmMembership(ctx, "orgName")
assert.Equal(t, tests.ErrFakeDB, err)
@ -360,7 +363,7 @@ func TestDelete(t *testing.T) {
t.Run("user id not found in ctx", func(t *testing.T) {
t.Parallel()
m := NewManager(nil, nil, nil)
m := NewManager(cfg, nil, nil, nil)
assert.Panics(t, func() {
_ = m.Add(context.Background(), &hub.Organization{})
})
@ -368,7 +371,7 @@ func TestDelete(t *testing.T) {
t.Run("invalid input", func(t *testing.T) {
t.Parallel()
m := NewManager(nil, nil, nil)
m := NewManager(cfg, nil, nil, nil)
err := m.Delete(ctx, "")
assert.True(t, errors.Is(err, hub.ErrInvalidInput))
assert.Contains(t, err.Error(), "name not provided")
@ -383,7 +386,7 @@ func TestDelete(t *testing.T) {
UserID: "userID",
Action: hub.DeleteOrganization,
}).Return(tests.ErrFake)
m := NewManager(db, nil, az)
m := NewManager(cfg, db, nil, az)
err := m.Delete(ctx, "org1")
assert.Equal(t, tests.ErrFake, err)
@ -400,7 +403,7 @@ func TestDelete(t *testing.T) {
UserID: "userID",
Action: hub.DeleteOrganization,
}).Return(nil)
m := NewManager(db, nil, az)
m := NewManager(cfg, db, nil, az)
err := m.Delete(ctx, "org1")
assert.NoError(t, err)
@ -417,7 +420,7 @@ func TestDelete(t *testing.T) {
UserID: "userID",
Action: hub.DeleteOrganization,
}).Return(nil)
m := NewManager(db, nil, az)
m := NewManager(cfg, db, nil, az)
err := m.Delete(ctx, "org1")
assert.Equal(t, tests.ErrFakeDB, err)
@ -430,7 +433,7 @@ func TestDeleteMember(t *testing.T) {
t.Run("user id not found in ctx", func(t *testing.T) {
t.Parallel()
m := NewManager(nil, nil, nil)
m := NewManager(cfg, nil, nil, nil)
assert.Panics(t, func() {
_ = m.DeleteMember(context.Background(), "orgName", "userAlias")
})
@ -457,7 +460,7 @@ func TestDeleteMember(t *testing.T) {
tc := tc
t.Run(tc.errMsg, func(t *testing.T) {
t.Parallel()
m := NewManager(nil, nil, nil)
m := NewManager(cfg, nil, nil, nil)
err := m.DeleteMember(ctx, tc.orgName, tc.userAlias)
assert.True(t, errors.Is(err, hub.ErrInvalidInput))
assert.Contains(t, err.Error(), tc.errMsg)
@ -469,7 +472,7 @@ func TestDeleteMember(t *testing.T) {
t.Parallel()
db := &tests.DBMock{}
db.On("QueryRow", ctx, getUserAliasDBQ, "userID").Return("", tests.ErrFakeDB)
m := NewManager(db, nil, nil)
m := NewManager(cfg, db, nil, nil)
err := m.DeleteMember(ctx, "orgName", "userAlias")
assert.Error(t, err)
@ -486,7 +489,7 @@ func TestDeleteMember(t *testing.T) {
UserID: "userID",
Action: hub.DeleteOrganizationMember,
}).Return(tests.ErrFake)
m := NewManager(db, nil, az)
m := NewManager(cfg, db, nil, az)
err := m.DeleteMember(ctx, "orgName", "userAlias")
assert.Equal(t, tests.ErrFake, err)
@ -504,7 +507,7 @@ func TestDeleteMember(t *testing.T) {
UserID: "userID",
Action: hub.DeleteOrganizationMember,
}).Return(nil)
m := NewManager(db, nil, az)
m := NewManager(cfg, db, nil, az)
err := m.DeleteMember(ctx, "orgName", "userAlias")
assert.NoError(t, err)
@ -517,7 +520,7 @@ func TestDeleteMember(t *testing.T) {
db := &tests.DBMock{}
db.On("QueryRow", ctx, getUserAliasDBQ, "userID").Return("userAlias", nil)
db.On("Exec", ctx, deleteOrgMemberDBQ, "userID", "orgName", "userAlias").Return(nil)
m := NewManager(db, nil, nil)
m := NewManager(cfg, db, nil, nil)
err := m.DeleteMember(ctx, "orgName", "userAlias")
assert.NoError(t, err)
@ -551,7 +554,7 @@ func TestDeleteMember(t *testing.T) {
UserID: "userID",
Action: hub.DeleteOrganizationMember,
}).Return(nil)
m := NewManager(db, nil, az)
m := NewManager(cfg, db, nil, az)
err := m.DeleteMember(ctx, "orgName", "userAlias")
assert.Equal(t, tc.expectedError, err)
@ -567,7 +570,7 @@ func TestGetAuthorizationPolicyJSON(t *testing.T) {
t.Run("user id not found in ctx", func(t *testing.T) {
t.Parallel()
m := NewManager(nil, nil, nil)
m := NewManager(cfg, nil, nil, nil)
assert.Panics(t, func() {
_, _ = m.GetAuthorizationPolicyJSON(context.Background(), "org1")
})
@ -575,7 +578,7 @@ func TestGetAuthorizationPolicyJSON(t *testing.T) {
t.Run("invalid input", func(t *testing.T) {
t.Parallel()
m := NewManager(nil, nil, nil)
m := NewManager(cfg, nil, nil, nil)
_, err := m.GetAuthorizationPolicyJSON(ctx, "")
assert.True(t, errors.Is(err, hub.ErrInvalidInput))
})
@ -588,7 +591,7 @@ func TestGetAuthorizationPolicyJSON(t *testing.T) {
UserID: "userID",
Action: hub.GetAuthorizationPolicy,
}).Return(tests.ErrFake)
m := NewManager(nil, nil, az)
m := NewManager(cfg, nil, nil, az)
dataJSON, err := m.GetAuthorizationPolicyJSON(ctx, "org1")
assert.Equal(t, tests.ErrFake, err)
@ -606,7 +609,7 @@ func TestGetAuthorizationPolicyJSON(t *testing.T) {
UserID: "userID",
Action: hub.GetAuthorizationPolicy,
}).Return(nil)
m := NewManager(db, nil, az)
m := NewManager(cfg, db, nil, az)
dataJSON, err := m.GetAuthorizationPolicyJSON(ctx, "org1")
assert.NoError(t, err)
@ -641,7 +644,7 @@ func TestGetAuthorizationPolicyJSON(t *testing.T) {
UserID: "userID",
Action: hub.GetAuthorizationPolicy,
}).Return(nil)
m := NewManager(db, nil, az)
m := NewManager(cfg, db, nil, az)
dataJSON, err := m.GetAuthorizationPolicyJSON(ctx, "org1")
assert.Equal(t, tc.expectedError, err)
@ -658,7 +661,7 @@ func TestGetByUserJSON(t *testing.T) {
t.Run("user id not found in ctx", func(t *testing.T) {
t.Parallel()
m := NewManager(nil, nil, nil)
m := NewManager(cfg, nil, nil, nil)
assert.Panics(t, func() {
_, _ = m.GetByUserJSON(context.Background())
})
@ -668,7 +671,7 @@ func TestGetByUserJSON(t *testing.T) {
t.Parallel()
db := &tests.DBMock{}
db.On("QueryRow", ctx, getUserOrgsDBQ, "userID").Return([]byte("dataJSON"), nil)
m := NewManager(db, nil, nil)
m := NewManager(cfg, db, nil, nil)
dataJSON, err := m.GetByUserJSON(ctx)
assert.NoError(t, err)
@ -680,7 +683,7 @@ func TestGetByUserJSON(t *testing.T) {
t.Parallel()
db := &tests.DBMock{}
db.On("QueryRow", ctx, getUserOrgsDBQ, "userID").Return(nil, tests.ErrFakeDB)
m := NewManager(db, nil, nil)
m := NewManager(cfg, db, nil, nil)
dataJSON, err := m.GetByUserJSON(ctx)
assert.Equal(t, tests.ErrFakeDB, err)
@ -694,7 +697,7 @@ func TestGetJSON(t *testing.T) {
t.Run("invalid input", func(t *testing.T) {
t.Parallel()
m := NewManager(nil, nil, nil)
m := NewManager(cfg, nil, nil, nil)
_, err := m.GetJSON(context.Background(), "")
assert.True(t, errors.Is(err, hub.ErrInvalidInput))
})
@ -703,7 +706,7 @@ func TestGetJSON(t *testing.T) {
t.Parallel()
db := &tests.DBMock{}
db.On("QueryRow", ctx, getOrgDBQ, "orgName").Return([]byte("dataJSON"), nil)
m := NewManager(db, nil, nil)
m := NewManager(cfg, db, nil, nil)
dataJSON, err := m.GetJSON(ctx, "orgName")
assert.NoError(t, err)
@ -715,7 +718,7 @@ func TestGetJSON(t *testing.T) {
t.Parallel()
db := &tests.DBMock{}
db.On("QueryRow", ctx, getOrgDBQ, "orgName").Return(nil, tests.ErrFakeDB)
m := NewManager(db, nil, nil)
m := NewManager(cfg, db, nil, nil)
dataJSON, err := m.GetJSON(ctx, "orgName")
assert.Equal(t, tests.ErrFakeDB, err)
@ -729,7 +732,7 @@ func TestGetMembersJSON(t *testing.T) {
t.Run("user id not found in ctx", func(t *testing.T) {
t.Parallel()
m := NewManager(nil, nil, nil)
m := NewManager(cfg, nil, nil, nil)
assert.Panics(t, func() {
_, _ = m.GetMembersJSON(context.Background(), "orgName")
})
@ -737,7 +740,7 @@ func TestGetMembersJSON(t *testing.T) {
t.Run("invalid input", func(t *testing.T) {
t.Parallel()
m := NewManager(nil, nil, nil)
m := NewManager(cfg, nil, nil, nil)
_, err := m.GetMembersJSON(ctx, "")
assert.True(t, errors.Is(err, hub.ErrInvalidInput))
})
@ -746,7 +749,7 @@ func TestGetMembersJSON(t *testing.T) {
t.Parallel()
db := &tests.DBMock{}
db.On("QueryRow", ctx, getOrgMembersDBQ, "userID", "orgName").Return([]byte("dataJSON"), nil)
m := NewManager(db, nil, nil)
m := NewManager(cfg, db, nil, nil)
dataJSON, err := m.GetMembersJSON(ctx, "orgName")
assert.NoError(t, err)
@ -774,7 +777,7 @@ func TestGetMembersJSON(t *testing.T) {
t.Parallel()
db := &tests.DBMock{}
db.On("QueryRow", ctx, getOrgMembersDBQ, "userID", "orgName").Return(nil, tc.dbErr)
m := NewManager(db, nil, nil)
m := NewManager(cfg, db, nil, nil)
dataJSON, err := m.GetMembersJSON(ctx, "orgName")
assert.Equal(t, tc.expectedError, err)
@ -790,7 +793,7 @@ func TestUpdate(t *testing.T) {
t.Run("user id not found in ctx", func(t *testing.T) {
t.Parallel()
m := NewManager(nil, nil, nil)
m := NewManager(cfg, nil, nil, nil)
assert.Panics(t, func() {
_ = m.Update(context.Background(), "org1", &hub.Organization{})
})
@ -831,7 +834,7 @@ func TestUpdate(t *testing.T) {
tc := tc
t.Run(tc.errMsg, func(t *testing.T) {
t.Parallel()
m := NewManager(nil, nil, nil)
m := NewManager(cfg, nil, nil, nil)
err := m.Update(ctx, "org1", tc.org)
assert.True(t, errors.Is(err, hub.ErrInvalidInput))
assert.Contains(t, err.Error(), tc.errMsg)
@ -847,7 +850,7 @@ func TestUpdate(t *testing.T) {
UserID: "userID",
Action: hub.UpdateOrganization,
}).Return(tests.ErrFake)
m := NewManager(nil, nil, az)
m := NewManager(cfg, nil, nil, az)
err := m.Update(ctx, "org1", &hub.Organization{
Name: "org1",
@ -867,7 +870,7 @@ func TestUpdate(t *testing.T) {
UserID: "userID",
Action: hub.UpdateOrganization,
}).Return(nil)
m := NewManager(db, nil, az)
m := NewManager(cfg, db, nil, az)
err := m.Update(ctx, "org1", &hub.Organization{
Name: "org1",
@ -904,7 +907,7 @@ func TestUpdate(t *testing.T) {
UserID: "userID",
Action: hub.UpdateOrganization,
}).Return(nil)
m := NewManager(db, nil, az)
m := NewManager(cfg, db, nil, az)
err := m.Update(ctx, "org1", &hub.Organization{
Name: "org1",
@ -928,7 +931,7 @@ func TestUpdateAuthorizationPolicy(t *testing.T) {
t.Run("user id not found in ctx", func(t *testing.T) {
t.Parallel()
m := NewManager(nil, nil, nil)
m := NewManager(cfg, nil, nil, nil)
assert.Panics(t, func() {
_ = m.UpdateAuthorizationPolicy(context.Background(), "org1", &hub.AuthorizationPolicy{})
})
@ -1032,7 +1035,7 @@ func TestUpdateAuthorizationPolicy(t *testing.T) {
t.Parallel()
az := &authz.AuthorizerMock{}
az.On("WillUserBeLockedOut", ctx, tc.policy, "userID").Return(true, nil).Maybe()
m := NewManager(nil, nil, az)
m := NewManager(cfg, nil, nil, az)
err := m.UpdateAuthorizationPolicy(ctx, tc.orgName, tc.policy)
assert.True(t, errors.Is(err, hub.ErrInvalidInput))
assert.Contains(t, err.Error(), tc.errMsg)
@ -1049,7 +1052,7 @@ func TestUpdateAuthorizationPolicy(t *testing.T) {
UserID: "userID",
Action: hub.UpdateAuthorizationPolicy,
}).Return(tests.ErrFake)
m := NewManager(nil, nil, az)
m := NewManager(cfg, nil, nil, az)
err := m.UpdateAuthorizationPolicy(ctx, "org1", validPolicy)
assert.Equal(t, tests.ErrFake, err)
@ -1067,7 +1070,7 @@ func TestUpdateAuthorizationPolicy(t *testing.T) {
UserID: "userID",
Action: hub.UpdateAuthorizationPolicy,
}).Return(nil)
m := NewManager(db, nil, az)
m := NewManager(cfg, db, nil, az)
err := m.UpdateAuthorizationPolicy(ctx, "org1", validPolicy)
assert.NoError(t, err)
@ -1102,7 +1105,7 @@ func TestUpdateAuthorizationPolicy(t *testing.T) {
UserID: "userID",
Action: hub.UpdateAuthorizationPolicy,
}).Return(nil)
m := NewManager(db, nil, az)
m := NewManager(cfg, db, nil, az)
err := m.UpdateAuthorizationPolicy(ctx, "org1", validPolicy)
assert.Equal(t, tc.expectedError, err)

View File

@ -2,7 +2,7 @@
{{ define "content" }}
<div class="content" style="box-sizing: border-box; display: block; Margin: 0 auto; max-width: 580px; padding: 10px;">
<!-- START CENTERED WHITE CONTAINER -->
<span class="preheader" style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">Invitation to {{ .orgName }} organization on Artifact Hub</span>
<span class="preheader" style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">Invitation to {{ .OrgName }} organization on {{ .Theme.SiteName }}</span>
<table class="main line" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; border-radius: 3px;">
<!-- START MAIN CONTENT AREA -->
@ -12,7 +12,7 @@
<tr>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;">
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Hi!</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 30px;">You have been invited to join <b>{{ .orgName }}</b> organization on Artifact Hub.</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 30px;">You have been invited to join <b>{{ .OrgName }}</b> organization on {{ .Theme.SiteName }}.</p>
<table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; box-sizing: border-box;">
<tbody>
<tr>
@ -20,7 +20,7 @@
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;">
<tbody>
<tr>
<td style="font-family: sans-serif; font-size: 14px; border-radius: 5px; vertical-align: top; text-align: center;"> <a href="{{ .link }}" class="AHbtn" target="_blank" style="display: inline-block; border-radius: 5px; box-sizing: border-box; cursor: pointer; text-decoration: none; font-size: 14px; font-weight: bold; margin: 0; padding: 12px 25px; text-transform: capitalize;">Accept invitation</a> </td>
<td style="font-family: sans-serif; font-size: 14px; border-radius: 5px; vertical-align: top; text-align: center;"> <a href="{{ .Link }}" class="AHbtn" target="_blank" style="display: inline-block; border-radius: 5px; box-sizing: border-box; cursor: pointer; text-decoration: none; font-size: 14px; font-weight: bold; margin: 0; padding: 12px 25px; text-transform: capitalize;">Accept invitation</a> </td>
</tr>
</tbody>
</table>
@ -32,7 +32,7 @@
<tbody>
<tr>
<td class="content-block powered-by" style="font-family: sans-serif; vertical-align: top; font-size: 11px; padding-bottom: 30px; padding-top: 10px;">
<p class="text-muted" style="font-size: 11px; text-decoration: none;">You can also accept the invitation by visiting the page directly at <span class="copy-link">{{ .link }}</span></p>
<p class="text-muted" style="font-size: 11px; text-decoration: none;">You can also accept the invitation by visiting the page directly at <span class="copy-link">{{ .Link }}</span></p>
</td>
</tr>
</tbody>
@ -57,7 +57,7 @@
</tr>
<tr>
<td class="content-block powered-by" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; font-size: 12px; text-align: center;">
<a href="https://artifacthub.io" class="AHlink" style="font-size: 12px; text-align: center; text-decoration: none;">© Artifact Hub</a>
<span class="AHlink" style="font-size: 12px; text-align: center; text-decoration: none;">© {{ .Theme.SiteName }}</span>
</td>
</tr>
</table>

View File

@ -23,6 +23,7 @@ import (
"github.com/pquerna/otp"
"github.com/pquerna/otp/totp"
"github.com/satori/uuid"
"github.com/spf13/viper"
pwvalidator "github.com/wagslane/go-password-validator"
"golang.org/x/crypto/bcrypt"
)
@ -110,16 +111,18 @@ var (
// Manager provides an API to manage users.
type Manager struct {
cfg *viper.Viper
db hub.DB
es hub.EmailSender
tmpl map[templateID]*template.Template
}
// NewManager creates a new Manager instance.
func NewManager(db hub.DB, es hub.EmailSender) *Manager {
func NewManager(cfg *viper.Viper, db hub.DB, es hub.EmailSender) *Manager {
return &Manager{
db: db,
es: es,
cfg: cfg,
db: db,
es: es,
tmpl: map[templateID]*template.Template{
passwordResetEmail: template.Must(template.New("").Parse(email.BaseTmpl + passwordResetEmailTmpl)),
passwordResetSuccessEmail: template.Must(template.New("").Parse(email.BaseTmpl + passwordResetSuccessEmailTmpl)),
@ -334,7 +337,7 @@ func (m *Manager) DisableTFA(ctx context.Context, passcode string) error {
return err
}
var emailBody bytes.Buffer
if err := m.tmpl[tfaDisabledEmail].Execute(&emailBody, nil); err != nil {
if err := m.tmpl[tfaDisabledEmail].Execute(&emailBody, baseTemplateData(m.cfg)); err != nil {
return err
}
emailData := &email.Data{
@ -387,7 +390,7 @@ func (m *Manager) EnableTFA(ctx context.Context, passcode string) error {
return err
}
var emailBody bytes.Buffer
if err := m.tmpl[tfaEnabledEmail].Execute(&emailBody, nil); err != nil {
if err := m.tmpl[tfaEnabledEmail].Execute(&emailBody, baseTemplateData(m.cfg)); err != nil {
return err
}
emailData := &email.Data{
@ -472,9 +475,8 @@ func (m *Manager) RegisterPasswordResetCode(ctx context.Context, userEmail, base
// Send password reset email
if m.es != nil {
templateData := map[string]string{
"link": fmt.Sprintf("%s/reset-password?code=%s", baseURL, code),
}
templateData := baseTemplateData(m.cfg)
templateData["Link"] = fmt.Sprintf("%s/reset-password?code=%s", baseURL, code)
var emailBody bytes.Buffer
if err := m.tmpl[passwordResetEmail].Execute(&emailBody, templateData); err != nil {
return err
@ -575,9 +577,8 @@ func (m *Manager) RegisterUser(ctx context.Context, user *hub.User, baseURL stri
// Send email verification code
if code != nil && m.es != nil {
templateData := map[string]string{
"link": fmt.Sprintf("%s/verify-email?code=%s", baseURL, *code),
}
templateData := baseTemplateData(m.cfg)
templateData["Link"] = fmt.Sprintf("%s/verify-email?code=%s", baseURL, *code)
var emailBody bytes.Buffer
if err := m.tmpl[verificationEmail].Execute(&emailBody, templateData); err != nil {
return err
@ -632,9 +633,8 @@ func (m *Manager) ResetPassword(ctx context.Context, code, newPassword, baseURL
// Send password reset success email
if m.es != nil {
templateData := map[string]string{
"baseURL": baseURL,
}
templateData := baseTemplateData(m.cfg)
templateData["BaseURL"] = baseURL
var emailBody bytes.Buffer
if err := m.tmpl[passwordResetSuccessEmail].Execute(&emailBody, templateData); err != nil {
return err
@ -667,7 +667,7 @@ func (m *Manager) SetupTFA(ctx context.Context) ([]byte, error) {
// Generate TOTP key
opts := totp.GenerateOpts{
Issuer: "Artifact Hub",
Issuer: m.cfg.GetString("theme.siteName"),
AccountName: userEmail,
}
key, err := totp.Generate(opts)
@ -807,3 +807,15 @@ func isValidRecoveryCode(recoveryCodes []string, code string) bool {
}
return false
}
// baseTemplateData creates a new base template data from the configuration
// provided.
func baseTemplateData(cfg *viper.Viper) map[string]interface{} {
return map[string]interface{}{
"Theme": map[string]string{
"PrimaryColor": cfg.GetString("theme.colors.primary"),
"SecondaryColor": cfg.GetString("theme.colors.secondary"),
"SiteName": cfg.GetString("theme.siteName"),
},
}
}

View File

@ -14,12 +14,20 @@ import (
"github.com/jackc/pgx/v4"
"github.com/pquerna/otp/totp"
"github.com/satori/uuid"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/bcrypt"
)
var cfg *viper.Viper
func init() {
cfg = viper.New()
cfg.Set("theme.siteName", "Artifact Hub")
}
func TestApproveSession(t *testing.T) {
ctx := context.Background()
sessionID := "sessionID"
@ -58,7 +66,7 @@ func TestApproveSession(t *testing.T) {
tc := tc
t.Run(tc.errMsg, func(t *testing.T) {
t.Parallel()
m := NewManager(nil, nil)
m := NewManager(cfg, nil, nil)
err := m.ApproveSession(ctx, tc.sessionID, tc.passcode)
assert.True(t, errors.Is(err, hub.ErrInvalidInput))
assert.Contains(t, err.Error(), tc.errMsg)
@ -70,7 +78,7 @@ func TestApproveSession(t *testing.T) {
t.Parallel()
db := &tests.DBMock{}
db.On("QueryRow", ctx, getUserIDFromSessionIDDBQ, hashedSessionID).Return("", tests.ErrFakeDB)
m := NewManager(db, nil)
m := NewManager(cfg, db, nil)
err := m.ApproveSession(ctx, sessionID, "123456")
assert.Equal(t, tests.ErrFakeDB, err)
@ -82,7 +90,7 @@ func TestApproveSession(t *testing.T) {
db := &tests.DBMock{}
db.On("QueryRow", ctx, getUserIDFromSessionIDDBQ, hash(sessionID)).Return("userID", nil)
db.On("QueryRow", ctx, getTFAConfigDBQ, "userID").Return(nil, tests.ErrFakeDB)
m := NewManager(db, nil)
m := NewManager(cfg, db, nil)
err := m.ApproveSession(ctx, sessionID, "123456")
assert.Equal(t, tests.ErrFakeDB, err)
@ -94,7 +102,7 @@ func TestApproveSession(t *testing.T) {
db := &tests.DBMock{}
db.On("QueryRow", ctx, getUserIDFromSessionIDDBQ, hash(sessionID)).Return("userID", nil)
db.On("QueryRow", ctx, getTFAConfigDBQ, "userID").Return(tfaConfigJSON, nil)
m := NewManager(db, nil)
m := NewManager(cfg, db, nil)
err := m.ApproveSession(ctx, sessionID, "123456")
assert.Equal(t, errInvalidTFAPasscode, err)
@ -107,7 +115,7 @@ func TestApproveSession(t *testing.T) {
db.On("QueryRow", ctx, getUserIDFromSessionIDDBQ, hash(sessionID)).Return("userID", nil)
db.On("QueryRow", ctx, getTFAConfigDBQ, "userID").Return(tfaConfigJSON, nil)
db.On("Exec", ctx, approveSessionDBQ, hashedSessionID, "").Return(nil)
m := NewManager(db, nil)
m := NewManager(cfg, db, nil)
passcode, _ := totp.GenerateCode(key.Secret(), time.Now())
err := m.ApproveSession(ctx, sessionID, passcode)
@ -121,7 +129,7 @@ func TestApproveSession(t *testing.T) {
db.On("QueryRow", ctx, getUserIDFromSessionIDDBQ, hash(sessionID)).Return("userID", nil)
db.On("QueryRow", ctx, getTFAConfigDBQ, "userID").Return(tfaConfigJSON, nil)
db.On("Exec", ctx, approveSessionDBQ, hashedSessionID, code1).Return(nil)
m := NewManager(db, nil)
m := NewManager(cfg, db, nil)
err := m.ApproveSession(ctx, sessionID, code1)
assert.Nil(t, err)
@ -153,7 +161,7 @@ func TestCheckAvailability(t *testing.T) {
tc := tc
t.Run(tc.errMsg, func(t *testing.T) {
t.Parallel()
m := NewManager(nil, nil)
m := NewManager(cfg, nil, nil)
_, err := m.CheckAvailability(ctx, tc.resourceKind, tc.value)
assert.True(t, errors.Is(err, hub.ErrInvalidInput))
assert.Contains(t, err.Error(), tc.errMsg)
@ -180,7 +188,7 @@ func TestCheckAvailability(t *testing.T) {
tc.dbQuery = fmt.Sprintf("select not exists (%s)", tc.dbQuery)
db := &tests.DBMock{}
db.On("QueryRow", ctx, tc.dbQuery, "value").Return(tc.available, nil)
m := NewManager(db, nil)
m := NewManager(cfg, db, nil)
available, err := m.CheckAvailability(ctx, tc.resourceKind, "value")
assert.NoError(t, err)
@ -195,7 +203,7 @@ func TestCheckAvailability(t *testing.T) {
db := &tests.DBMock{}
dbQuery := fmt.Sprintf(`select not exists (%s)`, checkUserAliasAvailDBQ)
db.On("QueryRow", ctx, dbQuery, "value").Return(false, tests.ErrFakeDB)
m := NewManager(db, nil)
m := NewManager(cfg, db, nil)
available, err := m.CheckAvailability(ctx, "userAlias", "value")
assert.Equal(t, tests.ErrFakeDB, err)
@ -228,7 +236,7 @@ func TestCheckCredentials(t *testing.T) {
tc := tc
t.Run(tc.errMsg, func(t *testing.T) {
t.Parallel()
m := NewManager(nil, nil)
m := NewManager(cfg, nil, nil)
_, err := m.CheckCredentials(ctx, tc.email, tc.password)
assert.True(t, errors.Is(err, hub.ErrInvalidInput))
assert.Contains(t, err.Error(), tc.errMsg)
@ -240,7 +248,7 @@ func TestCheckCredentials(t *testing.T) {
t.Parallel()
db := &tests.DBMock{}
db.On("QueryRow", ctx, checkUserCredsDBQ, "email").Return(nil, pgx.ErrNoRows)
m := NewManager(db, nil)
m := NewManager(cfg, db, nil)
output, err := m.CheckCredentials(ctx, "email", "pass")
assert.NoError(t, err)
@ -253,7 +261,7 @@ func TestCheckCredentials(t *testing.T) {
t.Parallel()
db := &tests.DBMock{}
db.On("QueryRow", ctx, checkUserCredsDBQ, "email").Return(nil, tests.ErrFakeDB)
m := NewManager(db, nil)
m := NewManager(cfg, db, nil)
output, err := m.CheckCredentials(ctx, "email", "pass")
assert.Equal(t, tests.ErrFakeDB, err)
@ -266,7 +274,7 @@ func TestCheckCredentials(t *testing.T) {
pw, _ := bcrypt.GenerateFromPassword([]byte("pass"), bcrypt.DefaultCost)
db := &tests.DBMock{}
db.On("QueryRow", ctx, checkUserCredsDBQ, "email").Return([]interface{}{"userID", string(pw)}, nil)
m := NewManager(db, nil)
m := NewManager(cfg, db, nil)
output, err := m.CheckCredentials(ctx, "email", "pass2")
assert.NoError(t, err)
@ -280,7 +288,7 @@ func TestCheckCredentials(t *testing.T) {
pw, _ := bcrypt.GenerateFromPassword([]byte("pass"), bcrypt.DefaultCost)
db := &tests.DBMock{}
db.On("QueryRow", ctx, checkUserCredsDBQ, "email").Return([]interface{}{"userID", string(pw)}, nil)
m := NewManager(db, nil)
m := NewManager(cfg, db, nil)
output, err := m.CheckCredentials(ctx, "email", "pass")
assert.NoError(t, err)
@ -316,7 +324,7 @@ func TestCheckSession(t *testing.T) {
tc := tc
t.Run(tc.errMsg, func(t *testing.T) {
t.Parallel()
m := NewManager(nil, nil)
m := NewManager(cfg, nil, nil)
_, err := m.CheckSession(ctx, tc.sessionID, tc.duration)
assert.True(t, errors.Is(err, hub.ErrInvalidInput))
assert.Contains(t, err.Error(), tc.errMsg)
@ -328,7 +336,7 @@ func TestCheckSession(t *testing.T) {
t.Parallel()
db := &tests.DBMock{}
db.On("QueryRow", ctx, getSessionDBQ, hashedSessionID).Return(nil, pgx.ErrNoRows)
m := NewManager(db, nil)
m := NewManager(cfg, db, nil)
output, err := m.CheckSession(ctx, sessionID, 1*time.Hour)
assert.NoError(t, err)
@ -341,7 +349,7 @@ func TestCheckSession(t *testing.T) {
t.Parallel()
db := &tests.DBMock{}
db.On("QueryRow", ctx, getSessionDBQ, hashedSessionID).Return(nil, tests.ErrFakeDB)
m := NewManager(db, nil)
m := NewManager(cfg, db, nil)
output, err := m.CheckSession(ctx, sessionID, 1*time.Hour)
assert.Equal(t, tests.ErrFakeDB, err)
@ -357,7 +365,7 @@ func TestCheckSession(t *testing.T) {
int64(1),
true,
}, nil)
m := NewManager(db, nil)
m := NewManager(cfg, db, nil)
output, err := m.CheckSession(ctx, sessionID, 1*time.Hour)
assert.NoError(t, err)
@ -374,7 +382,7 @@ func TestCheckSession(t *testing.T) {
time.Now().Unix(),
false,
}, nil)
m := NewManager(db, nil)
m := NewManager(cfg, db, nil)
output, err := m.CheckSession(ctx, sessionID, 1*time.Hour)
assert.NoError(t, err)
@ -391,7 +399,7 @@ func TestCheckSession(t *testing.T) {
time.Now().Unix(),
true,
}, nil)
m := NewManager(db, nil)
m := NewManager(cfg, db, nil)
output, err := m.CheckSession(ctx, sessionID, 1*time.Hour)
assert.NoError(t, err)
@ -422,7 +430,7 @@ func TestDeleteSession(t *testing.T) {
tc := tc
t.Run(tc.errMsg, func(t *testing.T) {
t.Parallel()
m := NewManager(nil, nil)
m := NewManager(cfg, nil, nil)
err := m.DeleteSession(ctx, tc.sessionID)
assert.True(t, errors.Is(err, hub.ErrInvalidInput))
assert.Contains(t, err.Error(), tc.errMsg)
@ -450,7 +458,7 @@ func TestDeleteSession(t *testing.T) {
t.Parallel()
db := &tests.DBMock{}
db.On("Exec", ctx, deleteSessionDBQ, hashedSessionID).Return(tc.dbResponse)
m := NewManager(db, nil)
m := NewManager(cfg, db, nil)
err := m.DeleteSession(ctx, sessionID)
assert.Equal(t, tc.dbResponse, err)
@ -477,7 +485,7 @@ func TestDisableTFA(t *testing.T) {
t.Run("user id not found in ctx", func(t *testing.T) {
t.Parallel()
m := NewManager(nil, nil)
m := NewManager(cfg, nil, nil)
assert.Panics(t, func() {
_ = m.DisableTFA(ctx, "123456")
})
@ -485,7 +493,7 @@ func TestDisableTFA(t *testing.T) {
t.Run("invalid input", func(t *testing.T) {
t.Parallel()
m := NewManager(nil, nil)
m := NewManager(cfg, nil, nil)
err := m.DisableTFA(ctx, "")
assert.True(t, errors.Is(err, hub.ErrInvalidInput))
@ -495,7 +503,7 @@ func TestDisableTFA(t *testing.T) {
t.Parallel()
db := &tests.DBMock{}
db.On("QueryRow", ctx, getTFAConfigDBQ, "userID").Return(nil, tests.ErrFakeDB)
m := NewManager(db, nil)
m := NewManager(cfg, db, nil)
err := m.DisableTFA(ctx, "123456")
assert.Equal(t, tests.ErrFakeDB, err)
@ -506,7 +514,7 @@ func TestDisableTFA(t *testing.T) {
t.Parallel()
db := &tests.DBMock{}
db.On("QueryRow", ctx, getTFAConfigDBQ, "userID").Return(tfaConfigJSON, nil)
m := NewManager(db, nil)
m := NewManager(cfg, db, nil)
err := m.DisableTFA(ctx, "123456")
assert.Equal(t, errInvalidTFAPasscode, err)
@ -518,7 +526,7 @@ func TestDisableTFA(t *testing.T) {
db := &tests.DBMock{}
db.On("QueryRow", ctx, getTFAConfigDBQ, "userID").Return(tfaConfigJSON, nil)
db.On("Exec", ctx, disableTFADBQ, "userID").Return(tests.ErrFakeDB)
m := NewManager(db, nil)
m := NewManager(cfg, db, nil)
passcode, _ := totp.GenerateCode(key.Secret(), time.Now())
err := m.DisableTFA(ctx, passcode)
@ -531,7 +539,7 @@ func TestDisableTFA(t *testing.T) {
db := &tests.DBMock{}
db.On("QueryRow", ctx, getTFAConfigDBQ, "userID").Return(tfaConfigJSON, nil)
db.On("Exec", ctx, disableTFADBQ, "userID").Return(nil)
m := NewManager(db, nil)
m := NewManager(cfg, db, nil)
passcode, _ := totp.GenerateCode(key.Secret(), time.Now())
err := m.DisableTFA(ctx, passcode)
@ -544,7 +552,7 @@ func TestDisableTFA(t *testing.T) {
db := &tests.DBMock{}
db.On("QueryRow", ctx, getTFAConfigDBQ, "userID").Return(tfaConfigJSON, nil)
db.On("Exec", ctx, disableTFADBQ, "userID").Return(nil)
m := NewManager(db, nil)
m := NewManager(cfg, db, nil)
err := m.DisableTFA(ctx, code1)
assert.Nil(t, err)
@ -567,7 +575,7 @@ func TestEnableTFA(t *testing.T) {
t.Run("user id not found in ctx", func(t *testing.T) {
t.Parallel()
m := NewManager(nil, nil)
m := NewManager(cfg, nil, nil)
assert.Panics(t, func() {
_ = m.EnableTFA(ctx, "123456")
})
@ -575,7 +583,7 @@ func TestEnableTFA(t *testing.T) {
t.Run("invalid input", func(t *testing.T) {
t.Parallel()
m := NewManager(nil, nil)
m := NewManager(cfg, nil, nil)
err := m.EnableTFA(ctx, "")
assert.True(t, errors.Is(err, hub.ErrInvalidInput))
@ -585,7 +593,7 @@ func TestEnableTFA(t *testing.T) {
t.Parallel()
db := &tests.DBMock{}
db.On("QueryRow", ctx, getTFAConfigDBQ, "userID").Return(nil, tests.ErrFakeDB)
m := NewManager(db, nil)
m := NewManager(cfg, db, nil)
err := m.EnableTFA(ctx, "123456")
assert.Equal(t, tests.ErrFakeDB, err)
@ -596,7 +604,7 @@ func TestEnableTFA(t *testing.T) {
t.Parallel()
db := &tests.DBMock{}
db.On("QueryRow", ctx, getTFAConfigDBQ, "userID").Return(tfaConfigJSON, nil)
m := NewManager(db, nil)
m := NewManager(cfg, db, nil)
err := m.EnableTFA(ctx, "123456")
assert.Equal(t, errInvalidTFAPasscode, err)
@ -608,7 +616,7 @@ func TestEnableTFA(t *testing.T) {
db := &tests.DBMock{}
db.On("QueryRow", ctx, getTFAConfigDBQ, "userID").Return(tfaConfigJSON, nil)
db.On("Exec", ctx, enableTFADBQ, "userID").Return(tests.ErrFakeDB)
m := NewManager(db, nil)
m := NewManager(cfg, db, nil)
passcode, _ := totp.GenerateCode(key.Secret(), time.Now())
err := m.EnableTFA(ctx, passcode)
@ -624,7 +632,7 @@ func TestEnableTFA(t *testing.T) {
db.On("QueryRow", ctx, getUserEmailDBQ, "userID").Return("email", nil)
es := &email.SenderMock{}
es.On("SendEmail", mock.Anything).Return(email.ErrFakeSenderFailure)
m := NewManager(db, es)
m := NewManager(cfg, db, es)
passcode, _ := totp.GenerateCode(key.Secret(), time.Now())
err := m.EnableTFA(ctx, passcode)
@ -640,7 +648,7 @@ func TestEnableTFA(t *testing.T) {
db.On("QueryRow", ctx, getUserEmailDBQ, "userID").Return("email", nil)
es := &email.SenderMock{}
es.On("SendEmail", mock.Anything).Return(nil)
m := NewManager(db, es)
m := NewManager(cfg, db, es)
passcode, _ := totp.GenerateCode(key.Secret(), time.Now())
err := m.EnableTFA(ctx, passcode)
@ -654,7 +662,7 @@ func TestGetProfile(t *testing.T) {
t.Run("user id not found in ctx", func(t *testing.T) {
t.Parallel()
m := NewManager(nil, nil)
m := NewManager(cfg, nil, nil)
assert.Panics(t, func() {
_, _ = m.GetProfile(context.Background())
})
@ -664,7 +672,7 @@ func TestGetProfile(t *testing.T) {
t.Parallel()
db := &tests.DBMock{}
db.On("QueryRow", ctx, getUserProfileDBQ, "userID").Return(nil, tests.ErrFakeDB)
m := NewManager(db, nil)
m := NewManager(cfg, db, nil)
profile, err := m.GetProfile(ctx)
assert.Equal(t, tests.ErrFakeDB, err)
@ -696,7 +704,7 @@ func TestGetProfile(t *testing.T) {
"tfa_enabled": true
}
`), nil)
m := NewManager(db, nil)
m := NewManager(cfg, db, nil)
profile, err := m.GetProfile(ctx)
assert.NoError(t, err)
@ -710,7 +718,7 @@ func TestGetProfileJSON(t *testing.T) {
t.Run("user id not found in ctx", func(t *testing.T) {
t.Parallel()
m := NewManager(nil, nil)
m := NewManager(cfg, nil, nil)
assert.Panics(t, func() {
_, _ = m.GetProfileJSON(context.Background())
})
@ -720,7 +728,7 @@ func TestGetProfileJSON(t *testing.T) {
t.Parallel()
db := &tests.DBMock{}
db.On("QueryRow", ctx, getUserProfileDBQ, "userID").Return([]byte("dataJSON"), nil)
m := NewManager(db, nil)
m := NewManager(cfg, db, nil)
data, err := m.GetProfileJSON(ctx)
assert.NoError(t, err)
@ -732,7 +740,7 @@ func TestGetProfileJSON(t *testing.T) {
t.Parallel()
db := &tests.DBMock{}
db.On("QueryRow", ctx, getUserProfileDBQ, "userID").Return(nil, tests.ErrFakeDB)
m := NewManager(db, nil)
m := NewManager(cfg, db, nil)
data, err := m.GetProfileJSON(ctx)
assert.Equal(t, tests.ErrFakeDB, err)
@ -746,7 +754,7 @@ func TestGetUserID(t *testing.T) {
t.Run("invalid input", func(t *testing.T) {
t.Parallel()
m := NewManager(nil, nil)
m := NewManager(cfg, nil, nil)
_, err := m.GetUserID(ctx, "")
assert.True(t, errors.Is(err, hub.ErrInvalidInput))
})
@ -755,7 +763,7 @@ func TestGetUserID(t *testing.T) {
t.Parallel()
db := &tests.DBMock{}
db.On("QueryRow", ctx, getUserIDFromEmailDBQ, "email").Return("userID", nil)
m := NewManager(db, nil)
m := NewManager(cfg, db, nil)
userID, err := m.GetUserID(ctx, "email")
assert.NoError(t, err)
@ -767,7 +775,7 @@ func TestGetUserID(t *testing.T) {
t.Parallel()
db := &tests.DBMock{}
db.On("QueryRow", ctx, getUserIDFromEmailDBQ, "email").Return("", tests.ErrFakeDB)
m := NewManager(db, nil)
m := NewManager(cfg, db, nil)
userID, err := m.GetUserID(ctx, "email")
assert.Equal(t, tests.ErrFakeDB, err)
@ -798,7 +806,7 @@ func TestRegisterSession(t *testing.T) {
tc := tc
t.Run(tc.errMsg, func(t *testing.T) {
t.Parallel()
m := NewManager(nil, nil)
m := NewManager(cfg, nil, nil)
s := &hub.Session{UserID: tc.userID}
_, err := m.RegisterSession(ctx, s)
assert.True(t, errors.Is(err, hub.ErrInvalidInput))
@ -811,7 +819,7 @@ func TestRegisterSession(t *testing.T) {
t.Parallel()
db := &tests.DBMock{}
db.On("QueryRow", ctx, registerSessionDBQ, mock.Anything).Return(nil, tests.ErrFakeDB)
m := NewManager(db, nil)
m := NewManager(cfg, db, nil)
sIN := &hub.Session{UserID: userID}
sOUT, err := m.RegisterSession(ctx, sIN)
@ -824,7 +832,7 @@ func TestRegisterSession(t *testing.T) {
t.Parallel()
db := &tests.DBMock{}
db.On("QueryRow", ctx, registerSessionDBQ, mock.Anything).Return(true, nil)
m := NewManager(db, nil)
m := NewManager(cfg, db, nil)
sIN := &hub.Session{
UserID: userID,
@ -865,7 +873,7 @@ func TestRegisterPasswordResetCode(t *testing.T) {
t.Run(tc.errMsg, func(t *testing.T) {
t.Parallel()
es := &email.SenderMock{}
m := NewManager(nil, es)
m := NewManager(cfg, nil, es)
err := m.RegisterPasswordResetCode(ctx, tc.userEmail, tc.baseURL)
assert.True(t, errors.Is(err, hub.ErrInvalidInput))
@ -896,7 +904,7 @@ func TestRegisterPasswordResetCode(t *testing.T) {
db.On("Exec", ctx, registerPasswordResetCodeDBQ, "email@email.com", mock.Anything).Return(nil)
es := &email.SenderMock{}
es.On("SendEmail", mock.Anything).Return(tc.emailSenderResponse)
m := NewManager(db, es)
m := NewManager(cfg, db, es)
err := m.RegisterPasswordResetCode(ctx, "email@email.com", "http://baseurl.com")
assert.Equal(t, tc.emailSenderResponse, err)
@ -910,7 +918,7 @@ func TestRegisterPasswordResetCode(t *testing.T) {
t.Parallel()
db := &tests.DBMock{}
db.On("Exec", ctx, registerPasswordResetCodeDBQ, "email@email.com", mock.Anything).Return(tests.ErrFakeDB)
m := NewManager(db, nil)
m := NewManager(cfg, db, nil)
err := m.RegisterPasswordResetCode(ctx, "email@email.com", "http://baseurl.com")
assert.Equal(t, tests.ErrFakeDB, err)
@ -959,7 +967,7 @@ func TestRegisterUser(t *testing.T) {
t.Run(tc.errMsg, func(t *testing.T) {
t.Parallel()
es := &email.SenderMock{}
m := NewManager(nil, es)
m := NewManager(cfg, nil, es)
err := m.RegisterUser(ctx, tc.user, tc.baseURL)
assert.True(t, errors.Is(err, hub.ErrInvalidInput))
@ -991,7 +999,7 @@ func TestRegisterUser(t *testing.T) {
db.On("QueryRow", ctx, registerUserDBQ, mock.Anything).Return(&code, nil)
es := &email.SenderMock{}
es.On("SendEmail", mock.Anything).Return(tc.emailSenderResponse)
m := NewManager(db, es)
m := NewManager(cfg, db, es)
u := &hub.User{
Alias: "alias",
@ -1014,7 +1022,7 @@ func TestRegisterUser(t *testing.T) {
code := ""
db := &tests.DBMock{}
db.On("QueryRow", ctx, registerUserDBQ, mock.Anything).Return(&code, tests.ErrFakeDB)
m := NewManager(db, nil)
m := NewManager(cfg, db, nil)
u := &hub.User{
Alias: "alias",
@ -1071,7 +1079,7 @@ func TestResetPassword(t *testing.T) {
t.Run(tc.errMsg, func(t *testing.T) {
t.Parallel()
es := &email.SenderMock{}
m := NewManager(nil, es)
m := NewManager(cfg, nil, es)
err := m.ResetPassword(ctx, tc.code, tc.newPassword, tc.baseURL)
assert.True(t, errors.Is(err, hub.ErrInvalidInput))
assert.Contains(t, err.Error(), tc.errMsg)
@ -1099,7 +1107,7 @@ func TestResetPassword(t *testing.T) {
t.Parallel()
db := &tests.DBMock{}
db.On("QueryRow", ctx, resetUserPasswordDBQ, codeHashed, mock.Anything).Return("", tc.dbErr)
m := NewManager(db, nil)
m := NewManager(cfg, db, nil)
err := m.ResetPassword(ctx, code, newPassword, baseURL)
assert.Equal(t, tc.expectedErr, err)
@ -1130,7 +1138,7 @@ func TestResetPassword(t *testing.T) {
db.On("QueryRow", ctx, resetUserPasswordDBQ, codeHashed, mock.Anything).Return("email", nil)
es := &email.SenderMock{}
es.On("SendEmail", mock.Anything).Return(tc.emailSenderResponse)
m := NewManager(db, es)
m := NewManager(cfg, db, es)
err := m.ResetPassword(ctx, code, newPassword, baseURL)
assert.Equal(t, tc.emailSenderResponse, err)
@ -1146,7 +1154,7 @@ func TestSetupTFA(t *testing.T) {
t.Run("user id not found in ctx", func(t *testing.T) {
t.Parallel()
m := NewManager(nil, nil)
m := NewManager(cfg, nil, nil)
assert.Panics(t, func() {
_, _ = m.SetupTFA(context.Background())
})
@ -1156,7 +1164,7 @@ func TestSetupTFA(t *testing.T) {
t.Parallel()
db := &tests.DBMock{}
db.On("QueryRow", ctx, getUserEmailDBQ, "userID").Return("", tests.ErrFakeDB)
m := NewManager(db, nil)
m := NewManager(cfg, db, nil)
dataJSON, err := m.SetupTFA(ctx)
assert.Nil(t, dataJSON)
@ -1169,7 +1177,7 @@ func TestSetupTFA(t *testing.T) {
db := &tests.DBMock{}
db.On("QueryRow", ctx, getUserEmailDBQ, "userID").Return("email", nil)
db.On("Exec", ctx, updateTFAInfoDBQ, "userID", mock.Anything, mock.Anything).Return(tests.ErrFake)
m := NewManager(db, nil)
m := NewManager(cfg, db, nil)
dataJSON, err := m.SetupTFA(ctx)
assert.Nil(t, dataJSON)
@ -1182,7 +1190,7 @@ func TestSetupTFA(t *testing.T) {
db := &tests.DBMock{}
db.On("QueryRow", ctx, getUserEmailDBQ, "userID").Return("email", nil)
db.On("Exec", ctx, updateTFAInfoDBQ, "userID", mock.Anything, mock.Anything).Return(nil)
m := NewManager(db, nil)
m := NewManager(cfg, db, nil)
dataJSON, err := m.SetupTFA(ctx)
assert.NotNil(t, dataJSON)
@ -1207,7 +1215,7 @@ func TestUpdatePassword(t *testing.T) {
t.Run("user id not found in ctx", func(t *testing.T) {
t.Parallel()
m := NewManager(nil, nil)
m := NewManager(cfg, nil, nil)
assert.Panics(t, func() {
_ = m.UpdatePassword(context.Background(), "old", "new")
})
@ -1239,7 +1247,7 @@ func TestUpdatePassword(t *testing.T) {
tc := tc
t.Run(tc.errMsg, func(t *testing.T) {
t.Parallel()
m := NewManager(nil, nil)
m := NewManager(cfg, nil, nil)
err := m.UpdatePassword(ctx, tc.old, tc.new)
assert.True(t, errors.Is(err, hub.ErrInvalidInput))
assert.Contains(t, err.Error(), tc.errMsg)
@ -1251,7 +1259,7 @@ func TestUpdatePassword(t *testing.T) {
t.Parallel()
db := &tests.DBMock{}
db.On("QueryRow", ctx, getUserPasswordDBQ, "userID").Return("", tests.ErrFakeDB)
m := NewManager(db, nil)
m := NewManager(cfg, db, nil)
err := m.UpdatePassword(ctx, "old", new)
assert.Equal(t, tests.ErrFakeDB, err)
@ -1262,7 +1270,7 @@ func TestUpdatePassword(t *testing.T) {
t.Parallel()
db := &tests.DBMock{}
db.On("QueryRow", ctx, getUserPasswordDBQ, "userID").Return(string(oldHashed), nil)
m := NewManager(db, nil)
m := NewManager(cfg, db, nil)
err := m.UpdatePassword(ctx, "old2", new)
assert.Error(t, err)
@ -1275,7 +1283,7 @@ func TestUpdatePassword(t *testing.T) {
db.On("QueryRow", ctx, getUserPasswordDBQ, "userID").Return(string(oldHashed), nil)
db.On("Exec", ctx, updateUserPasswordDBQ, "userID", mock.Anything, mock.Anything).
Return(tests.ErrFakeDB)
m := NewManager(db, nil)
m := NewManager(cfg, db, nil)
err := m.UpdatePassword(ctx, "old", new)
assert.Equal(t, tests.ErrFakeDB, err)
@ -1287,7 +1295,7 @@ func TestUpdatePassword(t *testing.T) {
db := &tests.DBMock{}
db.On("QueryRow", ctx, getUserPasswordDBQ, "userID").Return(string(oldHashed), nil)
db.On("Exec", ctx, updateUserPasswordDBQ, "userID", mock.Anything, mock.Anything).Return(nil)
m := NewManager(db, nil)
m := NewManager(cfg, db, nil)
err := m.UpdatePassword(ctx, "old", new)
assert.NoError(t, err)
@ -1300,7 +1308,7 @@ func TestUpdateProfile(t *testing.T) {
t.Run("user id not found in ctx", func(t *testing.T) {
t.Parallel()
m := NewManager(nil, nil)
m := NewManager(cfg, nil, nil)
assert.Panics(t, func() {
_ = m.UpdateProfile(context.Background(), &hub.User{})
})
@ -1324,7 +1332,7 @@ func TestUpdateProfile(t *testing.T) {
tc := tc
t.Run(tc.errMsg, func(t *testing.T) {
t.Parallel()
m := NewManager(nil, nil)
m := NewManager(cfg, nil, nil)
err := m.UpdateProfile(ctx, tc.user)
assert.True(t, errors.Is(err, hub.ErrInvalidInput))
assert.Contains(t, err.Error(), tc.errMsg)
@ -1336,7 +1344,7 @@ func TestUpdateProfile(t *testing.T) {
t.Parallel()
db := &tests.DBMock{}
db.On("Exec", ctx, updateUserProfileDBQ, "userID", mock.Anything).Return(nil)
m := NewManager(db, nil)
m := NewManager(cfg, db, nil)
err := m.UpdateProfile(ctx, &hub.User{Alias: "user1"})
assert.NoError(t, err)
@ -1347,7 +1355,7 @@ func TestUpdateProfile(t *testing.T) {
t.Parallel()
db := &tests.DBMock{}
db.On("Exec", ctx, updateUserProfileDBQ, "userID", mock.Anything).Return(tests.ErrFakeDB)
m := NewManager(db, nil)
m := NewManager(cfg, db, nil)
err := m.UpdateProfile(ctx, &hub.User{Alias: "user1"})
assert.Equal(t, tests.ErrFakeDB, err)
@ -1360,7 +1368,7 @@ func TestVerifyEmail(t *testing.T) {
t.Run("invalid input", func(t *testing.T) {
t.Parallel()
m := NewManager(nil, nil)
m := NewManager(cfg, nil, nil)
_, err := m.VerifyEmail(ctx, "")
assert.True(t, errors.Is(err, hub.ErrInvalidInput))
})
@ -1369,7 +1377,7 @@ func TestVerifyEmail(t *testing.T) {
t.Parallel()
db := &tests.DBMock{}
db.On("QueryRow", ctx, verifyEmailDBQ, "emailVerificationCode").Return(true, nil)
m := NewManager(db, nil)
m := NewManager(cfg, db, nil)
verified, err := m.VerifyEmail(ctx, "emailVerificationCode")
assert.NoError(t, err)
@ -1381,7 +1389,7 @@ func TestVerifyEmail(t *testing.T) {
t.Parallel()
db := &tests.DBMock{}
db.On("QueryRow", ctx, verifyEmailDBQ, "emailVerificationCode").Return(false, tests.ErrFakeDB)
m := NewManager(db, nil)
m := NewManager(cfg, db, nil)
verified, err := m.VerifyEmail(ctx, "emailVerificationCode")
assert.Equal(t, tests.ErrFakeDB, err)
@ -1409,7 +1417,7 @@ func TestVerifyPasswordResetCode(t *testing.T) {
tc := tc
t.Run(tc.errMsg, func(t *testing.T) {
t.Parallel()
m := NewManager(nil, nil)
m := NewManager(cfg, nil, nil)
err := m.VerifyPasswordResetCode(ctx, tc.code)
assert.True(t, errors.Is(err, hub.ErrInvalidInput))
assert.Contains(t, err.Error(), tc.errMsg)
@ -1437,7 +1445,7 @@ func TestVerifyPasswordResetCode(t *testing.T) {
t.Parallel()
db := &tests.DBMock{}
db.On("Exec", ctx, verifyPasswordResetCodeDBQ, codeHashed).Return(tc.dbErr)
m := NewManager(db, nil)
m := NewManager(cfg, db, nil)
err := m.VerifyPasswordResetCode(ctx, code)
assert.Equal(t, tc.expectedErr, err)
@ -1450,7 +1458,7 @@ func TestVerifyPasswordResetCode(t *testing.T) {
t.Parallel()
db := &tests.DBMock{}
db.On("Exec", ctx, verifyPasswordResetCodeDBQ, codeHashed).Return(nil)
m := NewManager(db, nil)
m := NewManager(cfg, db, nil)
err := m.VerifyPasswordResetCode(ctx, code)
assert.Equal(t, nil, err)

View File

@ -12,7 +12,7 @@
<tr>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;">
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Hi!</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;"> We got a request to reset your <span class="AHlink" style="font-weight: bold;">Artifact Hub</span> password.</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;"> We got a request to reset your <span class="AHlink" style="font-weight: bold;">{{ .Theme.SiteName }}</span> password.</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">If you did not perform this request, you can safely ignore this email. Otherwise, click the link below to complete the process.</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 30px;">Please note that the password reset link <span style="font-weight: bold;">will only be valid for 15 minutes</span>. If you haven't completed the process by then, you'll need to get a new password reset link.</p>
<table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; box-sizing: border-box;">
@ -22,7 +22,7 @@
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;">
<tbody>
<tr>
<td style="font-family: sans-serif; font-size: 14px; border-radius: 5px; vertical-align: top; text-align: center;"> <a href="{{ .link }}" class="AHbtn" target="_blank" style="display: inline-block; border-radius: 5px; box-sizing: border-box; cursor: pointer; text-decoration: none; font-size: 14px; font-weight: bold; margin: 0; padding: 12px 25px; text-transform: capitalize;">Reset password</a> </td>
<td style="font-family: sans-serif; font-size: 14px; border-radius: 5px; vertical-align: top; text-align: center;"> <a href="{{ .Link }}" class="AHbtn" target="_blank" style="display: inline-block; border-radius: 5px; box-sizing: border-box; cursor: pointer; text-decoration: none; font-size: 14px; font-weight: bold; margin: 0; padding: 12px 25px; text-transform: capitalize;">Reset password</a> </td>
</tr>
</tbody>
</table>
@ -34,7 +34,7 @@
<tbody>
<tr>
<td class="content-block powered-by" style="font-family: sans-serif; vertical-align: top; font-size: 11px; padding-bottom: 30px; padding-top: 10px;">
<p class="text-muted" style="font-size: 11px; text-decoration: none;">Or you can copy-paste this link: <span class="copy-link">{{ .link }}</span></p>
<p class="text-muted" style="font-size: 11px; text-decoration: none;">Or you can copy-paste this link: <span class="copy-link">{{ .Link }}</span></p>
</td>
</tr>
</tbody>
@ -53,7 +53,7 @@
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
<tr>
<td class="content-block powered-by" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; font-size: 12px; text-align: center;">
<a href="https://artifacthub.io" class="AHlink" style="font-size: 12px; text-align: center; text-decoration: none;">© Artifact Hub</a>
<span class="AHlink" style="font-size: 12px; text-align: center; text-decoration: none;">© {{ .Theme.SiteName }}</span>
</td>
</tr>
</table>

View File

@ -1,8 +1,8 @@
{{ define "title" }} Your Artifact Hub password has been reset {{ end }}
{{ define "title" }} Your {{ .Theme.SiteName }} password has been reset {{ end }}
{{ define "content" }}
<div class="content" style="box-sizing: border-box; display: block; Margin: 0 auto; max-width: 580px; padding: 10px;">
<!-- START CENTERED WHITE CONTAINER -->
<span class="preheader" style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">Your Artifact Hub password has been reset</span>
<span class="preheader" style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">Your {{ .Theme.SiteName }} password has been reset</span>
<table class="main line" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; border-radius: 3px;">
<!-- START MAIN CONTENT AREA -->
<tr>
@ -11,7 +11,7 @@
<tr>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;">
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Hi!</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Your <span class="AHlink" style="font-weight: bold;">Artifact Hub</span> password has been reset. You can now use your new password to log in to your account.</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Your <span class="AHlink" style="font-weight: bold;">{{ .Theme.SiteName }}</span> password has been reset. You can now use your new password to log in to your account.</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 30px;">If this wasn't you, please reset your password to secure your account.</p>
<table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; box-sizing: border-box;">
<tbody>
@ -32,7 +32,7 @@
<tbody>
<tr>
<td class="content-block powered-by" style="font-family: sans-serif; vertical-align: top; font-size: 11px; padding-bottom: 30px; padding-top: 10px;">
<p class="text-muted" style="font-size: 11px; text-decoration: none;">Or you can copy-paste this link: <span class="copy-link">{{ .baseURL }}/?modal=login</span></p>
<p class="text-muted" style="font-size: 11px; text-decoration: none;">Or you can copy-paste this link: <span class="copy-link">{{ .BaseURL }}/?modal=login</span></p>
</td>
</tr>
</tbody>
@ -49,7 +49,7 @@
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
<tr>
<td class="content-block powered-by" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; font-size: 12px; text-align: center;">
<a href="https://artifacthub.io" class="AHlink" style="font-size: 12px; text-align: center; text-decoration: none;">© Artifact Hub</a>
<span class="AHlink" style="font-size: 12px; text-align: center; text-decoration: none;">© {{ .Theme.SiteName }}</span>
</td>
</tr>
</table>

View File

@ -13,8 +13,8 @@
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;">
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Hi!</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">
Two-factor authentication has been successfully disabled for your <span class="AHlink" style="font-weight: bold;">Artifact Hub</span> account.</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 30px;">Please, remember that two-factor authentication is an additional layer of security designed to prevent unauthorised access to your account and protect all your data in Artifact Hub.</p>
Two-factor authentication has been successfully disabled for your <span class="AHlink" style="font-weight: bold;">{{ .Theme.SiteName }}</span> account.</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 30px;">Please, remember that two-factor authentication is an additional layer of security designed to prevent unauthorised access to your account and protect all your data in {{ .Theme.SiteName }}.</p>
</td>
</tr>
</table>
@ -29,7 +29,7 @@
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
<tr>
<td class="content-block powered-by" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; font-size: 12px; text-align: center;">
<a href="https://artifacthub.io" class="AHlink" style="font-size: 12px; text-align: center; text-decoration: none;">© Artifact Hub</a>
<span class="AHlink" style="font-size: 12px; text-align: center; text-decoration: none;">© {{ .Theme.SiteName }}</span>
</td>
</tr>
</table>

View File

@ -12,7 +12,7 @@
<tr>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;">
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Hi!</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 30px;">Two-factor authentication has been successfully enabled for your <span class="AHlink" style="font-weight: bold;">Artifact Hub</span> account. Please don't forget to print the recovery codes provided during the setup process.</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 30px;">Two-factor authentication has been successfully enabled for your <span class="AHlink" style="font-weight: bold;">{{ .Theme.SiteName }}</span> account. Please don't forget to print the recovery codes provided during the setup process.</p>
</td>
</tr>
</table>
@ -27,7 +27,7 @@
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
<tr>
<td class="content-block powered-by" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; font-size: 12px; text-align: center;">
<a href="https://artifacthub.io" class="AHlink" style="font-size: 12px; text-align: center; text-decoration: none;">© Artifact Hub</a>
<span class="AHlink" style="font-size: 12px; text-align: center; text-decoration: none;">© {{ .Theme.SiteName }}</span>
</td>
</tr>
</table>

View File

@ -2,7 +2,7 @@
{{ define "content" }}
<div class="content" style="box-sizing: border-box; display: block; Margin: 0 auto; max-width: 580px; padding: 10px;">
<!-- START CENTERED WHITE CONTAINER -->
<span class="preheader" style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">Welcome to Artifact Hub!</span>
<span class="preheader" style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">Welcome to {{ .Theme.SiteName }}!</span>
<table class="main line" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; border-radius: 3px;">
<!-- START MAIN CONTENT AREA -->
@ -12,7 +12,7 @@
<tr>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;">
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Hi!</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Welcome to Artifact Hub! You are only one step from being able to sign in on our site. Please simply click on the link below to confirm your account.</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Welcome to {{ .Theme.SiteName }}! You are only one step from being able to sign in on our site. Please simply click on the link below to confirm your account.</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 30px;">Please note that the verification code <span style="font-weight: bold;">is only valid for 24 hours</span>. If you haven't verified your account by then you'll need to sign up again.</p>
<table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; box-sizing: border-box;">
<tbody>
@ -21,7 +21,7 @@
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;">
<tbody>
<tr>
<td style="font-family: sans-serif; font-size: 14px; border-radius: 5px; vertical-align: top; text-align: center;"> <a href="{{ .link }}" class="AHbtn" target="_blank" style="display: inline-block; color: #ffffff; border-radius: 5px; box-sizing: border-box; cursor: pointer; text-decoration: none; font-size: 14px; font-weight: bold; margin: 0; padding: 12px 25px; text-transform: capitalize;">Confirm your account</a> </td>
<td style="font-family: sans-serif; font-size: 14px; border-radius: 5px; vertical-align: top; text-align: center;"> <a href="{{ .Link }}" class="AHbtn" target="_blank" style="display: inline-block; color: #ffffff; border-radius: 5px; box-sizing: border-box; cursor: pointer; text-decoration: none; font-size: 14px; font-weight: bold; margin: 0; padding: 12px 25px; text-transform: capitalize;">Confirm your account</a> </td>
</tr>
</tbody>
</table>
@ -33,12 +33,12 @@
<tbody>
<tr>
<td class="content-block powered-by" style="font-family: sans-serif; vertical-align: top; font-size: 11px; padding-bottom: 30px; padding-top: 10px;">
<p class="text-muted" style="font-size: 11px; text-decoration: none;">Or you can copy-paste this link: <span class="copy-link">{{ .link }}</span></p>
<p class="text-muted" style="font-size: 11px; text-decoration: none;">Or you can copy-paste this link: <span class="copy-link">{{ .Link }}</span></p>
</td>
</tr>
</tbody>
</table>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">After activation you may sign in to Artifact Hub using your credentials.</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">After activation you may sign in to {{ .Theme.SiteName }} using your credentials.</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Thanks for creating an account.</p>
</td>
</tr>
@ -54,12 +54,12 @@
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
<tr>
<td class="content-block powered-by" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; font-size: 10px; text-align: center;">
<p class="text-muted" style="font-size: 10px; text-align: center; text-decoration: none;">Didn't create an Artifact Hub account? I's likely someone just typed in your email address by accident.<br>Feel free to ignore this email.</p>
<p class="text-muted" style="font-size: 10px; text-align: center; text-decoration: none;">Didn't create an {{ .Theme.SiteName }} account? I's likely someone just typed in your email address by accident.<br>Feel free to ignore this email.</p>
</td>
</tr>
<tr>
<td class="content-block powered-by" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; font-size: 12px; color: #2d4857; text-align: center;">
<a href="https://artifacthub.io" class="AHlink" style="font-size: 12px; text-align: center; text-decoration: none;">© Artifact Hub</a>
<td class="content-block powered-by" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; font-size: 12px; text-align: center;">
<span class="AHlink" style="font-size: 12px; text-align: center; text-decoration: none;">© {{ .Theme.SiteName }}</span>
</td>
</tr>
</table>

View File

@ -40,6 +40,7 @@
"regexify-string": "^1.0.5",
"remark-gfm": "^1.0.0",
"semver": "^7.3.2",
"tinycolor2": "^1.4.2",
"yaml": "^1.10.0"
},
"devDependencies": {

View File

@ -2,30 +2,34 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="shortcut icon" type="image/png" href="{{ .baseURL }}/static/media/logo_v2.png" />
<link rel="shortcut icon" type="image/png" href="{{ .shortcutIcon }}" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<link rel="apple-touch-icon" href="{{ .baseURL }}/static/media/logo192_v2.png" />
<link rel="apple-touch-icon" sizes="512x512" href="{{ .baseURL }}/static/media/logo512_v2.png" />
<link rel="manifest" href="{{ .baseURL }}/manifest.json" />
<link rel="apple-touch-icon" href="{{ .appleTouchIcon192 }}" />
<link rel="apple-touch-icon" sizes="512x512" href="{{ .appleTouchIcon512 }}" />
<link rel="manifest" href="/manifest.json" />
<title>{{ .title }}</title>
<meta name="description" content="{{ .description }}" />
<meta property="og:type" content="website" />
<meta property="og:title" content="{{ .title }}" />
<meta property="og:description" content="{{ .description }}" />
<meta property="og:image" content="{{ .baseURL }}/static/media/artifactHub_v2.png" />
<meta property="og:image" content="{{ .openGraphImage }}" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="{{ .title }}" />
<meta name="twitter:description" content="{{ .description }}" />
<meta name="twitter:image:src" content="{{ .baseURL }}/static/media/artifactHub_v2.png" />
<meta name="twitter:image:src" content="{{ .openGraphImage }}" />
<meta name="artifacthub:allowPrivateRepositories" content="{{ .allowPrivateRepositories }}" />
<meta name="artifacthub:gaTrackingID" content="{{ .gaTrackingID }}" />
<meta name="artifacthub:githubAuth" content="{{ .githubAuth }}" />
<meta name="artifacthub:googleAuth" content="{{ .googleAuth }}" />
<meta name="artifacthub:oidcAuth" content="{{ .oidcAuth }}" />
<meta name="artifacthub:motd" content="{{ .motd }}" />
<meta name="artifacthub:motdSeverity" content="{{ .motdSeverity }}" />
<meta name="artifacthub:gaTrackingID" content="{{ .gaTrackingID }}" />
<script type="text/javascript" src="{{ .baseURL }}/static/js/fixFirefoxNightMode.js" async></script>
<meta name="artifacthub:primaryColor" content="{{ .primaryColor }}" />
<meta name="artifacthub:secondaryColor" content="{{ .secondaryColor }}" />
<meta name="artifacthub:siteName" content="{{ .siteName }}" />
<meta name="artifacthub:websiteLogo" content="{{ .websiteLogo }}" />
<script type="text/javascript" src="/static/js/fixFirefoxNightMode.js" async></script>
<script type="application/ld+json">
{
"@context": "https://schema.org",

View File

@ -3,11 +3,11 @@ import googleAnalytics from '@analytics/google-analytics';
import Analytics from 'analytics';
import { isNull } from 'lodash';
import getMetaTag from '../utils/getMetaTag';
const getPlugins = (): object[] => {
let plugins: object[] = [];
const analyticsConfig: string | null = document.querySelector(`meta[name='artifacthub:gaTrackingID']`)
? document.querySelector(`meta[name='artifacthub:gaTrackingID']`)!.getAttribute('content')
: null;
const analyticsConfig: string | null = getMetaTag('gaTrackingID');
if (!isNull(analyticsConfig) && analyticsConfig !== '' && analyticsConfig !== '{{ .gaTrackingID }}') {
plugins.push(

View File

@ -11,6 +11,7 @@ import buildSearchParams from '../utils/buildSearchParams';
import detectActiveThemeMode from '../utils/detectActiveThemeMode';
import history from '../utils/history';
import lsPreferences from '../utils/localStoragePreferences';
import themeBuilder from '../utils/themeBuilder';
import AlertController from './common/AlertController';
import UserNotificationsController from './common/userNotifications';
import ControlPanelView from './controlPanel';
@ -41,6 +42,7 @@ export default function App() {
const [scrollPosition, setScrollPosition] = useState<undefined | number>(undefined);
useEffect(() => {
themeBuilder.init();
const activeProfile = lsPreferences.getActiveProfile();
const theme =
activeProfile.theme.configured === 'automatic' ? detectActiveThemeMode() : activeProfile.theme.configured;
@ -55,7 +57,7 @@ export default function App() {
return (
<AppCtxProvider>
<Router history={history}>
<div className="d-flex flex-column min-vh-100 position-relative">
<div className="d-flex flex-column min-vh-100 position-relative whiteBranded">
<div className="sr-only sr-only-focusable">
<a href="#content">Skip to Main Content</a>
</div>

View File

@ -8,11 +8,11 @@
}
.tooltipArrow::before {
border-bottom-color: var(--color-1-900) !important;
border-bottom-color: var(--dark) !important;
}
.tooltipContent {
background-color: var(--color-1-900) !important;
background-color: var(--dark) !important;
}
.btn {

View File

@ -23,11 +23,11 @@
}
.tooltipArrow::before {
border-bottom-color: var(--color-1-900) !important;
border-bottom-color: var(--dark) !important;
}
.tooltipContent {
background-color: var(--color-1-900) !important;
background-color: var(--dark) !important;
max-width: 100%;
}

View File

@ -207,7 +207,7 @@ const FilesModal = (props: Props) => {
<div className="text-center">
<button
data-testid="filesModalBtn"
className="btn btn-secondary btn-sm text-nowrap btn-block"
className="btn btn-outline-secondary btn-sm text-nowrap btn-block"
onClick={onOpenModal}
aria-label={`Open ${props.title} modal`}
disabled={isUndefined(props.files) || props.files.length === 0}

View File

@ -21,7 +21,7 @@
}
.hightlighted {
background-color: var(--color-3-50);
background-color: var(--color-2-500);
}
.checkMark {

View File

@ -61,9 +61,9 @@ const InputTypeaheadWithDropdown = (props: Props) => {
aria-expanded={!collapsed}
>
<div className="d-flex flex-row align-items-center justify-content-between">
<SmallTitle text={props.label} className="text-secondary font-weight-bold pt-2" />
<SmallTitle text={props.label} className="text-dark font-weight-bold pt-2" />
<MdFilterList className="mt-2 mb-1 text-secondary" />
<MdFilterList className="mt-2 mb-1 text-dark" />
</div>
<div>

View File

@ -158,7 +158,7 @@ const Modal = (props: Props) => {
<button
data-testid="closeModalFooterBtn"
type="button"
className="btn btn-sm btn-secondary text-uppercase"
className="btn btn-sm btn-outline-secondary text-uppercase"
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
closeModal();

View File

@ -6,8 +6,8 @@
@media (hover: hover) {
.card:hover {
border-color: var(--color-1-700);
box-shadow: 0px 0px 5px 0px var(--color-1-900);
border-color: var(--color-black-50);
box-shadow: 0px 0px 5px 0px var(--color-black-75);
}
}

View File

@ -69,7 +69,7 @@
}
.activeDropdownItem {
background-color: var(--color-black-5);
background-color: var(--color-2-500);
}
.truncateWrapper {

View File

@ -100,7 +100,7 @@ const SearchPackages = (props: Props) => {
<button
data-testid="searchIconBtn"
type="button"
className={`btn btn-secondary ml-3 text-center p-0 ${styles.searchBtn}`}
className={`btn btn-outline-secondary ml-3 text-center p-0 ${styles.searchBtn}`}
disabled={searchQuery === '' || isSearching}
onClick={(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.preventDefault();

View File

@ -100,7 +100,7 @@ const Sidebar = (props: Props) => {
<button
data-testid="closeSidebarFooterBtn"
type="button"
className="ml-auto btn btn-sm btn-secondary"
className="ml-auto btn btn-sm btn-outline-secondary"
onClick={() => openStatusChange(false)}
aria-label="Close"
>

View File

@ -23,7 +23,7 @@ exports[`SearchPackages creates snapshot 1`] = `
<button
aria-expanded="false"
aria-label="Search by "
class="btn btn-secondary ml-3 text-center p-0 searchBtn"
class="btn btn-outline-secondary ml-3 text-center p-0 searchBtn"
data-testid="searchIconBtn"
disabled=""
type="button"

View File

@ -17,7 +17,7 @@ interface HeadingProps {
const Heading: React.ElementType = (data: HeadingProps) => {
const Tag = `h${data.level}` as 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
return <Tag className={`text-secondary ${styles.header}`}>{data.title || data.children}</Tag>;
return <Tag className={`text-dark ${styles.header}`}>{data.title || data.children}</Tag>;
};
const ANIMATION_TIME = 300; //300ms

View File

@ -9,11 +9,11 @@
}
.tooltipArrow::before {
border-bottom-color: var(--color-1-900) !important;
border-bottom-color: var(--dark) !important;
}
.tooltipContent {
background-color: var(--color-1-900) !important;
background-color: var(--dark) !important;
}
.disabled {

View File

@ -3,6 +3,7 @@
min-width: 18px;
height: 18px;
line-height: 1rem;
top: 0;
}
.ctxBtn {
@ -34,7 +35,7 @@
}
.caret {
bottom: 5px;
bottom: 6px;
right: 7px;
}

View File

@ -70,7 +70,7 @@ const UserContext = () => {
<div className="d-flex flex-row align-items-center">
<button
data-testid="ctxBtn"
className={`btn btn-primary badge-pill btn-sm pr-3 position-relative ${styles.ctxBtn}`}
className={`btn btn-primary badge-pill border-0 btn-sm pr-3 position-relative ${styles.ctxBtn}`}
type="button"
onClick={() => {
fetchOrganizations();

View File

@ -19,7 +19,7 @@ exports[`UserContext creates snapshot 1`] = `
<button
aria-expanded="false"
aria-label="Open context"
class="btn btn-primary badge-pill btn-sm pr-3 position-relative ctxBtn"
class="btn btn-primary badge-pill border-0 btn-sm pr-3 position-relative ctxBtn"
data-testid="ctxBtn"
type="button"
>

View File

@ -151,7 +151,7 @@ exports[`ControlPanelView renders correctly 1`] = `
<button
aria-expanded="false"
aria-label="Open context"
class="btn btn-primary badge-pill btn-sm pr-3 position-relative ctxBtn"
class="btn btn-primary badge-pill border-0 btn-sm pr-3 position-relative ctxBtn"
data-testid="ctxBtn"
type="button"
>

View File

@ -6,8 +6,9 @@
height: 1.75rem;
width: 1.75rem;
border-radius: 50% !important;
background-color: var(--color-1-10) !important;
line-height: 1rem;
background-color: var(--white) !important;
border-color: var(--color-1-500);
line-height: 0.85rem;
}
.dropdownMenu {

View File

@ -107,7 +107,7 @@ const MemberCard = (props: Props) => {
closeButton={
<>
<button
className={`btn btn-sm btn-light text-uppercase ${styles.btnLight}`}
className="btn btn-sm btn-outline-secondary"
onClick={() => setModalStatus(false)}
aria-label="Cancel"
>
@ -177,7 +177,7 @@ const MemberCard = (props: Props) => {
{isUser ? (
<button
data-testid="leaveOrRemoveModalBtn"
className="dropdown-item btn btn-sm rounded-0 text-secondary"
className="dropdown-item btn btn-sm rounded-0 text-dark"
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
closeDropdown();
@ -193,7 +193,7 @@ const MemberCard = (props: Props) => {
) : (
<ActionBtn
testId="leaveOrRemoveModalBtn"
className="dropdown-item btn btn-sm rounded-0 text-secondary"
className="dropdown-item btn btn-sm rounded-0 text-dark"
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
closeDropdown();
@ -210,7 +210,7 @@ const MemberCard = (props: Props) => {
</div>
<button
className={`btn btn-light p-0 text-secondary text-center ${styles.btnDropdown}`}
className={`btn p-0 text-primary text-center iconSubsWrapper ${styles.btnDropdown}`}
onClick={() => setDropdownMenuStatus(true)}
aria-label="Open menu"
aria-expanded={dropdownMenuStatus}

View File

@ -116,7 +116,7 @@ const MemberModal = (props: Props) => {
closeButton={
<button
data-testid="membersFormBtn"
className="btn btn-sm btn-secondary"
className="btn btn-sm btn-outline-secondary"
type="button"
disabled={isSending}
onClick={submitForm}

View File

@ -65,7 +65,7 @@ exports[`Member Card - members section creates snapshot 1`] = `
>
<button
aria-label="Action"
class="dropdown-item btn btn-sm rounded-0 text-secondary"
class="dropdown-item btn btn-sm rounded-0 text-dark"
data-testid="leaveOrRemoveModalBtn"
type="button"
>
@ -96,7 +96,7 @@ exports[`Member Card - members section creates snapshot 1`] = `
<button
aria-expanded="false"
aria-label="Open menu"
class="btn btn-light p-0 text-secondary text-center btnDropdown"
class="btn p-0 text-primary text-center iconSubsWrapper btnDropdown"
>
<svg
fill="currentColor"

View File

@ -111,7 +111,7 @@ exports[`Members Modal - members section creates snapshot 1`] = `
>
<button
aria-label="Invite member"
class="btn btn-sm btn-secondary"
class="btn btn-sm btn-outline-secondary"
data-testid="membersFormBtn"
type="button"
>

View File

@ -82,7 +82,7 @@ exports[`UserInvitation creates snapshot 1`] = `
>
<button
aria-label="Close modal"
class="btn btn-sm btn-secondary text-uppercase"
class="btn btn-sm btn-outline-secondary text-uppercase"
data-testid="closeModalFooterBtn"
disabled=""
type="button"

View File

@ -24,7 +24,7 @@ exports[`Members section index creates snapshot 1`] = `
>
<button
aria-label="Action"
class="btn btn-secondary btn-sm text-uppercase btnAction"
class="btn btn-outline-secondary btn-sm text-uppercase btnAction"
data-testid="addMemberBtn"
type="button"
>

View File

@ -67,7 +67,7 @@ const MembersSection = (props: Props) => {
<div>
<ActionBtn
testId="addMemberBtn"
className={`btn btn-secondary btn-sm text-uppercase ${styles.btnAction}`}
className={`btn btn-outline-secondary btn-sm text-uppercase ${styles.btnAction}`}
contentClassName="justify-content-center"
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
@ -98,7 +98,7 @@ const MembersSection = (props: Props) => {
<button
type="button"
className="btn btn-secondary"
className="btn btn-outline-secondary"
onClick={() => setModalMemberOpen(true)}
data-testid="addFirstMemberBtn"
aria-label="Open modal"

View File

@ -6,8 +6,8 @@
height: 1.75rem;
width: 1.75rem;
border-radius: 50% !important;
background-color: var(--color-1-10) !important;
line-height: 1rem;
border-color: var(--color-1-500);
line-height: 0.85rem;
}
.dropdownMenu {

View File

@ -138,7 +138,7 @@ const OrganizationCard = (props: Props) => {
{isMember && props.organization.membersCount && props.organization.membersCount > 1 && (
<button
data-testid="leaveOrgModalBtn"
className="dropdown-item btn btn-sm rounded-0 text-secondary"
className="dropdown-item btn btn-sm rounded-0 text-dark"
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
closeDropdown();
@ -157,7 +157,7 @@ const OrganizationCard = (props: Props) => {
<div>
<button
data-testid="acceptInvitationBtn"
className="dropdown-item btn btn-sm rounded-0 text-secondary"
className="dropdown-item btn btn-sm rounded-0 text-dark"
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
confirmOrganizationMembership();
@ -186,7 +186,7 @@ const OrganizationCard = (props: Props) => {
{hasDropdownContent && (
<button
className={`ml-3 btn btn-light p-0 text-secondary text-center ${styles.btnDropdown}`}
className={`ml-3 mb-2 btn p-0 text-primary text-center iconSubsWrapper ${styles.btnDropdown}`}
onClick={() => setDropdownMenuStatus(true)}
aria-label="Open menu"
aria-expanded={dropdownMenuStatus}
@ -203,7 +203,7 @@ const OrganizationCard = (props: Props) => {
closeButton={
<>
<button
className={`btn btn-sm btn-light text-uppercase ${styles.btnLight}`}
className="btn btn-sm btn-outline-secondary text-uppercase"
onClick={() => setLeaveModalStatus(false)}
aria-label="Close modal"
>

View File

@ -42,7 +42,7 @@ const OrganizationModal = (props: Props) => {
modalClassName={styles.modal}
closeButton={
<button
className="btn btn-sm btn-secondary"
className="btn btn-sm btn-outline-secondary"
type="button"
disabled={isSending}
onClick={submitForm}

View File

@ -56,7 +56,7 @@ exports[`Organization Card - organization section creates snapshot 1`] = `
/>
<button
aria-label="Open modal"
class="dropdown-item btn btn-sm rounded-0 text-secondary"
class="dropdown-item btn btn-sm rounded-0 text-dark"
data-testid="leaveOrgModalBtn"
>
<div
@ -85,7 +85,7 @@ exports[`Organization Card - organization section creates snapshot 1`] = `
<button
aria-expanded="false"
aria-label="Open menu"
class="ml-3 btn btn-light p-0 text-secondary text-center btnDropdown"
class="ml-3 mb-2 btn p-0 text-primary text-center iconSubsWrapper btnDropdown"
>
<svg
fill="currentColor"

View File

@ -221,7 +221,7 @@ exports[`OrganizationModal - organizations section creates snapshot 1`] = `
>
<button
aria-label="Update organization"
class="btn btn-sm btn-secondary"
class="btn btn-sm btn-outline-secondary"
type="button"
>
<div

View File

@ -21,7 +21,7 @@ exports[`Organizations section index creates snapshot 1`] = `
<div>
<button
aria-label="Open modal"
class="btn btn-secondary btn-sm text-uppercase btnAction"
class="btn btn-outline-secondary btn-sm text-uppercase btnAction"
data-testid="addOrgButton"
>
<div
@ -132,7 +132,7 @@ exports[`Organizations section index creates snapshot 1`] = `
>
<button
aria-label="Add organization"
class="btn btn-sm btn-secondary"
class="btn btn-sm btn-outline-secondary"
type="button"
>
<div

View File

@ -62,7 +62,7 @@ const OrganizationsSection = (props: Props) => {
<div>
<button
data-testid="addOrgButton"
className={`btn btn-secondary btn-sm text-uppercase ${styles.btnAction}`}
className={`btn btn-outline-secondary btn-sm text-uppercase ${styles.btnAction}`}
onClick={() => setModalStatus({ open: true })}
aria-label="Open modal"
>
@ -88,7 +88,7 @@ const OrganizationsSection = (props: Props) => {
<button
data-testid="addFirstOrgBtn"
type="button"
className="btn btn-secondary"
className="btn btn-outline-secondary"
onClick={() => setModalStatus({ open: true })}
aria-label="Open modal"
>

View File

@ -46,7 +46,7 @@ describe('Badge Modal - repositories section', () => {
);
expect(
getByText(
`[![Artifact HUB](https://img.shields.io/endpoint?url=http://localhost/badge/repository/${repoMock.name})](http://localhost/packages/search?repo=${repoMock.name})`
`[![null](https://img.shields.io/endpoint?url=http://localhost/badge/repository/${repoMock.name})](http://localhost/packages/search?repo=${repoMock.name})`
)
).toBeInTheDocument();
});
@ -67,7 +67,7 @@ describe('Badge Modal - repositories section', () => {
);
expect(
getByText(
`http://localhost/packages/search?repo=${repoMock.name}[image:https://img.shields.io/endpoint?url=http://localhost/badge/repository/${repoMock.name}[Artifact HUB]]`
`http://localhost/packages/search?repo=${repoMock.name}[image:https://img.shields.io/endpoint?url=http://localhost/badge/repository/${repoMock.name}[null]]`
)
).toBeInTheDocument();
});

View File

@ -3,6 +3,7 @@ import SyntaxHighlighter from 'react-syntax-highlighter';
import { docco } from 'react-syntax-highlighter/dist/cjs/styles/hljs';
import { Repository } from '../../../types';
import getMetaTag from '../../../utils/getMetaTag';
import ButtonCopyToClipboard from '../../common/ButtonCopyToClipboard';
import Modal from '../../common/Modal';
import Tabs from '../../common/Tabs';
@ -15,10 +16,11 @@ interface Props {
}
const BadgeModal = (props: Props) => {
const siteName = getMetaTag('siteName');
const origin = window.location.origin;
const badgeImage = `https://img.shields.io/endpoint?url=${origin}/badge/repository/${props.repository.name}`;
const markdownLink = `[![Artifact HUB](${badgeImage})](${origin}/packages/search?repo=${props.repository.name})`;
const asciiLink = `${origin}/packages/search?repo=${props.repository.name}[image:${badgeImage}[Artifact HUB]]`;
const markdownLink = `[![${siteName}](${badgeImage})](${origin}/packages/search?repo=${props.repository.name})`;
const asciiLink = `${origin}/packages/search?repo=${props.repository.name}[image:${badgeImage}[${siteName}]]`;
const onCloseModal = () => {
props.onClose();

View File

@ -15,8 +15,9 @@
height: 1.75rem;
width: 1.75rem;
border-radius: 50% !important;
background-color: var(--color-1-10) !important;
line-height: 1rem;
background-color: var(--white) !important;
border-color: var(--color-1-500);
line-height: 0.85rem;
}
.dropdownMenu {

View File

@ -111,7 +111,7 @@ const RepositoryCard = (props: Props) => {
<Modal
modalDialogClassName={styles.modalDialog}
className={`d-inline-block ${styles.modal}`}
buttonType={`ml-1 btn badge btn-secondary ${styles.btn}`}
buttonType={`ml-1 btn badge btn-outline-secondary ${styles.btn}`}
buttonContent={
<div className="d-flex flex-row align-items-center">
<HiExclamation className="mr-2" />
@ -207,7 +207,7 @@ const RepositoryCard = (props: Props) => {
<Modal
modalDialogClassName={styles.modalDialog}
className={`d-inline-block ${styles.modal}`}
buttonType={`ml-1 btn badge btn-secondary ${styles.btn}`}
buttonType={`ml-1 btn badge btn-outline-secondary ${styles.btn}`}
buttonContent={
<div className="d-flex flex-row align-items-center">
<HiExclamation className="mr-2" />
@ -331,7 +331,7 @@ const RepositoryCard = (props: Props) => {
<button
data-testid="getBadgeBtn"
className="dropdown-item btn btn-sm rounded-0 text-secondary"
className="dropdown-item btn btn-sm rounded-0 text-dark"
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
closeDropdown();
@ -347,7 +347,7 @@ const RepositoryCard = (props: Props) => {
<ActionBtn
testId="transferRepoBtn"
className="dropdown-item btn btn-sm rounded-0 text-secondary"
className="dropdown-item btn btn-sm rounded-0 text-dark"
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
closeDropdown();
@ -363,7 +363,7 @@ const RepositoryCard = (props: Props) => {
<ActionBtn
testId="updateRepoBtn"
className="dropdown-item btn btn-sm rounded-0 text-secondary"
className="dropdown-item btn btn-sm rounded-0 text-dark"
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
closeDropdown();
@ -382,7 +382,7 @@ const RepositoryCard = (props: Props) => {
<ActionBtn
testId="deleteRepoDropdownBtn"
className="dropdown-item btn btn-sm rounded-0 text-secondary"
className="dropdown-item btn btn-sm rounded-0 text-dark"
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
closeDropdown();
@ -398,7 +398,7 @@ const RepositoryCard = (props: Props) => {
</div>
<button
className={`btn btn-light p-0 text-secondary text-center ${styles.btnDropdown}`}
className={`btn p-0 text-primary text-center iconSubsWrapper ${styles.btnDropdown}`}
onClick={() => setDropdownMenuStatus(true)}
aria-label="Open menu"
aria-expanded={dropdownMenuStatus}
@ -417,7 +417,7 @@ const RepositoryCard = (props: Props) => {
<div className={`position-absolute ${styles.copyBtnWrapper}`}>
<ButtonCopyToClipboard
text={props.repository.repositoryId}
className="btn-link border-0 text-secondary font-weight-bold"
className="btn-link border-0 text-dark font-weight-bold"
label="Copy repository ID to clipboard"
/>
</div>

View File

@ -120,7 +120,7 @@ describe('Claim Repository Modal - repositories section', () => {
expect(getByTestId('select_claim_orgs')).toBeInTheDocument();
expect(getByTestId('claimRepoBtn')).toBeInTheDocument();
expect(
getByText(/Please make sure the email used in the metatata file matches with the one you use in Artifact Hub./g)
getByText(/Please make sure the email used in the metatata file matches with the one you use in/g)
).toBeInTheDocument();
expect(getByText('It may take a few minutes for this change to be visible across the Hub.')).toBeInTheDocument();
@ -157,7 +157,7 @@ describe('Claim Repository Modal - repositories section', () => {
expect(getByTestId('select_claim_orgs')).toBeInTheDocument();
expect(getByTestId('claimRepoBtn')).toBeInTheDocument();
expect(
getByText(/Please make sure the email used in the metatata file matches with the one you use in Artifact Hub./g)
getByText(/Please make sure the email used in the metatata file matches with the one you use in/g)
).toBeInTheDocument();
expect(getByText('It may take a few minutes for this change to be visible across the Hub.')).toBeInTheDocument();

View File

@ -11,6 +11,7 @@ import { AppCtx } from '../../../context/AppCtx';
import { ErrorKind, Organization, Repository } from '../../../types';
import compoundErrorMessage from '../../../utils/compoundErrorMessage';
import { OCI_PREFIX } from '../../../utils/data';
import getMetaTag from '../../../utils/getMetaTag';
import ExternalLink from '../../common/ExternalLink';
import Modal from '../../common/Modal';
import RepositoryIcon from '../../common/RepositoryIcon';
@ -26,6 +27,7 @@ interface Props {
const ClaimRepositoryOwnerShipModal = (props: Props) => {
const { ctx } = useContext(AppCtx);
const siteName = getMetaTag('siteName');
const form = useRef<HTMLFormElement>(null);
const [isFetchingOrgs, setIsFetchingOrgs] = useState(false);
const [isSending, setIsSending] = useState(false);
@ -181,7 +183,7 @@ const ClaimRepositoryOwnerShipModal = (props: Props) => {
closeButton={
<button
data-testid="claimRepoBtn"
className="btn btn-sm btn-secondary"
className="btn btn-sm btn-outline-secondary"
type="button"
disabled={isSending || isNull(repoItem)}
onClick={submitForm}
@ -219,7 +221,7 @@ const ClaimRepositoryOwnerShipModal = (props: Props) => {
</ExternalLink>{' '}
to your repository and include yourself (or the person who will do the request) as an owner. This will be
checked during the ownership claim process. Please make sure the email used in the metatata file matches
with the one you use in Artifact Hub.
with the one you use in {siteName}.
</p>
</div>
<form
@ -299,11 +301,11 @@ const ClaimRepositoryOwnerShipModal = (props: Props) => {
<label id="claiming" className={`font-weight-bold ${styles.label}`}>
Transfer to:
</label>
<div className="form-check mb-2">
<div className="custom-control custom-radio mb-2">
<input
aria-labelledby="claiming user"
data-testid="radio_claim_user"
className="form-check-input"
className="custom-control-input"
type="radio"
name="claim"
id="user"
@ -312,16 +314,16 @@ const ClaimRepositoryOwnerShipModal = (props: Props) => {
onChange={() => handleClaimingFromOpt('user')}
required
/>
<label id="user" className={`form-check-label ${styles.label}`} htmlFor="user">
<label id="user" className={`custom-control-label ${styles.label}`} htmlFor="user">
My user
</label>
</div>
<div className="form-check mb-3">
<div className="custom-control custom-radio mb-3">
<input
aria-labelledby="claiming org"
data-testid="radio_claim_org"
className="form-check-input"
className="custom-control-input"
type="radio"
name="claim"
id="org"
@ -330,7 +332,7 @@ const ClaimRepositoryOwnerShipModal = (props: Props) => {
onChange={() => handleClaimingFromOpt('org')}
required
/>
<label id="org" className={`form-check-label ${styles.label}`} htmlFor="org">
<label id="org" className={`custom-control-label ${styles.label}`} htmlFor="org">
Organization
</label>
</div>

View File

@ -55,7 +55,7 @@ const DeletionModal = (props: Props) => {
closeButton={
<>
<button
className={`btn btn-sm btn-light text-uppercase ${styles.btnLight}`}
className="btn btn-sm btn-outline-secondary text-uppercase"
onClick={() => props.setDeletionModalStatus(false)}
aria-label="Cancel"
>

View File

@ -11,6 +11,7 @@ import { AppCtx } from '../../../context/AppCtx';
import { ErrorKind, RefInputField, Repository, RepositoryKind, ResourceKind } from '../../../types';
import compoundErrorMessage from '../../../utils/compoundErrorMessage';
import { OCI_PREFIX, RepoKindDef, REPOSITORY_KINDS } from '../../../utils/data';
import getMetaTag from '../../../utils/getMetaTag';
import ExternalLink from '../../common/ExternalLink';
import InputField from '../../common/InputField';
import Modal from '../../common/Modal';
@ -58,9 +59,8 @@ const RepositoryModal = (props: Props) => {
setUrlContainsTreeTxt(e.target.value.includes('/tree/'));
};
const allowPrivateRepositories: boolean = document.querySelector(`meta[name='artifacthub:allowPrivateRepositories']`)
? document.querySelector(`meta[name='artifacthub:allowPrivateRepositories']`)!.getAttribute('content') === 'true'
: false;
const allowPrivateRepositories: boolean = getMetaTag('allowPrivateRepositories', true);
const siteName = getMetaTag('siteName');
// Clean API error when form is focused after validation
const cleanApiError = () => {
@ -351,7 +351,11 @@ const RepositoryModal = (props: Props) => {
<button
data-testid="confirmDisabledRepo"
type="button"
className={classnames('btn btn-sm ml-3', { 'btn-dark': !isValidInput }, { 'btn-danger': isValidInput })}
className={classnames(
'btn btn-sm ml-3',
{ 'btn-outline-secondary': !isValidInput },
{ 'btn-danger': isValidInput }
)}
onClick={(e) => {
e.preventDefault();
setIsDisabled(!isDisabled);
@ -366,7 +370,7 @@ const RepositoryModal = (props: Props) => {
) : (
<button
data-testid="repoBtn"
className="btn btn-sm btn-secondary"
className="btn btn-sm btn-outline-secondary"
type="button"
disabled={isSending || visibleDisabledConfirmation}
onClick={submitForm}
@ -417,7 +421,7 @@ const RepositoryModal = (props: Props) => {
<p>
You can enable back your repository at any time and the information available in the source repository
will be indexed and made available in Artifact Hub again.
will be indexed and made available in {siteName} again.
</p>
<p>

View File

@ -122,7 +122,7 @@ const TransferRepositoryModal = (props: Props) => {
closeButton={
<button
data-testid="transferRepoBtn"
className="btn btn-sm btn-secondary"
className="btn btn-sm btn-outline-secondary"
type="button"
disabled={isSending}
onClick={submitForm}
@ -156,10 +156,10 @@ const TransferRepositoryModal = (props: Props) => {
>
{!isUndefined(organizationName) ? (
<>
<div className="form-check mb-3">
<div className="custom-control custom-radio mb-3">
<input
data-testid="radio_user"
className="form-check-input"
className="custom-control-input"
type="radio"
name="transfer"
id="user"
@ -168,15 +168,15 @@ const TransferRepositoryModal = (props: Props) => {
onChange={() => setSelectedTransferOption('user')}
required
/>
<label className={`form-check-label font-weight-bold ${styles.label}`} htmlFor="user">
<label className={`custom-control-label font-weight-bold ${styles.label}`} htmlFor="user">
Transfer to my user
</label>
</div>
<div className="form-check mb-3">
<div className="custom-control custom-radio mb-3">
<input
data-testid="radio_org"
className="form-check-input"
className="custom-control-input"
type="radio"
name="transfer"
id="org"
@ -185,7 +185,7 @@ const TransferRepositoryModal = (props: Props) => {
onChange={() => setSelectedTransferOption('org')}
required
/>
<label className={`form-check-label font-weight-bold ${styles.label}`} htmlFor="org">
<label className={`custom-control-label font-weight-bold ${styles.label}`} htmlFor="org">
Transfer to organization
</label>
</div>

View File

@ -154,7 +154,7 @@ exports[`Badge Modal - repositories section creates snapshot 1`] = `
style="white-space: pre;"
>
<span>
[![Artifact HUB](https://img.shields.io/endpoint?url=http://localhost/badge/repository/repoTest)](http://localhost/packages/search?repo=repoTest)
[![null](https://img.shields.io/endpoint?url=http://localhost/badge/repository/repoTest)](http://localhost/packages/search?repo=repoTest)
</span>
</code>
</pre>
@ -173,7 +173,7 @@ exports[`Badge Modal - repositories section creates snapshot 1`] = `
>
<button
aria-label="Close modal"
class="btn btn-sm btn-secondary text-uppercase"
class="btn btn-sm btn-outline-secondary text-uppercase"
data-testid="closeModalFooterBtn"
type="button"
>

View File

@ -63,7 +63,7 @@ exports[`Repository Card - packages section creates snapshot 1`] = `
/>
<button
aria-label="Open modal"
class="dropdown-item btn btn-sm rounded-0 text-secondary"
class="dropdown-item btn btn-sm rounded-0 text-dark"
data-testid="getBadgeBtn"
>
<div
@ -93,7 +93,7 @@ exports[`Repository Card - packages section creates snapshot 1`] = `
>
<button
aria-label="Action"
class="dropdown-item btn btn-sm rounded-0 text-secondary"
class="dropdown-item btn btn-sm rounded-0 text-dark"
data-testid="transferRepoBtn"
type="button"
>
@ -131,7 +131,7 @@ exports[`Repository Card - packages section creates snapshot 1`] = `
>
<button
aria-label="Action"
class="dropdown-item btn btn-sm rounded-0 text-secondary"
class="dropdown-item btn btn-sm rounded-0 text-dark"
data-testid="updateRepoBtn"
type="button"
>
@ -163,7 +163,7 @@ exports[`Repository Card - packages section creates snapshot 1`] = `
>
<button
aria-label="Action"
class="dropdown-item btn btn-sm rounded-0 text-secondary"
class="dropdown-item btn btn-sm rounded-0 text-dark"
data-testid="deleteRepoDropdownBtn"
type="button"
>
@ -194,7 +194,7 @@ exports[`Repository Card - packages section creates snapshot 1`] = `
<button
aria-expanded="false"
aria-label="Open menu"
class="btn btn-light p-0 text-secondary text-center btnDropdown"
class="btn p-0 text-primary text-center iconSubsWrapper btnDropdown"
>
<svg
fill="currentColor"

View File

@ -63,7 +63,7 @@ exports[`Claim Repository Modal - repositories section creates snapshot 1`] = `
metadata file
</u>
</a>
to your repository and include yourself (or the person who will do the request) as an owner. This will be checked during the ownership claim process. Please make sure the email used in the metatata file matches with the one you use in Artifact Hub.
to your repository and include yourself (or the person who will do the request) as an owner. This will be checked during the ownership claim process. Please make sure the email used in the metatata file matches with the one you use in .
</p>
</div>
<form
@ -132,11 +132,11 @@ exports[`Claim Repository Modal - repositories section creates snapshot 1`] = `
Transfer to:
</label>
<div
class="form-check mb-2"
class="custom-control custom-radio mb-2"
>
<input
aria-labelledby="claiming user"
class="form-check-input"
class="custom-control-input"
data-testid="radio_claim_user"
id="user"
name="claim"
@ -145,7 +145,7 @@ exports[`Claim Repository Modal - repositories section creates snapshot 1`] = `
value="user"
/>
<label
class="form-check-label label"
class="custom-control-label label"
for="user"
id="user"
>
@ -153,12 +153,12 @@ exports[`Claim Repository Modal - repositories section creates snapshot 1`] = `
</label>
</div>
<div
class="form-check mb-3"
class="custom-control custom-radio mb-3"
>
<input
aria-labelledby="claiming org"
checked=""
class="form-check-input"
class="custom-control-input"
data-testid="radio_claim_org"
id="org"
name="claim"
@ -167,7 +167,7 @@ exports[`Claim Repository Modal - repositories section creates snapshot 1`] = `
value="org"
/>
<label
class="form-check-label label"
class="custom-control-label label"
for="org"
id="org"
>
@ -222,7 +222,7 @@ exports[`Claim Repository Modal - repositories section creates snapshot 1`] = `
>
<button
aria-label="Claim ownership"
class="btn btn-sm btn-secondary"
class="btn btn-sm btn-outline-secondary"
data-testid="claimRepoBtn"
disabled=""
type="button"

View File

@ -110,7 +110,7 @@ exports[`Deletion modal Modal - packages section creates snapshot 1`] = `
>
<button
aria-label="Cancel"
class="btn btn-sm btn-light text-uppercase btnLight"
class="btn btn-sm btn-outline-secondary text-uppercase"
>
<div
class="d-flex flex-row align-items-center"

View File

@ -337,7 +337,7 @@ exports[`Repository Modal - repositories section creates snapshot 1`] = `
>
<button
aria-label="Add repository"
class="btn btn-sm btn-secondary"
class="btn btn-sm btn-outline-secondary"
data-testid="repoBtn"
type="button"
>

View File

@ -52,11 +52,11 @@ exports[`Transfer Repository Modal - packages section creates snapshot 1`] = `
novalidate=""
>
<div
class="form-check mb-3"
class="custom-control custom-radio mb-3"
>
<input
checked=""
class="form-check-input"
class="custom-control-input"
data-testid="radio_user"
id="user"
name="transfer"
@ -65,17 +65,17 @@ exports[`Transfer Repository Modal - packages section creates snapshot 1`] = `
value="user"
/>
<label
class="form-check-label font-weight-bold label"
class="custom-control-label font-weight-bold label"
for="user"
>
Transfer to my user
</label>
</div>
<div
class="form-check mb-3"
class="custom-control custom-radio mb-3"
>
<input
class="form-check-input"
class="custom-control-input"
data-testid="radio_org"
id="org"
name="transfer"
@ -84,7 +84,7 @@ exports[`Transfer Repository Modal - packages section creates snapshot 1`] = `
value="org"
/>
<label
class="form-check-label font-weight-bold label"
class="custom-control-label font-weight-bold label"
for="org"
>
Transfer to organization
@ -137,7 +137,7 @@ exports[`Transfer Repository Modal - packages section creates snapshot 1`] = `
>
<button
aria-label="Transfer repository"
class="btn btn-sm btn-secondary"
class="btn btn-sm btn-outline-secondary"
data-testid="transferRepoBtn"
type="button"
>

View File

@ -21,7 +21,7 @@ exports[`Repository index creates snapshot 1`] = `
<div>
<button
aria-label="Refresh repositories list"
class="btn btn-secondary btn-sm text-uppercase mr-0 mr-md-2 btnAction"
class="btn btn-outline-secondary btn-sm text-uppercase mr-0 mr-md-2 btnAction"
data-testid="refreshRepoBtn"
>
<div
@ -64,7 +64,7 @@ exports[`Repository index creates snapshot 1`] = `
</button>
<button
aria-label="Open claim repository modal"
class="btn btn-secondary btn-sm text-uppercase mr-0 mr-md-2 btnAction"
class="btn btn-outline-secondary btn-sm text-uppercase mr-0 mr-md-2 btnAction"
data-testid="claimRepoBtn"
>
<div
@ -102,7 +102,7 @@ exports[`Repository index creates snapshot 1`] = `
>
<button
aria-label="Action"
class="btn btn-secondary btn-sm text-uppercase btnAction"
class="btn btn-outline-secondary btn-sm text-uppercase btnAction"
data-testid="addRepoBtn"
type="button"
>

View File

@ -91,7 +91,7 @@ const RepositoriesSection = (props: Props) => {
<div>
<button
data-testid="refreshRepoBtn"
className={`btn btn-secondary btn-sm text-uppercase mr-0 mr-md-2 ${styles.btnAction}`}
className={`btn btn-outline-secondary btn-sm text-uppercase mr-0 mr-md-2 ${styles.btnAction}`}
onClick={fetchRepositories}
aria-label="Refresh repositories list"
>
@ -104,7 +104,7 @@ const RepositoriesSection = (props: Props) => {
<button
data-testid="claimRepoBtn"
className={`btn btn-secondary btn-sm text-uppercase mr-0 mr-md-2 ${styles.btnAction}`}
className={`btn btn-outline-secondary btn-sm text-uppercase mr-0 mr-md-2 ${styles.btnAction}`}
onClick={() => setOpenClaimRepo(true)}
aria-label="Open claim repository modal"
>
@ -116,7 +116,7 @@ const RepositoriesSection = (props: Props) => {
<ActionBtn
testId="addRepoBtn"
className={`btn btn-secondary btn-sm text-uppercase ${styles.btnAction}`}
className={`btn btn-outline-secondary btn-sm text-uppercase ${styles.btnAction}`}
contentClassName="justify-content-center"
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
@ -180,7 +180,7 @@ const RepositoriesSection = (props: Props) => {
<ActionBtn
testId="addFirstRepoBtn"
className="btn btn-secondary"
className="btn btn-outline-secondary"
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
setModalStatus({ open: true });

View File

@ -33,7 +33,7 @@ exports[`Authorization settings index creates snapshot 1`] = `
class="mt-4 mt-md-5"
>
<p>
Artifact Hub allows you to setup fine-grained access control based on authorization policies. Authorization polices are written in
allows you to setup fine-grained access control based on authorization policies. Authorization polices are written in
<a
aria-label="Open rego documentation"
class="link text-reset link"

View File

@ -20,6 +20,7 @@ import authorizer from '../../../../../utils/authorizer';
import { checkUnsavedPolicyChanges, PolicyChangeAction } from '../../../../../utils/checkUnsavedPolicyChanges';
import compoundErrorMessage from '../../../../../utils/compoundErrorMessage';
import { PREDEFINED_POLICIES } from '../../../../../utils/data';
import getMetaTag from '../../../../../utils/getMetaTag';
import isValidJSON from '../../../../../utils/isValidJSON';
import prepareRegoPolicyForPlayground from '../../../../../utils/prepareRegoPolicyForPlayground';
import stringifyPolicyData from '../../../../../utils/stringifyPolicyData';
@ -62,6 +63,7 @@ const DEFAULT_POLICY_NAME = 'rbac.v1';
const AuthorizationSection = (props: Props) => {
const { ctx, dispatch } = useContext(AppCtx);
const siteName = getMetaTag('siteName');
const updateActionBtn = useRef<RefActionBtn>(null);
const [apiError, setApiError] = useState<string | JSX.Element | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
@ -396,7 +398,7 @@ const AuthorizationSection = (props: Props) => {
<div className="mt-4 mt-md-5" onClick={() => setApiError(null)}>
<p>
Artifact Hub allows you to setup fine-grained access control based on authorization policies. Authorization
{siteName} allows you to setup fine-grained access control based on authorization policies. Authorization
polices are written in{' '}
<ExternalLink
href="https://www.openpolicyagent.org/docs/latest/#rego"
@ -580,7 +582,7 @@ const AuthorizationSection = (props: Props) => {
<ActionBtn
ref={updateActionBtn}
testId="updateAuthorizationPolicyBtn"
className="btn btn-sm btn-secondary"
className="btn btn-sm btn-outline-secondary"
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
onSaveAuthorizationPolicy();
@ -617,7 +619,7 @@ const AuthorizationSection = (props: Props) => {
<>
<button
data-testid="modalCancelBtn"
className={`btn btn-sm btn-light text-uppercase ${styles.btnLight}`}
className="btn btn-sm btn-outline-secondary text-uppercase"
onClick={() => setConfirmationModal({ open: false })}
aria-label="Cancel"
>
@ -626,7 +628,7 @@ const AuthorizationSection = (props: Props) => {
<button
data-testid="modalOKBtn"
className="btn btn-sm btn-primary text-uppercase ml-3"
className="btn btn-sm btn-outline-secondary text-uppercase ml-3"
onClick={(e) => {
e.preventDefault();
confirmationModal.onConfirm!();

View File

@ -79,7 +79,7 @@ const DeleteOrganization = (props: Props) => {
closeButton={
<>
<button
className={`btn btn-sm btn-light text-uppercase ${styles.btnLight}`}
className="btn btn-sm btn-outline-secondary text-uppercase"
onClick={() => setOpenStatus(false)}
aria-label="Close"
>

View File

@ -38,7 +38,7 @@ const UpdateOrganization = (props: Props) => {
<div className="mt-4">
<ActionBtn
testId="updateOrgBtn"
className="btn btn-sm btn-secondary"
className="btn btn-sm btn-outline-secondary"
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
submitForm();

View File

@ -103,7 +103,7 @@ exports[`DeleteOrg creates snapshot 1`] = `
>
<button
aria-label="Close"
class="btn btn-sm btn-light text-uppercase btnLight"
class="btn btn-sm btn-outline-secondary text-uppercase"
>
<div
class="d-flex flex-row align-items-center"

View File

@ -173,7 +173,7 @@ exports[`Organization settings index creates snapshot 1`] = `
>
<button
aria-label="Action"
class="btn btn-sm btn-secondary"
class="btn btn-sm btn-outline-secondary"
data-testid="updateOrgBtn"
type="button"
>

View File

@ -6,8 +6,9 @@
height: 1.75rem;
width: 1.75rem;
border-radius: 50% !important;
background-color: var(--color-1-10) !important;
line-height: 1rem;
background-color: var(--white) !important;
border-color: var(--color-1-500);
line-height: 0.85rem;
}
.dropdownMenu {

View File

@ -68,7 +68,7 @@ const APIKeyCard = (props: Props) => {
closeButton={
<>
<button
className={`btn btn-sm btn-light text-uppercase ${styles.btnLight}`}
className="btn btn-sm btn-outline-secondary text-uppercase"
onClick={() => setDeletionModalStatus(false)}
aria-label="Cancel"
>
@ -126,7 +126,7 @@ const APIKeyCard = (props: Props) => {
<button
data-testid="updateAPIKeyBtn"
className="dropdown-item btn btn-sm rounded-0 text-secondary"
className="dropdown-item btn btn-sm rounded-0 text-dark"
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
closeDropdown();
@ -145,7 +145,7 @@ const APIKeyCard = (props: Props) => {
<button
data-testid="deleteAPIKeyModalBtn"
className="dropdown-item btn btn-sm rounded-0 text-secondary"
className="dropdown-item btn btn-sm rounded-0 text-dark"
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
closeDropdown();
@ -161,7 +161,7 @@ const APIKeyCard = (props: Props) => {
</div>
<button
className={`btn btn-light p-0 text-secondary text-center ${styles.btnDropdown}`}
className={`btn p-0 text-primary text-center iconSubsWrapper ${styles.btnDropdown}`}
onClick={() => setDropdownMenuStatus(true)}
aria-label="Open menu"
aria-expanded={dropdownMenuStatus}
@ -180,7 +180,7 @@ const APIKeyCard = (props: Props) => {
<div className={`position-absolute ${styles.copyBtnWrapper}`}>
<ButtonCopyToClipboard
text={props.apiKey.apiKeyId!}
className="btn-link border-0 text-secondary font-weight-bold"
className="btn-link border-0 text-dark font-weight-bold"
label="Copy API key ID to clipboard"
/>
</div>

View File

@ -138,7 +138,7 @@ const APIKeyModal = (props: Props) => {
const sendBtn = (
<button
data-testid="apiKeyFormBtn"
className="btn btn-sm btn-secondary"
className="btn btn-sm btn-outline-secondary"
type="button"
disabled={isSending}
onClick={submitForm}

View File

@ -31,7 +31,7 @@ exports[`API key Card - API keys section creates snapshot 1`] = `
/>
<button
aria-label="Open API key modal"
class="dropdown-item btn btn-sm rounded-0 text-secondary"
class="dropdown-item btn btn-sm rounded-0 text-dark"
data-testid="updateAPIKeyBtn"
>
<div
@ -58,7 +58,7 @@ exports[`API key Card - API keys section creates snapshot 1`] = `
</button>
<button
aria-label="Open deletion modal"
class="dropdown-item btn btn-sm rounded-0 text-secondary"
class="dropdown-item btn btn-sm rounded-0 text-dark"
data-testid="deleteAPIKeyModalBtn"
>
<div
@ -87,7 +87,7 @@ exports[`API key Card - API keys section creates snapshot 1`] = `
<button
aria-expanded="false"
aria-label="Open menu"
class="btn btn-light p-0 text-secondary text-center btnDropdown"
class="btn p-0 text-primary text-center iconSubsWrapper btnDropdown"
>
<svg
fill="currentColor"
@ -133,7 +133,7 @@ exports[`API key Card - API keys section creates snapshot 1`] = `
>
<button
aria-label="Copy API key ID to clipboard"
class="btn btn-sm btn-link border-0 text-secondary font-weight-bold"
class="btn btn-sm btn-link border-0 text-dark font-weight-bold"
data-testid="ctcBtn"
type="button"
>

View File

@ -100,7 +100,7 @@ exports[`APIKeyModal - API keys section creates snapshot 1`] = `
>
<button
aria-label="Add API key"
class="btn btn-sm btn-secondary"
class="btn btn-sm btn-outline-secondary"
data-testid="apiKeyFormBtn"
type="button"
>

View File

@ -41,7 +41,7 @@ exports[`API keys section index creates snapshot 1`] = `
<div>
<button
aria-label="Open modal to add API key"
class="btn btn-secondary btn-sm text-uppercase btnAction"
class="btn btn-outline-secondary btn-sm text-uppercase btnAction"
data-testid="addAPIKeyBtn"
>
<div
@ -135,7 +135,7 @@ exports[`API keys section index creates snapshot 1`] = `
>
<button
aria-label="Add API key"
class="btn btn-sm btn-secondary"
class="btn btn-sm btn-outline-secondary"
data-testid="apiKeyFormBtn"
type="button"
>

View File

@ -60,7 +60,7 @@ const APIKeysSection = (props: Props) => {
<div>
<button
data-testid="addAPIKeyBtn"
className={`btn btn-secondary btn-sm text-uppercase ${styles.btnAction}`}
className={`btn btn-outline-secondary btn-sm text-uppercase ${styles.btnAction}`}
onClick={() => setModalStatus({ open: true })}
aria-label="Open modal to add API key"
>
@ -85,7 +85,7 @@ const APIKeysSection = (props: Props) => {
<button
data-testid="addFirstAPIKeyBtn"
type="button"
className="btn btn-secondary"
className="btn btn-outline-secondary"
onClick={() => setModalStatus({ open: true })}
aria-label="Add API key"
>

View File

@ -161,7 +161,7 @@ const UpdatePassword = () => {
<div className="mt-4 mb-2">
<button
data-testid="updatePasswordBtn"
className="btn btn-sm btn-secondary"
className="btn btn-sm btn-outline-secondary"
type="button"
disabled={isSending}
onClick={submitForm}

View File

@ -175,7 +175,7 @@ const UpdateProfile = (props: Props) => {
<div className="mt-4">
<button
className="btn btn-sm btn-secondary"
className="btn btn-sm btn-outline-secondary"
type="button"
disabled={isSending}
onClick={submitForm}

Some files were not shown because too many files have changed in this diff Show More