feat(api/v2): add public instance info endpoint
Add GET /api/v2/info (public — no auth). Extract the /info response type and its assembly out of the v1 handler into pkg/routes/api/shared.BuildInfo() so both API versions return byte-identical info; refactor v1's handler onto it. Add the v2 path to unauthenticatedAPIPaths.
This commit is contained in:
parent
6f3dab53cb
commit
56b1ba47ec
|
|
@ -0,0 +1,164 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package shared
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/license"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/modules/auth/openid"
|
||||
csvmigrator "code.vikunja.io/api/pkg/modules/migration/csv"
|
||||
microsofttodo "code.vikunja.io/api/pkg/modules/migration/microsoft-todo"
|
||||
"code.vikunja.io/api/pkg/modules/migration/ticktick"
|
||||
"code.vikunja.io/api/pkg/modules/migration/todoist"
|
||||
"code.vikunja.io/api/pkg/modules/migration/trello"
|
||||
vikunja_file "code.vikunja.io/api/pkg/modules/migration/vikunja-file"
|
||||
"code.vikunja.io/api/pkg/modules/migration/wekan"
|
||||
"code.vikunja.io/api/pkg/version"
|
||||
)
|
||||
|
||||
// VikunjaInfos holds public information about this Vikunja instance.
|
||||
type VikunjaInfos struct {
|
||||
Version string `json:"version" doc:"The Vikunja version this instance runs."`
|
||||
FrontendURL string `json:"frontend_url" doc:"The publicly configured frontend URL of this instance."`
|
||||
Motd string `json:"motd" doc:"The message of the day, shown to all users."`
|
||||
LinkSharingEnabled bool `json:"link_sharing_enabled" doc:"Whether sharing projects via public links is enabled."`
|
||||
MaxFileSize string `json:"max_file_size" doc:"The maximum allowed upload size, as a human-readable string (e.g. 20MB)."`
|
||||
MaxItemsPerPage int `json:"max_items_per_page" doc:"The maximum number of items a paginated endpoint returns per page."`
|
||||
AvailableMigrators []string `json:"available_migrators" doc:"The migrators enabled on this instance."`
|
||||
TaskAttachmentsEnabled bool `json:"task_attachments_enabled" doc:"Whether task attachments are enabled."`
|
||||
EnabledBackgroundProviders []string `json:"enabled_background_providers" doc:"The project-background providers enabled on this instance (e.g. upload, unsplash)."`
|
||||
TotpEnabled bool `json:"totp_enabled" doc:"Whether TOTP two-factor authentication is enabled."`
|
||||
Legal LegalInfo `json:"legal" doc:"Links to the instance's legal documents."`
|
||||
CaldavEnabled bool `json:"caldav_enabled" doc:"Whether the CalDAV interface is enabled."`
|
||||
AuthInfo AuthInfo `json:"auth" doc:"The authentication methods enabled on this instance."`
|
||||
EmailRemindersEnabled bool `json:"email_reminders_enabled" doc:"Whether email reminders are enabled."`
|
||||
UserDeletionEnabled bool `json:"user_deletion_enabled" doc:"Whether users may delete their own account."`
|
||||
TaskCommentsEnabled bool `json:"task_comments_enabled" doc:"Whether task comments are enabled."`
|
||||
DemoModeEnabled bool `json:"demo_mode_enabled" doc:"Whether this instance runs in demo mode (data is periodically reset)."`
|
||||
WebhooksEnabled bool `json:"webhooks_enabled" doc:"Whether webhooks are enabled."`
|
||||
PublicTeamsEnabled bool `json:"public_teams_enabled" doc:"Whether public teams are enabled."`
|
||||
AllowIconChanges bool `json:"allow_icon_changes" doc:"Whether users may change project icons."`
|
||||
EnabledProFeatures []license.Feature `json:"enabled_pro_features" doc:"The licensed pro features enabled on this instance."`
|
||||
}
|
||||
|
||||
// AuthInfo describes the authentication methods enabled on this instance.
|
||||
type AuthInfo struct {
|
||||
Local LocalAuthInfo `json:"local"`
|
||||
Ldap LdapAuthInfo `json:"ldap"`
|
||||
OpenIDConnect OpenIDAuthInfo `json:"openid_connect"`
|
||||
}
|
||||
|
||||
// LocalAuthInfo describes the local (username/password) authentication method.
|
||||
type LocalAuthInfo struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
RegistrationEnabled bool `json:"registration_enabled"`
|
||||
}
|
||||
|
||||
// LdapAuthInfo describes the LDAP authentication method.
|
||||
type LdapAuthInfo struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
// OpenIDAuthInfo describes the OpenID Connect authentication method.
|
||||
type OpenIDAuthInfo struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Providers []*openid.Provider `json:"providers"`
|
||||
}
|
||||
|
||||
// LegalInfo holds links to the instance's legal documents.
|
||||
type LegalInfo struct {
|
||||
ImprintURL string `json:"imprint_url"`
|
||||
PrivacyPolicyURL string `json:"privacy_policy_url"`
|
||||
}
|
||||
|
||||
// BuildInfo assembles the public instance information returned by GET /info on
|
||||
// both API versions.
|
||||
func BuildInfo() VikunjaInfos {
|
||||
info := VikunjaInfos{
|
||||
Version: version.Version,
|
||||
FrontendURL: config.ServicePublicURL.GetString(),
|
||||
Motd: config.ServiceMotd.GetString(),
|
||||
LinkSharingEnabled: config.ServiceEnableLinkSharing.GetBool(),
|
||||
MaxFileSize: config.FilesMaxSize.GetString(),
|
||||
MaxItemsPerPage: config.ServiceMaxItemsPerPage.GetInt(),
|
||||
TaskAttachmentsEnabled: config.ServiceEnableTaskAttachments.GetBool(),
|
||||
TotpEnabled: config.ServiceEnableTotp.GetBool(),
|
||||
CaldavEnabled: config.ServiceEnableCaldav.GetBool(),
|
||||
EmailRemindersEnabled: config.ServiceEnableEmailReminders.GetBool(),
|
||||
UserDeletionEnabled: config.ServiceEnableUserDeletion.GetBool(),
|
||||
TaskCommentsEnabled: config.ServiceEnableTaskComments.GetBool(),
|
||||
DemoModeEnabled: config.ServiceDemoMode.GetBool(),
|
||||
WebhooksEnabled: config.WebhooksEnabled.GetBool(),
|
||||
PublicTeamsEnabled: config.ServiceEnablePublicTeams.GetBool(),
|
||||
AllowIconChanges: config.ServiceAllowIconChanges.GetBool(),
|
||||
EnabledProFeatures: license.EnabledProFeatures(),
|
||||
AvailableMigrators: []string{
|
||||
(&vikunja_file.FileMigrator{}).Name(),
|
||||
(&ticktick.Migrator{}).Name(),
|
||||
(&wekan.Migrator{}).Name(),
|
||||
(&csvmigrator.Migrator{}).Name(),
|
||||
},
|
||||
Legal: LegalInfo{
|
||||
ImprintURL: config.LegalImprintURL.GetString(),
|
||||
PrivacyPolicyURL: config.LegalPrivacyURL.GetString(),
|
||||
},
|
||||
AuthInfo: AuthInfo{
|
||||
Local: LocalAuthInfo{
|
||||
Enabled: config.AuthLocalEnabled.GetBool(),
|
||||
RegistrationEnabled: config.AuthLocalEnabled.GetBool() && config.ServiceEnableRegistration.GetBool(),
|
||||
},
|
||||
Ldap: LdapAuthInfo{
|
||||
Enabled: config.AuthLdapEnabled.GetBool(),
|
||||
},
|
||||
OpenIDConnect: OpenIDAuthInfo{
|
||||
Enabled: config.AuthOpenIDEnabled.GetBool(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
providers, err := openid.GetAllProviders()
|
||||
if err != nil {
|
||||
log.Errorf("Error while getting openid providers for /info: %s", err)
|
||||
// No return here to not break /info
|
||||
}
|
||||
info.AuthInfo.OpenIDConnect.Providers = providers
|
||||
|
||||
if config.MigrationTodoistEnable.GetBool() {
|
||||
m := &todoist.Migration{}
|
||||
info.AvailableMigrators = append(info.AvailableMigrators, m.Name())
|
||||
}
|
||||
if config.MigrationTrelloEnable.GetBool() {
|
||||
m := &trello.Migration{}
|
||||
info.AvailableMigrators = append(info.AvailableMigrators, m.Name())
|
||||
}
|
||||
if config.MigrationMicrosoftTodoEnable.GetBool() {
|
||||
m := µsofttodo.Migration{}
|
||||
info.AvailableMigrators = append(info.AvailableMigrators, m.Name())
|
||||
}
|
||||
|
||||
if config.BackgroundsEnabled.GetBool() {
|
||||
if config.BackgroundsUploadEnabled.GetBool() {
|
||||
info.EnabledBackgroundProviders = append(info.EnabledBackgroundProviders, "upload")
|
||||
}
|
||||
if config.BackgroundsUnsplashEnabled.GetBool() {
|
||||
info.EnabledBackgroundProviders = append(info.EnabledBackgroundProviders, "unsplash")
|
||||
}
|
||||
}
|
||||
|
||||
return info
|
||||
}
|
||||
|
|
@ -19,151 +19,18 @@ package v1
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/license"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/modules/auth/openid"
|
||||
csvmigrator "code.vikunja.io/api/pkg/modules/migration/csv"
|
||||
microsofttodo "code.vikunja.io/api/pkg/modules/migration/microsoft-todo"
|
||||
"code.vikunja.io/api/pkg/modules/migration/ticktick"
|
||||
"code.vikunja.io/api/pkg/modules/migration/todoist"
|
||||
"code.vikunja.io/api/pkg/modules/migration/trello"
|
||||
vikunja_file "code.vikunja.io/api/pkg/modules/migration/vikunja-file"
|
||||
"code.vikunja.io/api/pkg/modules/migration/wekan"
|
||||
"code.vikunja.io/api/pkg/version"
|
||||
"code.vikunja.io/api/pkg/routes/api/shared"
|
||||
|
||||
"github.com/labstack/echo/v5"
|
||||
)
|
||||
|
||||
type vikunjaInfos struct {
|
||||
Version string `json:"version"`
|
||||
FrontendURL string `json:"frontend_url"`
|
||||
Motd string `json:"motd"`
|
||||
LinkSharingEnabled bool `json:"link_sharing_enabled"`
|
||||
MaxFileSize string `json:"max_file_size"`
|
||||
MaxItemsPerPage int `json:"max_items_per_page"`
|
||||
AvailableMigrators []string `json:"available_migrators"`
|
||||
TaskAttachmentsEnabled bool `json:"task_attachments_enabled"`
|
||||
EnabledBackgroundProviders []string `json:"enabled_background_providers"`
|
||||
TotpEnabled bool `json:"totp_enabled"`
|
||||
Legal legalInfo `json:"legal"`
|
||||
CaldavEnabled bool `json:"caldav_enabled"`
|
||||
AuthInfo authInfo `json:"auth"`
|
||||
EmailRemindersEnabled bool `json:"email_reminders_enabled"`
|
||||
UserDeletionEnabled bool `json:"user_deletion_enabled"`
|
||||
TaskCommentsEnabled bool `json:"task_comments_enabled"`
|
||||
DemoModeEnabled bool `json:"demo_mode_enabled"`
|
||||
WebhooksEnabled bool `json:"webhooks_enabled"`
|
||||
PublicTeamsEnabled bool `json:"public_teams_enabled"`
|
||||
AllowIconChanges bool `json:"allow_icon_changes"`
|
||||
EnabledProFeatures []license.Feature `json:"enabled_pro_features"`
|
||||
}
|
||||
|
||||
type authInfo struct {
|
||||
Local localAuthInfo `json:"local"`
|
||||
Ldap ldapAuthInfo `json:"ldap"`
|
||||
OpenIDConnect openIDAuthInfo `json:"openid_connect"`
|
||||
}
|
||||
|
||||
type localAuthInfo struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
RegistrationEnabled bool `json:"registration_enabled"`
|
||||
}
|
||||
|
||||
type ldapAuthInfo struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
type openIDAuthInfo struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Providers []*openid.Provider `json:"providers"`
|
||||
}
|
||||
|
||||
type legalInfo struct {
|
||||
ImprintURL string `json:"imprint_url"`
|
||||
PrivacyPolicyURL string `json:"privacy_policy_url"`
|
||||
}
|
||||
|
||||
// Info is the handler to get infos about this vikunja instance
|
||||
// @Summary Info
|
||||
// @Description Returns the version, frontendurl, motd and various settings of Vikunja
|
||||
// @tags service
|
||||
// @Produce json
|
||||
// @Success 200 {object} v1.vikunjaInfos
|
||||
// @Success 200 {object} shared.VikunjaInfos
|
||||
// @Router /info [get]
|
||||
func Info(c *echo.Context) error {
|
||||
info := vikunjaInfos{
|
||||
Version: version.Version,
|
||||
FrontendURL: config.ServicePublicURL.GetString(),
|
||||
Motd: config.ServiceMotd.GetString(),
|
||||
LinkSharingEnabled: config.ServiceEnableLinkSharing.GetBool(),
|
||||
MaxFileSize: config.FilesMaxSize.GetString(),
|
||||
MaxItemsPerPage: config.ServiceMaxItemsPerPage.GetInt(),
|
||||
TaskAttachmentsEnabled: config.ServiceEnableTaskAttachments.GetBool(),
|
||||
TotpEnabled: config.ServiceEnableTotp.GetBool(),
|
||||
CaldavEnabled: config.ServiceEnableCaldav.GetBool(),
|
||||
EmailRemindersEnabled: config.ServiceEnableEmailReminders.GetBool(),
|
||||
UserDeletionEnabled: config.ServiceEnableUserDeletion.GetBool(),
|
||||
TaskCommentsEnabled: config.ServiceEnableTaskComments.GetBool(),
|
||||
DemoModeEnabled: config.ServiceDemoMode.GetBool(),
|
||||
WebhooksEnabled: config.WebhooksEnabled.GetBool(),
|
||||
PublicTeamsEnabled: config.ServiceEnablePublicTeams.GetBool(),
|
||||
AllowIconChanges: config.ServiceAllowIconChanges.GetBool(),
|
||||
EnabledProFeatures: license.EnabledProFeatures(),
|
||||
AvailableMigrators: []string{
|
||||
(&vikunja_file.FileMigrator{}).Name(),
|
||||
(&ticktick.Migrator{}).Name(),
|
||||
(&wekan.Migrator{}).Name(),
|
||||
(&csvmigrator.Migrator{}).Name(),
|
||||
},
|
||||
Legal: legalInfo{
|
||||
ImprintURL: config.LegalImprintURL.GetString(),
|
||||
PrivacyPolicyURL: config.LegalPrivacyURL.GetString(),
|
||||
},
|
||||
AuthInfo: authInfo{
|
||||
Local: localAuthInfo{
|
||||
Enabled: config.AuthLocalEnabled.GetBool(),
|
||||
RegistrationEnabled: config.AuthLocalEnabled.GetBool() && config.ServiceEnableRegistration.GetBool(),
|
||||
},
|
||||
Ldap: ldapAuthInfo{
|
||||
Enabled: config.AuthLdapEnabled.GetBool(),
|
||||
},
|
||||
OpenIDConnect: openIDAuthInfo{
|
||||
Enabled: config.AuthOpenIDEnabled.GetBool(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
providers, err := openid.GetAllProviders()
|
||||
if err != nil {
|
||||
log.Errorf("Error while getting openid providers for /info: %s", err)
|
||||
// No return here to not break /info
|
||||
}
|
||||
|
||||
info.AuthInfo.OpenIDConnect.Providers = providers
|
||||
|
||||
// Migrators
|
||||
if config.MigrationTodoistEnable.GetBool() {
|
||||
m := &todoist.Migration{}
|
||||
info.AvailableMigrators = append(info.AvailableMigrators, m.Name())
|
||||
}
|
||||
if config.MigrationTrelloEnable.GetBool() {
|
||||
m := &trello.Migration{}
|
||||
info.AvailableMigrators = append(info.AvailableMigrators, m.Name())
|
||||
}
|
||||
if config.MigrationMicrosoftTodoEnable.GetBool() {
|
||||
m := µsofttodo.Migration{}
|
||||
info.AvailableMigrators = append(info.AvailableMigrators, m.Name())
|
||||
}
|
||||
|
||||
if config.BackgroundsEnabled.GetBool() {
|
||||
if config.BackgroundsUploadEnabled.GetBool() {
|
||||
info.EnabledBackgroundProviders = append(info.EnabledBackgroundProviders, "upload")
|
||||
}
|
||||
if config.BackgroundsUnsplashEnabled.GetBool() {
|
||||
info.EnabledBackgroundProviders = append(info.EnabledBackgroundProviders, "unsplash")
|
||||
}
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, info)
|
||||
return c.JSON(http.StatusOK, shared.BuildInfo())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package apiv2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"code.vikunja.io/api/pkg/routes/api/shared"
|
||||
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
)
|
||||
|
||||
type infoBody struct {
|
||||
Body shared.VikunjaInfos
|
||||
}
|
||||
|
||||
// RegisterInfoRoutes wires the public instance-info endpoint onto the Huma API.
|
||||
func RegisterInfoRoutes(api huma.API) {
|
||||
Register(api, huma.Operation{
|
||||
OperationID: "info",
|
||||
Summary: "Instance info",
|
||||
Description: "Returns version, frontend URL, motd and the enabled features of this Vikunja instance. Public — no authentication required.",
|
||||
Method: http.MethodGet,
|
||||
Path: "/info",
|
||||
Tags: []string{"service"},
|
||||
// Public: opt out of the globally-applied auth. The path is also listed
|
||||
// in unauthenticatedAPIPaths so the token middleware lets it through.
|
||||
Security: []map[string][]string{},
|
||||
}, info)
|
||||
}
|
||||
|
||||
func init() { AddRouteRegistrar(RegisterInfoRoutes) }
|
||||
|
||||
func info(_ context.Context, _ *struct{}) (*infoBody, error) {
|
||||
return &infoBody{Body: shared.BuildInfo()}, nil
|
||||
}
|
||||
|
|
@ -343,6 +343,7 @@ var unauthenticatedAPIPaths = map[string]bool{
|
|||
"/api/v2/docs": true,
|
||||
"/api/v2/docs/scalar.standalone.js": true,
|
||||
"/api/v2/schemas/:schema": true,
|
||||
"/api/v2/info": true,
|
||||
}
|
||||
|
||||
// collectRoutesForAPITokens collects all routes for API token permission checking.
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
package webtests
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
|
|
@ -29,6 +30,22 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestHumaInfo covers the public instance-info endpoint. It needs no auth and
|
||||
// always reports the running version.
|
||||
func TestHumaInfo(t *testing.T) {
|
||||
e, err := setupTestEnv()
|
||||
require.NoError(t, err)
|
||||
|
||||
rec := humaRequest(t, e, http.MethodGet, "/api/v2/info", "", "", "")
|
||||
require.Equal(t, http.StatusOK, rec.Code, "body: %s", rec.Body.String())
|
||||
|
||||
var body map[string]any
|
||||
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &body))
|
||||
assert.Contains(t, body, "version")
|
||||
assert.Contains(t, body, "auth")
|
||||
assert.Contains(t, body, "available_migrators")
|
||||
}
|
||||
|
||||
// TestHumaProjectBackgroundDelete covers removing a project background. It
|
||||
// mirrors the v1 background_test.go matrix: the owner clears the background
|
||||
// (and keeps the title), a read-only user is refused.
|
||||
|
|
|
|||
Loading…
Reference in New Issue