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:
kolaente 2026-06-11 20:41:18 +02:00 committed by kolaente
parent 6f3dab53cb
commit 56b1ba47ec
5 changed files with 236 additions and 136 deletions

View File

@ -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 := &microsofttodo.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
}

View File

@ -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 := &microsofttodo.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())
}

51
pkg/routes/api/v2/info.go Normal file
View File

@ -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
}

View File

@ -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.

View File

@ -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.