From 992f18370da7bac0e265c5db5527d4864b3b557e Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Wed, 11 Mar 2026 14:59:57 +0000 Subject: [PATCH 01/14] Transcoder: Quality -> VideoQuality --- transcoder/src/api/streams.go | 4 +- transcoder/src/filestream.go | 10 ++--- transcoder/src/transcoder.go | 2 +- .../src/{quality.go => videoquality.go} | 38 +++++++++---------- transcoder/src/videostream.go | 6 +-- 5 files changed, 30 insertions(+), 30 deletions(-) rename transcoder/src/{quality.go => videoquality.go} (69%) diff --git a/transcoder/src/api/streams.go b/transcoder/src/api/streams.go index 5f73e7ad..33ed46a9 100644 --- a/transcoder/src/api/streams.go +++ b/transcoder/src/api/streams.go @@ -88,7 +88,7 @@ func (h *shandler) GetVideoIndex(c echo.Context) error { if err != nil { return err } - quality, err := src.QualityFromString(c.Param("quality")) + quality, err := src.VideoQualityFromString(c.Param("quality")) if err != nil { return err } @@ -154,7 +154,7 @@ func (h *shandler) GetVideoSegment(c echo.Context) error { if err != nil { return err } - quality, err := src.QualityFromString(c.Param("quality")) + quality, err := src.VideoQualityFromString(c.Param("quality")) if err != nil { return err } diff --git a/transcoder/src/filestream.go b/transcoder/src/filestream.go index 94c566e2..45161b25 100644 --- a/transcoder/src/filestream.go +++ b/transcoder/src/filestream.go @@ -24,7 +24,7 @@ type FileStream struct { type VideoKey struct { idx uint32 - quality Quality + quality VideoQuality } func (t *Transcoder) newFileStream(path string, sha string) *FileStream { @@ -110,7 +110,7 @@ func (fs *FileStream) GetMaster(client string) string { } if def_video != nil { - qualities := utils.Filter(Qualities, func(quality Quality) bool { + qualities := utils.Filter(VideoQualities, func(quality VideoQuality) bool { return quality.Height() < def_video.Height }) transcode_count := len(qualities) @@ -179,7 +179,7 @@ func (fs *FileStream) GetMaster(client string) string { return master } -func (fs *FileStream) getVideoStream(idx uint32, quality Quality) (*VideoStream, error) { +func (fs *FileStream) getVideoStream(idx uint32, quality VideoQuality) (*VideoStream, error) { stream, _ := fs.videos.GetOrCreate(VideoKey{idx, quality}, func() *VideoStream { ret, _ := fs.transcoder.NewVideoStream(fs, idx, quality) return ret @@ -188,7 +188,7 @@ func (fs *FileStream) getVideoStream(idx uint32, quality Quality) (*VideoStream, return stream, nil } -func (fs *FileStream) GetVideoIndex(idx uint32, quality Quality, client string) (string, error) { +func (fs *FileStream) GetVideoIndex(idx uint32, quality VideoQuality, client string) (string, error) { stream, err := fs.getVideoStream(idx, quality) if err != nil { return "", err @@ -196,7 +196,7 @@ func (fs *FileStream) GetVideoIndex(idx uint32, quality Quality, client string) return stream.GetIndex(client) } -func (fs *FileStream) GetVideoSegment(idx uint32, quality Quality, segment int32) (string, error) { +func (fs *FileStream) GetVideoSegment(idx uint32, quality VideoQuality, segment int32) (string, error) { stream, err := fs.getVideoStream(idx, quality) if err != nil { return "", err diff --git a/transcoder/src/transcoder.go b/transcoder/src/transcoder.go index b1aa5e28..5dee0aec 100644 --- a/transcoder/src/transcoder.go +++ b/transcoder/src/transcoder.go @@ -70,7 +70,7 @@ func (t *Transcoder) GetVideoIndex( ctx context.Context, path string, video uint32, - quality Quality, + quality VideoQuality, client string, sha string, ) (string, error) { diff --git a/transcoder/src/quality.go b/transcoder/src/videoquality.go similarity index 69% rename from transcoder/src/quality.go rename to transcoder/src/videoquality.go index 3b0f98be..0eb06ca8 100644 --- a/transcoder/src/quality.go +++ b/transcoder/src/videoquality.go @@ -6,25 +6,25 @@ import ( "github.com/labstack/echo/v4" ) -type Quality string +type VideoQuality string const ( - P240 Quality = "240p" - P360 Quality = "360p" - P480 Quality = "480p" - P720 Quality = "720p" - P1080 Quality = "1080p" - P1440 Quality = "1440p" - P4k Quality = "4k" - P8k Quality = "8k" - NoResize Quality = "transcode" - Original Quality = "original" + P240 VideoQuality = "240p" + P360 VideoQuality = "360p" + P480 VideoQuality = "480p" + P720 VideoQuality = "720p" + P1080 VideoQuality = "1080p" + P1440 VideoQuality = "1440p" + P4k VideoQuality = "4k" + P8k VideoQuality = "8k" + NoResize VideoQuality = "transcode" + Original VideoQuality = "original" ) // Purposfully removing Original from this list (since it require special treatments anyways) -var Qualities = []Quality{P240, P360, P480, P720, P1080, P1440, P4k, P8k} +var VideoQualities = []VideoQuality{P240, P360, P480, P720, P1080, P1440, P4k, P8k} -func QualityFromString(str string) (Quality, error) { +func VideoQualityFromString(str string) (VideoQuality, error) { if str == string(Original) { return Original, nil } @@ -32,7 +32,7 @@ func QualityFromString(str string) (Quality, error) { return NoResize, nil } - for _, quality := range Qualities { + for _, quality := range VideoQualities { if string(quality) == str { return quality, nil } @@ -41,7 +41,7 @@ func QualityFromString(str string) (Quality, error) { } // I'm not entierly sure about the values for bitrates. Double checking would be nice. -func (v Quality) AverageBitrate() uint32 { +func (v VideoQuality) AverageBitrate() uint32 { switch v { case P240: return 400_000 @@ -65,7 +65,7 @@ func (v Quality) AverageBitrate() uint32 { panic("Invalid quality value") } -func (v Quality) MaxBitrate() uint32 { +func (v VideoQuality) MaxBitrate() uint32 { switch v { case P240: return 700_000 @@ -89,7 +89,7 @@ func (v Quality) MaxBitrate() uint32 { panic("Invalid quality value") } -func (q Quality) Height() uint32 { +func (q VideoQuality) Height() uint32 { switch q { case P240: return 240 @@ -113,8 +113,8 @@ func (q Quality) Height() uint32 { panic("Invalid quality value") } -func (video *Video) Quality() Quality { - for _, quality := range Qualities { +func (video *Video) Quality() VideoQuality { + for _, quality := range VideoQualities { if quality.Height() >= video.Height || quality.AverageBitrate() >= video.Bitrate { return quality } diff --git a/transcoder/src/videostream.go b/transcoder/src/videostream.go index 55c4d13f..8782da83 100644 --- a/transcoder/src/videostream.go +++ b/transcoder/src/videostream.go @@ -8,10 +8,10 @@ import ( type VideoStream struct { Stream video *Video - quality Quality + quality VideoQuality } -func (t *Transcoder) NewVideoStream(file *FileStream, idx uint32, quality Quality) (*VideoStream, error) { +func (t *Transcoder) NewVideoStream(file *FileStream, idx uint32, quality VideoQuality) (*VideoStream, error) { log.Printf( "Creating a new video stream for %s (n %d) in quality %s", file.Info.Path, @@ -84,7 +84,7 @@ func (vs *VideoStream) getTranscodeArgs(segments string) []string { args = append(args, "-vf", Settings.HwAccel.NoResizeFilter) // NoResize doesn't have bitrate info, fallback to a know quality higher or equal. - for _, q := range Qualities { + for _, q := range VideoQualities { if q.Height() >= vs.video.Height { quality = q break From e8539a4ea1ed31cdad97590c96ebc06c816592bf Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Wed, 11 Mar 2026 19:30:02 +0000 Subject: [PATCH 02/14] Transcoder: Audio Quality Enum --- transcoder/src/audioquality.go | 60 ++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 transcoder/src/audioquality.go diff --git a/transcoder/src/audioquality.go b/transcoder/src/audioquality.go new file mode 100644 index 00000000..2c0fc6b3 --- /dev/null +++ b/transcoder/src/audioquality.go @@ -0,0 +1,60 @@ +package src + +import ( + "net/http" + + "github.com/labstack/echo/v4" +) + +type AudioQuality string + +const ( + K128 AudioQuality = "128k" + K192 AudioQuality = "192k" + K256 AudioQuality = "256k" + K320 AudioQuality = "320k" + M1 AudioQuality = "1411k" // AKA CD Quality (1,411mbps) + AOriginal AudioQuality = "original" +) + +var AudioQualities = []AudioQuality{K128, K192, K256, K320, M1} + +func AudioQualityFromString(str string) (AudioQuality, error) { + if str == string(AOriginal) { + return AOriginal, nil + } + + for _, quality := range AudioQualities { + if string(quality) == str { + return quality, nil + } + } + return AOriginal, echo.NewHTTPError(http.StatusBadRequest, "Invalid quality") +} + +func (a AudioQuality) Bitrate() uint32 { + switch a { + case K128: + return 128_000 + case K192: + return 192_000 + case K256: + return 256_000 + case K320: + return 320_000 + case M1: + return 1_411_000 + case AOriginal: + panic("Original quality must be handled specially") + } + panic("Invalid quality value") +} + +func (audio *Audio) Quality() AudioQuality { + for _, quality := range AudioQualities { + if quality.Bitrate() >= audio.Bitrate { + return quality + } + } + return K128 +} From 42ba285948ee4861ec50cd7c50da99caa9677378 Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Wed, 11 Mar 2026 19:30:48 +0000 Subject: [PATCH 03/14] Transcoder: Audio: set ffmpeg bitrate flag according to requested quality --- transcoder/src/audiostream.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/transcoder/src/audiostream.go b/transcoder/src/audiostream.go index 24f561a4..e8bbbde4 100644 --- a/transcoder/src/audiostream.go +++ b/transcoder/src/audiostream.go @@ -7,10 +7,11 @@ import ( type AudioStream struct { Stream - index uint32 + index uint32 + quality AudioQuality } -func (t *Transcoder) NewAudioStream(file *FileStream, idx uint32) (*AudioStream, error) { +func (t *Transcoder) NewAudioStream(file *FileStream, idx uint32, quality AudioQuality) (*AudioStream, error) { log.Printf("Creating a audio stream %d for %s", idx, file.Info.Path) keyframes, err := t.metadataService.GetKeyframes(file.Info, false, idx) @@ -20,12 +21,13 @@ func (t *Transcoder) NewAudioStream(file *FileStream, idx uint32) (*AudioStream, ret := new(AudioStream) ret.index = idx + ret.quality = quality NewStream(file, keyframes, ret, &ret.Stream) return ret, nil } func (as *AudioStream) getOutPath(encoder_id int) string { - return fmt.Sprintf("%s/segment-a%d-%d-%%d.ts", as.file.Out, as.index, encoder_id) + return fmt.Sprintf("%s/segment-a%d-%d-%d-%%d.ts", as.file.Out, as.quality, as.index, encoder_id) } func (as *AudioStream) getFlags() Flags { @@ -38,7 +40,6 @@ func (as *AudioStream) getTranscodeArgs(segments string) []string { "-c:a", "aac", // TODO: Support 5.1 audio streams. "-ac", "2", - // TODO: Support multi audio qualities. - "-b:a", "128k", + "-b:a", string(as.quality), } } From 7f800f82d00c2cd60038b3987cd863b80b2bb356 Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Wed, 11 Mar 2026 19:32:37 +0000 Subject: [PATCH 04/14] Transcoder: Audio: Add quality to AudioKey + param to api endpoint --- transcoder/src/api/streams.go | 20 +++++++---- transcoder/src/filestream.go | 62 +++++++++++++++++++---------------- transcoder/src/tracker.go | 6 ++-- transcoder/src/transcoder.go | 12 ++++--- 4 files changed, 58 insertions(+), 42 deletions(-) diff --git a/transcoder/src/api/streams.go b/transcoder/src/api/streams.go index 33ed46a9..b6f65301 100644 --- a/transcoder/src/api/streams.go +++ b/transcoder/src/api/streams.go @@ -20,9 +20,9 @@ func RegisterStreamHandlers(e *echo.Group, transcoder *src.Transcoder) { e.GET("/:path/direct/:identifier", DirectStream) e.GET("/:path/master.m3u8", h.GetMaster) e.GET("/:path/:video/:quality/index.m3u8", h.GetVideoIndex) - e.GET("/:path/audio/:audio/index.m3u8", h.GetAudioIndex) + e.GET("/:path/audio/:audio/:quality/index.m3u8", h.GetAudioIndex) e.GET("/:path/:video/:quality/:chunk", h.GetVideoSegment) - e.GET("/:path/audio/:audio/:chunk", h.GetAudioSegment) + e.GET("/:path/audio/:audio/:quality/:chunk", h.GetAudioSegment) } // @Summary Direct video @@ -115,7 +115,7 @@ func (h *shandler) GetVideoIndex(c echo.Context) error { // This route can take a few seconds to respond since it will way for at least one segment to be // available. // -// Path: /:path/audio/:audio/index.m3u8 +// Path: /:path/audio/:audio/:quality/index.m3u8 // // PRIVATE ROUTE (not documented in swagger, can change at any time) // Only reached via the master.m3u8. @@ -124,6 +124,10 @@ func (h *shandler) GetAudioIndex(c echo.Context) error { if err != nil { return err } + quality, err := src.AudioQualityFromString(c.Param("quality")) + if err != nil { + return err + } client, err := getClientId(c) if err != nil { return err @@ -133,7 +137,7 @@ func (h *shandler) GetAudioIndex(c echo.Context) error { return err } - ret, err := h.transcoder.GetAudioIndex(c.Request().Context(), path, uint32(audio), client, sha) + ret, err := h.transcoder.GetAudioIndex(c.Request().Context(), path, uint32(audio), quality, client, sha) if err != nil { return err } @@ -190,7 +194,7 @@ func (h *shandler) GetVideoSegment(c echo.Context) error { // // Retrieve a chunk of a transcoded audio. // -// Path: /:path/audio/:audio/segments-:chunk.ts +// Path: /:path/audio/:audio/:quality/segments-:chunk.ts // // PRIVATE ROUTE (not documented in swagger, can change at any time) // Only reached via the master.m3u8. @@ -199,6 +203,10 @@ func (h *shandler) GetAudioSegment(c echo.Context) error { if err != nil { return err } + quality, err := src.AudioQualityFromString(c.Param("quality")) + if err != nil { + return err + } segment, err := parseSegment(c.Param("chunk")) if err != nil { return err @@ -212,7 +220,7 @@ func (h *shandler) GetAudioSegment(c echo.Context) error { return err } - ret, err := h.transcoder.GetAudioSegment(c.Request().Context(), path, uint32(audio), segment, client, sha) + ret, err := h.transcoder.GetAudioSegment(c.Request().Context(), path, uint32(audio), quality, segment, client, sha) if err != nil { return err } diff --git a/transcoder/src/filestream.go b/transcoder/src/filestream.go index 45161b25..8a879d79 100644 --- a/transcoder/src/filestream.go +++ b/transcoder/src/filestream.go @@ -19,7 +19,12 @@ type FileStream struct { Out string Info *MediaInfo videos CMap[VideoKey, *VideoStream] - audios CMap[uint32, *AudioStream] + audios CMap[AudioKey, *AudioStream] +} + +type AudioKey struct { + idx uint32 + quality AudioQuality } type VideoKey struct { @@ -32,7 +37,7 @@ func (t *Transcoder) newFileStream(path string, sha string) *FileStream { transcoder: t, Out: fmt.Sprintf("%s/%s", Settings.Outpath, sha), videos: NewCMap[VideoKey, *VideoStream](), - audios: NewCMap[uint32, *AudioStream](), + audios: NewCMap[AudioKey, *AudioStream](), } ret.ready.Add(1) @@ -71,27 +76,28 @@ func (fs *FileStream) Destroy() { func (fs *FileStream) GetMaster(client string) string { master := "#EXTM3U\n" - // TODO: support multiples audio qualities (and original) - for _, audio := range fs.Info.Audios { - master += "#EXT-X-MEDIA:TYPE=AUDIO," - master += "GROUP-ID=\"audio\"," - if audio.Language != nil { - master += fmt.Sprintf("LANGUAGE=\"%s\",", *audio.Language) + for _, quality := range AudioQualities { + for _, audio := range fs.Info.Audios { + master += "#EXT-X-MEDIA:TYPE=AUDIO," + master += fmt.Sprintf("GROUP-ID=\"audio-%s\",", quality) + if audio.Language != nil { + master += fmt.Sprintf("LANGUAGE=\"%s\",", *audio.Language) + } + if audio.Title != nil { + master += fmt.Sprintf("NAME=\"%s\",", *audio.Title) + } else if audio.Language != nil { + master += fmt.Sprintf("NAME=\"%s\",", *audio.Language) + } else { + master += fmt.Sprintf("NAME=\"Audio %d\",", audio.Index) + } + if audio.IsDefault { + master += "DEFAULT=YES," + } + master += "CHANNELS=\"2\"," + master += fmt.Sprintf("URI=\"audio/%d/%s/index.m3u8?clientId=%s\"\n", audio.Index, quality, client) } - if audio.Title != nil { - master += fmt.Sprintf("NAME=\"%s\",", *audio.Title) - } else if audio.Language != nil { - master += fmt.Sprintf("NAME=\"%s\",", *audio.Language) - } else { - master += fmt.Sprintf("NAME=\"Audio %d\",", audio.Index) - } - if audio.IsDefault { - master += "DEFAULT=YES," - } - master += "CHANNELS=\"2\"," - master += fmt.Sprintf("URI=\"audio/%d/index.m3u8?clientId=%s\"\n", audio.Index, client) + master += "\n" } - master += "\n" // codec is the prefix + the level, the level is not part of the codec we want to compare for the same_codec check bellow transcode_prefix := "avc1.6400" @@ -204,25 +210,25 @@ func (fs *FileStream) GetVideoSegment(idx uint32, quality VideoQuality, segment return stream.GetSegment(segment) } -func (fs *FileStream) getAudioStream(audio uint32) (*AudioStream, error) { - stream, _ := fs.audios.GetOrCreate(audio, func() *AudioStream { - ret, _ := fs.transcoder.NewAudioStream(fs, audio) +func (fs *FileStream) getAudioStream(idx uint32, quality AudioQuality) (*AudioStream, error) { + stream, _ := fs.audios.GetOrCreate(AudioKey{idx, quality}, func() *AudioStream { + ret, _ := fs.transcoder.NewAudioStream(fs, idx, quality) return ret }) stream.ready.Wait() return stream, nil } -func (fs *FileStream) GetAudioIndex(audio uint32, client string) (string, error) { - stream, err := fs.getAudioStream(audio) +func (fs *FileStream) GetAudioIndex(idx uint32, quality AudioQuality, client string) (string, error) { + stream, err := fs.getAudioStream(idx, quality) if err != nil { return "", nil } return stream.GetIndex(client) } -func (fs *FileStream) GetAudioSegment(audio uint32, segment int32) (string, error) { - stream, err := fs.getAudioStream(audio) +func (fs *FileStream) GetAudioSegment(idx uint32, quality AudioQuality, segment int32) (string, error) { + stream, err := fs.getAudioStream(idx, quality) if err != nil { return "", nil } diff --git a/transcoder/src/tracker.go b/transcoder/src/tracker.go index 015a89f0..56d4cf2d 100644 --- a/transcoder/src/tracker.go +++ b/transcoder/src/tracker.go @@ -10,7 +10,7 @@ type ClientInfo struct { sha string path string video *VideoKey - audio *uint32 + audio *AudioKey vhead int32 ahead int32 } @@ -151,7 +151,7 @@ func (t *Tracker) DestroyStreamIfOld(sha string) { stream.Destroy() } -func (t *Tracker) KillAudioIfDead(sha string, path string, audio uint32) bool { +func (t *Tracker) KillAudioIfDead(sha string, path string, audio AudioKey) bool { for _, stream := range t.clients { if stream.sha == sha && stream.audio != nil && *stream.audio == audio { return false @@ -191,7 +191,7 @@ func (t *Tracker) KillVideoIfDead(sha string, path string, video VideoKey) bool return true } -func (t *Tracker) KillOrphanedHeads(sha string, video *VideoKey, audio *uint32) { +func (t *Tracker) KillOrphanedHeads(sha string, video *VideoKey, audio *AudioKey) { stream, ok := t.transcoder.streams.Get(sha) if !ok { return diff --git a/transcoder/src/transcoder.go b/transcoder/src/transcoder.go index 5dee0aec..dfb960a4 100644 --- a/transcoder/src/transcoder.go +++ b/transcoder/src/transcoder.go @@ -94,6 +94,7 @@ func (t *Transcoder) GetAudioIndex( ctx context.Context, path string, audio uint32, + quality AudioQuality, client string, sha string, ) (string, error) { @@ -105,18 +106,18 @@ func (t *Transcoder) GetAudioIndex( client: client, sha: sha, path: path, - audio: &audio, + audio: &AudioKey{audio, quality}, vhead: -1, ahead: -1, } - return stream.GetAudioIndex(audio, client) + return stream.GetAudioIndex(audio, quality, client) } func (t *Transcoder) GetVideoSegment( ctx context.Context, path string, video uint32, - quality Quality, + quality VideoQuality, segment int32, client string, sha string, @@ -141,6 +142,7 @@ func (t *Transcoder) GetAudioSegment( ctx context.Context, path string, audio uint32, + quality AudioQuality, segment int32, client string, sha string, @@ -153,9 +155,9 @@ func (t *Transcoder) GetAudioSegment( client: client, sha: sha, path: path, - audio: &audio, + audio: &AudioKey{audio, quality}, ahead: segment, vhead: -1, } - return stream.GetAudioSegment(audio, segment) + return stream.GetAudioSegment(audio, quality, segment) } From a17c01c9c75fa12fd55b6fb0fa51e9f4f5a36bd0 Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Wed, 11 Mar 2026 19:32:53 +0000 Subject: [PATCH 05/14] Transcoder: Remove TODO --- transcoder/README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/transcoder/README.md b/transcoder/README.md index d23dfa59..eba2cc99 100644 --- a/transcoder/README.md +++ b/transcoder/README.md @@ -35,10 +35,7 @@ Projects using gocoder: I did a blog post explaining the core idea, you can find it at [zoriya.dev/blogs/transcoder](https://zoriya.dev/blogs/transcoder). ## TODO: -- Add a swagger -- Add configurable JWT authorization (v5 of kyoo) - Add credits/recaps/intro/preview detection -- Add multiples qualities for audio streams - Improve multi-video support - Add optional redis synchronization for replication (`RunLock` was made with this in mind) - fmp4 support From 7ca27eb07696256f3b838c76827aaffc7b7e47c2 Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Thu, 12 Mar 2026 08:38:53 +0000 Subject: [PATCH 06/14] Transcoder: Fix ffmepg flag for audio --- transcoder/src/audiostream.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/transcoder/src/audiostream.go b/transcoder/src/audiostream.go index e8bbbde4..a920f952 100644 --- a/transcoder/src/audiostream.go +++ b/transcoder/src/audiostream.go @@ -35,11 +35,12 @@ func (as *AudioStream) getFlags() Flags { } func (as *AudioStream) getTranscodeArgs(segments string) []string { + // TODO If quality is Original, get quality of original source return []string{ "-map", fmt.Sprintf("0:a:%d", as.index), "-c:a", "aac", // TODO: Support 5.1 audio streams. "-ac", "2", - "-b:a", string(as.quality), + "-b:a", fmt.Sprint(as.quality.Bitrate()), } } From edccf25bcf620f81dcac1db8a407291254ba3976 Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Fri, 13 Mar 2026 10:44:12 +0000 Subject: [PATCH 07/14] Transcoder: set max audio quality to 512k --- transcoder/src/audioquality.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/transcoder/src/audioquality.go b/transcoder/src/audioquality.go index 2c0fc6b3..78e946f1 100644 --- a/transcoder/src/audioquality.go +++ b/transcoder/src/audioquality.go @@ -13,11 +13,11 @@ const ( K192 AudioQuality = "192k" K256 AudioQuality = "256k" K320 AudioQuality = "320k" - M1 AudioQuality = "1411k" // AKA CD Quality (1,411mbps) + K512 AudioQuality = "512k" AOriginal AudioQuality = "original" ) -var AudioQualities = []AudioQuality{K128, K192, K256, K320, M1} +var AudioQualities = []AudioQuality{K128, K192, K256, K320, K512} func AudioQualityFromString(str string) (AudioQuality, error) { if str == string(AOriginal) { @@ -42,8 +42,8 @@ func (a AudioQuality) Bitrate() uint32 { return 256_000 case K320: return 320_000 - case M1: - return 1_411_000 + case K512: + return 512_000 case AOriginal: panic("Original quality must be handled specially") } From 5ebfe13799fdd3227ccc652f8f42938ca2f7371d Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Fri, 13 Mar 2026 10:46:07 +0000 Subject: [PATCH 08/14] Transcoder: Correctly set bitrate flag when request og quality --- transcoder/src/audiostream.go | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/transcoder/src/audiostream.go b/transcoder/src/audiostream.go index a920f952..d3db3fcb 100644 --- a/transcoder/src/audiostream.go +++ b/transcoder/src/audiostream.go @@ -7,7 +7,7 @@ import ( type AudioStream struct { Stream - index uint32 + audio *Audio quality AudioQuality } @@ -20,14 +20,20 @@ func (t *Transcoder) NewAudioStream(file *FileStream, idx uint32, quality AudioQ } ret := new(AudioStream) - ret.index = idx ret.quality = quality + for _, audio := range file.Info.Audios { + if audio.Index == idx { + ret.audio = &audio + break + } + } + NewStream(file, keyframes, ret, &ret.Stream) return ret, nil } func (as *AudioStream) getOutPath(encoder_id int) string { - return fmt.Sprintf("%s/segment-a%d-%d-%d-%%d.ts", as.file.Out, as.quality, as.index, encoder_id) + return fmt.Sprintf("%s/segment-a%d-%d-%d-%%d.ts", as.file.Out, as.quality, as.audio.Index, encoder_id) } func (as *AudioStream) getFlags() Flags { @@ -35,12 +41,18 @@ func (as *AudioStream) getFlags() Flags { } func (as *AudioStream) getTranscodeArgs(segments string) []string { - // TODO If quality is Original, get quality of original source - return []string{ - "-map", fmt.Sprintf("0:a:%d", as.index), + args := []string{ + "-map", fmt.Sprintf("0:a:%d", as.audio.Index), "-c:a", "aac", // TODO: Support 5.1 audio streams. "-ac", "2", - "-b:a", fmt.Sprint(as.quality.Bitrate()), } + if as.quality == AOriginal { + args = append(args, "-b:a", fmt.Sprint(as.audio.Bitrate)) + } else { + args = append(args, + "-b:a", fmt.Sprint(as.quality.Bitrate()), + ) + } + return args } From 51959a7be0f118cee9ee58ccf5206cdb41b9a2a2 Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Fri, 13 Mar 2026 11:38:59 +0000 Subject: [PATCH 09/14] Transcoder: Fix file path template for audio --- transcoder/src/audiostream.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/transcoder/src/audiostream.go b/transcoder/src/audiostream.go index d3db3fcb..c380320a 100644 --- a/transcoder/src/audiostream.go +++ b/transcoder/src/audiostream.go @@ -33,7 +33,7 @@ func (t *Transcoder) NewAudioStream(file *FileStream, idx uint32, quality AudioQ } func (as *AudioStream) getOutPath(encoder_id int) string { - return fmt.Sprintf("%s/segment-a%d-%d-%d-%%d.ts", as.file.Out, as.quality, as.audio.Index, encoder_id) + return fmt.Sprintf("%s/segment-a%d-%s-%d-%%d.ts", as.file.Out, as.audio.Index, string(as.quality), encoder_id) } func (as *AudioStream) getFlags() Flags { From 77b548fceacf0c74a5a3741bd949e3a479598822 Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Fri, 13 Mar 2026 14:20:30 +0000 Subject: [PATCH 10/14] Transcoder: Audio: If original, then keep no re-encode --- transcoder/src/audiostream.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/transcoder/src/audiostream.go b/transcoder/src/audiostream.go index c380320a..eae658c4 100644 --- a/transcoder/src/audiostream.go +++ b/transcoder/src/audiostream.go @@ -43,15 +43,15 @@ func (as *AudioStream) getFlags() Flags { func (as *AudioStream) getTranscodeArgs(segments string) []string { args := []string{ "-map", fmt.Sprintf("0:a:%d", as.audio.Index), - "-c:a", "aac", - // TODO: Support 5.1 audio streams. - "-ac", "2", } if as.quality == AOriginal { - args = append(args, "-b:a", fmt.Sprint(as.audio.Bitrate)) + args = append(args, "-c:a", "copy") } else { args = append(args, + // TODO: Support 5.1 audio streams. + "-ac", "2", "-b:a", fmt.Sprint(as.quality.Bitrate()), + "-c:a", "aac", ) } return args From f8dfdb4f9aa87da9dfcf855340484949741c05ed Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Sat, 21 Mar 2026 17:47:32 +0100 Subject: [PATCH 11/14] Transcoder: Correct listing of audio streams in master.m3u8 --- transcoder/src/filestream.go | 50 ++++++++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/transcoder/src/filestream.go b/transcoder/src/filestream.go index 8a879d79..43fe0840 100644 --- a/transcoder/src/filestream.go +++ b/transcoder/src/filestream.go @@ -76,8 +76,8 @@ func (fs *FileStream) Destroy() { func (fs *FileStream) GetMaster(client string) string { master := "#EXTM3U\n" - for _, quality := range AudioQualities { - for _, audio := range fs.Info.Audios { + for _, audio := range fs.Info.Audios { + for _, quality := range AudioQualities { master += "#EXT-X-MEDIA:TYPE=AUDIO," master += fmt.Sprintf("GROUP-ID=\"audio-%s\",", quality) if audio.Language != nil { @@ -96,7 +96,23 @@ func (fs *FileStream) GetMaster(client string) string { master += "CHANNELS=\"2\"," master += fmt.Sprintf("URI=\"audio/%d/%s/index.m3u8?clientId=%s\"\n", audio.Index, quality, client) } - master += "\n" + master += "#EXT-X-MEDIA:TYPE=AUDIO," + master += fmt.Sprintf("GROUP-ID=\"audio-%s\",", AOriginal) + if audio.Language != nil { + master += fmt.Sprintf("LANGUAGE=\"%s\",", *audio.Language) + } + if audio.Title != nil { + master += fmt.Sprintf("NAME=\"%s\",", *audio.Title) + } else if audio.Language != nil { + master += fmt.Sprintf("NAME=\"%s\",", *audio.Language) + } else { + master += fmt.Sprintf("NAME=\"Audio %d\",", audio.Index) + } + if audio.IsDefault { + master += "DEFAULT=YES," + } + master += "CHANNELS=\"2\"," // TODO + master += fmt.Sprintf("URI=\"audio/%d/%s/index.m3u8?clientId=%s\"\n", audio.Index, AOriginal, client) } // codec is the prefix + the level, the level is not part of the codec we want to compare for the same_codec check bellow @@ -165,18 +181,19 @@ func (fs *FileStream) GetMaster(client string) string { } else if def_video.MimeCodec != nil { master += fmt.Sprintf("CODECS=\"%s\",", strings.Join([]string{*def_video.MimeCodec, audio_codec}, ",")) } - master += "AUDIO=\"audio\"," + master += fmt.Sprintf("AUDIO=\"audio-%s\",", string(matchAudioQuality(quality))) master += "CLOSED-CAPTIONS=NONE\n" master += fmt.Sprintf("%d/%s/index.m3u8?clientId=%s\n", def_video.Index, quality, client) continue } + // TODO: Add another w/ transcoded audio master += "#EXT-X-STREAM-INF:" master += fmt.Sprintf("AVERAGE-BANDWIDTH=%d,", quality.AverageBitrate()) master += fmt.Sprintf("BANDWIDTH=%d,", quality.MaxBitrate()) master += fmt.Sprintf("RESOLUTION=%dx%d,", int(aspectRatio*float32(quality.Height())+0.5), quality.Height()) master += fmt.Sprintf("CODECS=\"%s\",", strings.Join([]string{transcode_codec, audio_codec}, ",")) - master += "AUDIO=\"audio\"," + master += fmt.Sprintf("AUDIO=\"audio-%s\",", string(matchAudioQuality(quality))) master += "CLOSED-CAPTIONS=NONE\n" master += fmt.Sprintf("%d/%s/index.m3u8?clientId=%s\n", def_video.Index, quality, client) } @@ -234,3 +251,26 @@ func (fs *FileStream) GetAudioSegment(idx uint32, quality AudioQuality, segment } return stream.GetSegment(segment) } + +func matchAudioQuality(q VideoQuality) AudioQuality { + switch q { + case P240: + return K128 + case P360: + return K128 + case P480: + return K128 + case P720: + return K192 + case P1080: + return K192 + case P1440: + return K256 + case P4k: + return K512 + case P8k: + return K512 + default: + return AOriginal + } +} From 0614037464c8e2636e73c307279f21b3866f7136 Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Sun, 22 Mar 2026 18:51:31 +0100 Subject: [PATCH 12/14] Transcoder: Master: Add entry with og video stream but transcoded audio --- transcoder/src/filestream.go | 41 ++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/transcoder/src/filestream.go b/transcoder/src/filestream.go index 43fe0840..7a44c3dc 100644 --- a/transcoder/src/filestream.go +++ b/transcoder/src/filestream.go @@ -118,9 +118,10 @@ func (fs *FileStream) GetMaster(client string) string { // codec is the prefix + the level, the level is not part of the codec we want to compare for the same_codec check bellow transcode_prefix := "avc1.6400" transcode_codec := transcode_prefix + "28" - audio_codec := "mp4a.40.2" + transcode_audio_codec := "mp4a.40.2" var def_video *Video + var def_audio *Audio for _, video := range fs.Info.Videos { if video.IsDefault { def_video = &video @@ -130,6 +131,9 @@ func (fs *FileStream) GetMaster(client string) string { if def_video == nil && len(fs.Info.Videos) > 0 { def_video = &fs.Info.Videos[0] } + if len(fs.Info.Audios) > 0 { + def_audio = &fs.Info.Audios[0] + } if def_video != nil { qualities := utils.Filter(VideoQualities, func(quality VideoQuality) bool { @@ -170,29 +174,34 @@ func (fs *FileStream) GetMaster(client string) string { aspectRatio := float32(def_video.Width) / float32(def_video.Height) for i, quality := range qualities { if i >= transcode_count { - // original & noresize streams - bitrate := float64(def_video.Bitrate) - master += "#EXT-X-STREAM-INF:" - master += fmt.Sprintf("AVERAGE-BANDWIDTH=%d,", int(math.Min(bitrate*0.8, float64(def_video.Quality().AverageBitrate())))) - master += fmt.Sprintf("BANDWIDTH=%d,", int(math.Min(bitrate, float64(def_video.Quality().MaxBitrate())))) - master += fmt.Sprintf("RESOLUTION=%dx%d,", def_video.Width, def_video.Height) - if quality != Original { - master += fmt.Sprintf("CODECS=\"%s\",", strings.Join([]string{transcode_codec, audio_codec}, ",")) - } else if def_video.MimeCodec != nil { - master += fmt.Sprintf("CODECS=\"%s\",", strings.Join([]string{*def_video.MimeCodec, audio_codec}, ",")) + for _, audio_quality := range []AudioQuality{AOriginal, matchAudioQuality(def_video.Quality())} { + // original & noresize streams + bitrate := float64(def_video.Bitrate) + master += "#EXT-X-STREAM-INF:" + master += fmt.Sprintf("AVERAGE-BANDWIDTH=%d,", int(math.Min(bitrate*0.8, float64(def_video.Quality().AverageBitrate())))) + master += fmt.Sprintf("BANDWIDTH=%d,", int(math.Min(bitrate, float64(def_video.Quality().MaxBitrate())))) + master += fmt.Sprintf("RESOLUTION=%dx%d,", def_video.Width, def_video.Height) + var audio_codec = transcode_audio_codec + if def_audio != nil && audio_quality == AOriginal { + audio_codec = *def_audio.MimeCodec + } + if quality != Original { + master += fmt.Sprintf("CODECS=\"%s\",", strings.Join([]string{transcode_codec, audio_codec}, ",")) + } else if def_video.MimeCodec != nil { + master += fmt.Sprintf("CODECS=\"%s\",", strings.Join([]string{*def_video.MimeCodec, audio_codec}, ",")) + } + master += fmt.Sprintf("AUDIO=\"audio-%s\",", string(audio_quality)) + master += "CLOSED-CAPTIONS=NONE\n" + master += fmt.Sprintf("%d/%s/index.m3u8?clientId=%s\n", def_video.Index, quality, client) } - master += fmt.Sprintf("AUDIO=\"audio-%s\",", string(matchAudioQuality(quality))) - master += "CLOSED-CAPTIONS=NONE\n" - master += fmt.Sprintf("%d/%s/index.m3u8?clientId=%s\n", def_video.Index, quality, client) continue } - // TODO: Add another w/ transcoded audio master += "#EXT-X-STREAM-INF:" master += fmt.Sprintf("AVERAGE-BANDWIDTH=%d,", quality.AverageBitrate()) master += fmt.Sprintf("BANDWIDTH=%d,", quality.MaxBitrate()) master += fmt.Sprintf("RESOLUTION=%dx%d,", int(aspectRatio*float32(quality.Height())+0.5), quality.Height()) - master += fmt.Sprintf("CODECS=\"%s\",", strings.Join([]string{transcode_codec, audio_codec}, ",")) + master += fmt.Sprintf("CODECS=\"%s\",", strings.Join([]string{transcode_codec, transcode_audio_codec}, ",")) master += fmt.Sprintf("AUDIO=\"audio-%s\",", string(matchAudioQuality(quality))) master += "CLOSED-CAPTIONS=NONE\n" master += fmt.Sprintf("%d/%s/index.m3u8?clientId=%s\n", def_video.Index, quality, client) From 40b63001cee384bc26167a5a0905076fa3745242 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sat, 28 Mar 2026 12:52:56 +0100 Subject: [PATCH 13/14] Fix auth swagger --- auth/docs/docs.go | 773 +++++++++++++++++++++++--- auth/docs/swagger.json | 773 +++++++++++++++++++++++--- auth/docs/swagger.yaml | 1164 ++++++++++++++++++++++++++++++++++++++++ auth/go.mod | 67 ++- auth/go.sum | 141 +++-- auth/main.go | 2 +- auth/oidc.go | 2 +- 7 files changed, 2649 insertions(+), 273 deletions(-) create mode 100644 auth/docs/swagger.yaml diff --git a/auth/docs/docs.go b/auth/docs/docs.go index b220190e..8b6b4925 100644 --- a/auth/docs/docs.go +++ b/auth/docs/docs.go @@ -42,6 +42,26 @@ const docTemplate = `{ } } }, + "/info": { + "get": { + "description": "List keibi's settings (oidc providers, public url...)", + "produces": [ + "application/json" + ], + "tags": [ + "oidc" + ], + "summary": "Auth info", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.ServerInfo" + } + } + } + } + }, "/jwt": { "get": { "security": [ @@ -199,7 +219,252 @@ const docTemplate = `{ } } }, + "/oidc/callback/{provider}": { + "get": { + "description": "Exchange an opaque OIDC token for a local session.", + "produces": [ + "application/json" + ], + "tags": [ + "oidc" + ], + "summary": "OIDC callback", + "parameters": [ + { + "type": "string", + "example": "google", + "description": "OIDC provider id", + "name": "provider", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Opaque token returned by /oidc/logged/:provider", + "name": "token", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Optional tenant passthrough for federated setups", + "name": "tenant", + "in": "query" + }, + { + "type": "string", + "description": "Bearer token to link provider to current account", + "name": "Authorization", + "in": "header" + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/main.SessionWToken" + } + }, + "404": { + "description": "Unknown OIDC provider", + "schema": { + "$ref": "#/definitions/main.KError" + } + }, + "410": { + "description": "Login token expired or already used", + "schema": { + "$ref": "#/definitions/main.KError" + } + } + } + } + }, + "/oidc/logged/{provider}": { + "get": { + "description": "Callback endpoint called by OIDC providers after login.", + "produces": [ + "application/json" + ], + "tags": [ + "oidc" + ], + "summary": "OIDC logged callback", + "parameters": [ + { + "type": "string", + "example": "google", + "description": "OIDC provider id", + "name": "provider", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "State value returned by the provider", + "name": "state", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Authorization code", + "name": "code", + "in": "query" + }, + { + "type": "string", + "description": "Provider callback error", + "name": "error", + "in": "query" + } + ], + "responses": { + "302": { + "description": "Found" + }, + "400": { + "description": "Invalid state", + "schema": { + "$ref": "#/definitions/main.KError" + } + }, + "404": { + "description": "Unknown OIDC provider", + "schema": { + "$ref": "#/definitions/main.KError" + } + } + } + } + }, + "/oidc/login/{provider}": { + "get": { + "description": "Start an OIDC login with a provider.", + "produces": [ + "application/json" + ], + "tags": [ + "oidc" + ], + "summary": "OIDC login", + "parameters": [ + { + "type": "string", + "example": "google", + "description": "OIDC provider id", + "name": "provider", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "URL to redirect the browser to after provider callback", + "name": "redirectUrl", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Optional tenant passthrough for federated setups", + "name": "tenant", + "in": "query" + } + ], + "responses": { + "302": { + "description": "Found" + }, + "400": { + "description": "Missing redirectUrl", + "schema": { + "$ref": "#/definitions/main.KError" + } + }, + "404": { + "description": "Unknown OIDC provider", + "schema": { + "$ref": "#/definitions/main.KError" + } + } + } + }, + "delete": { + "security": [ + { + "Jwt": [] + } + ], + "description": "Remove an OIDC provider from the current account.", + "produces": [ + "application/json" + ], + "tags": [ + "oidc" + ], + "summary": "OIDC unlink provider", + "parameters": [ + { + "type": "string", + "example": "google", + "description": "OIDC provider id", + "name": "provider", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "404": { + "description": "Unknown OIDC provider", + "schema": { + "$ref": "#/definitions/main.KError" + } + } + } + } + }, "/sessions": { + "get": { + "security": [ + { + "Jwt": [] + } + ], + "description": "List all active sessions for the currently connected user", + "produces": [ + "application/json" + ], + "tags": [ + "sessions" + ], + "summary": "List my sessions", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/main.SessionWCurrent" + } + } + }, + "401": { + "description": "Missing jwt token", + "schema": { + "$ref": "#/definitions/main.KError" + } + }, + "403": { + "description": "Invalid jwt token (or expired)", + "schema": { + "$ref": "#/definitions/main.KError" + } + } + } + }, "post": { "description": "Login to your account and open a session", "consumes": [ @@ -374,7 +639,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/main.Page-main_User" + "$ref": "#/definitions/main.Page-models_User" } }, "422": { @@ -410,7 +675,7 @@ const docTemplate = `{ "name": "user", "in": "body", "schema": { - "$ref": "#/definitions/main.RegisterDto" + "$ref": "#/definitions/models.RegisterDto" } } ], @@ -421,6 +686,12 @@ const docTemplate = `{ "$ref": "#/definitions/main.SessionWToken" } }, + "403": { + "description": "Registrations are disabled", + "schema": { + "$ref": "#/definitions/main.KError" + } + }, "409": { "description": "Duplicated email or username", "schema": { @@ -455,7 +726,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/main.User" + "$ref": "#/definitions/models.User" } }, "401": { @@ -493,7 +764,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/main.User" + "$ref": "#/definitions/models.User" } } } @@ -521,7 +792,7 @@ const docTemplate = `{ "name": "user", "in": "body", "schema": { - "$ref": "#/definitions/main.EditUserDto" + "$ref": "#/definitions/models.EditUserDto" } } ], @@ -529,7 +800,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/main.User" + "$ref": "#/definitions/models.User" } }, "403": { @@ -547,6 +818,137 @@ const docTemplate = `{ } } }, + "/users/me/logo": { + "get": { + "security": [ + { + "Jwt": [] + } + ], + "description": "Get the current user's logo (manual upload if available, gravatar otherwise)", + "produces": [ + "image/*" + ], + "tags": [ + "users" + ], + "summary": "Get my logo", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "file" + } + }, + "401": { + "description": "Missing jwt token", + "schema": { + "$ref": "#/definitions/main.KError" + } + }, + "403": { + "description": "Invalid jwt token (or expired)", + "schema": { + "$ref": "#/definitions/main.KError" + } + }, + "404": { + "description": "No gravatar image found for this user", + "schema": { + "$ref": "#/definitions/main.KError" + } + } + } + }, + "post": { + "security": [ + { + "Jwt": [] + } + ], + "description": "Upload a manual profile picture for the current user", + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Upload my logo", + "parameters": [ + { + "type": "file", + "description": "Profile picture image (jpeg/png/gif/webp, max 5MB)", + "name": "logo", + "in": "formData", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "401": { + "description": "Missing jwt token", + "schema": { + "$ref": "#/definitions/main.KError" + } + }, + "403": { + "description": "Invalid jwt token (or expired)", + "schema": { + "$ref": "#/definitions/main.KError" + } + }, + "413": { + "description": "File too large", + "schema": { + "$ref": "#/definitions/main.KError" + } + }, + "422": { + "description": "Missing or invalid logo file", + "schema": { + "$ref": "#/definitions/main.KError" + } + } + } + }, + "delete": { + "security": [ + { + "Jwt": [] + } + ], + "description": "Delete the current user's manually uploaded profile picture", + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Delete my logo", + "responses": { + "204": { + "description": "No Content" + }, + "401": { + "description": "Missing jwt token", + "schema": { + "$ref": "#/definitions/main.KError" + } + }, + "403": { + "description": "Invalid jwt token (or expired)", + "schema": { + "$ref": "#/definitions/main.KError" + } + } + } + } + }, "/users/me/password": { "patch": { "security": [ @@ -578,7 +980,7 @@ const docTemplate = `{ "name": "user", "in": "body", "schema": { - "$ref": "#/definitions/main.EditPasswordDto" + "$ref": "#/definitions/models.EditPasswordDto" } } ], @@ -595,6 +997,49 @@ const docTemplate = `{ } } }, + "/users/me/{id}": { + "delete": { + "security": [ + { + "Jwt": [] + } + ], + "description": "Delete the user's manually uploaded profile picture", + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Delete user logo", + "parameters": [ + { + "type": "string", + "description": "The id or username of the user", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "401": { + "description": "Missing jwt token", + "schema": { + "$ref": "#/definitions/main.KError" + } + }, + "403": { + "description": "Invalid jwt token (or expired)", + "schema": { + "$ref": "#/definitions/main.KError" + } + } + } + } + }, "/users/{id}": { "get": { "security": [ @@ -626,7 +1071,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/main.User" + "$ref": "#/definitions/models.User" } }, "404": { @@ -675,7 +1120,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/main.User" + "$ref": "#/definitions/models.User" } }, "404": { @@ -724,7 +1169,7 @@ const docTemplate = `{ "name": "user", "in": "body", "schema": { - "$ref": "#/definitions/main.EditUserDto" + "$ref": "#/definitions/models.EditUserDto" } } ], @@ -732,7 +1177,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/main.User" + "$ref": "#/definitions/models.User" } }, "403": { @@ -749,6 +1194,104 @@ const docTemplate = `{ } } } + }, + "/users/{id}/logo": { + "get": { + "security": [ + { + "Jwt": [ + "users.read" + ] + } + ], + "description": "Get a user's logo (manual upload if available, gravatar otherwise)", + "produces": [ + "image/*" + ], + "tags": [ + "users" + ], + "summary": "Get user logo", + "parameters": [ + { + "type": "string", + "description": "The id or username of the user", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "file" + } + }, + "404": { + "description": "No gravatar image found for this user", + "schema": { + "$ref": "#/definitions/main.KError" + } + } + } + } + }, + "/users/{id}/sessions": { + "get": { + "security": [ + { + "Jwt": [] + } + ], + "description": "List all active sessions for a user. Listing someone else's sessions requires users.read.", + "produces": [ + "application/json" + ], + "tags": [ + "sessions" + ], + "summary": "List user sessions", + "parameters": [ + { + "type": "string", + "example": "e05089d6-9179-4b5b-a63e-94dd5fc2a397", + "description": "The id or username of the user", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/main.Session" + } + } + }, + "401": { + "description": "Missing jwt token", + "schema": { + "$ref": "#/definitions/main.KError" + } + }, + "403": { + "description": "Missing permissions: users.read.", + "schema": { + "$ref": "#/definitions/main.KError" + } + }, + "404": { + "description": "No user found with id or username", + "schema": { + "$ref": "#/definitions/main.KError" + } + } + } + } } }, "definitions": { @@ -834,40 +1377,6 @@ const docTemplate = `{ } } }, - "main.EditPasswordDto": { - "type": "object", - "required": [ - "password" - ], - "properties": { - "password": { - "type": "string", - "example": "password1234" - } - } - }, - "main.EditUserDto": { - "type": "object", - "properties": { - "claims": { - "type": "object", - "additionalProperties": { - "type": "string" - }, - "example": { - "preferOriginal": " true" - } - }, - "email": { - "type": "string", - "example": "kyoo@zoriya.dev" - }, - "username": { - "type": "string", - "example": "zoriya" - } - } - }, "main.JwkSet": { "type": "object", "properties": { @@ -949,24 +1458,14 @@ const docTemplate = `{ } } }, - "main.OidcHandle": { + "main.OidcInfo": { "type": "object", "properties": { - "id": { - "description": "Id of this oidc handle.", - "type": "string", - "example": "e05089d6-9179-4b5b-a63e-94dd5fc2a397" + "logo": { + "type": "string" }, - "profileUrl": { - "description": "Link to the profile of the user on the external service. Null if unknown or irrelevant.", - "type": "string", - "format": "url", - "example": "https://myanimelist.net/profile/zoriya" - }, - "username": { - "description": "Username of the user on the external service.", - "type": "string", - "example": "zoriya" + "name": { + "type": "string" } } }, @@ -989,13 +1488,13 @@ const docTemplate = `{ } } }, - "main.Page-main_User": { + "main.Page-models_User": { "type": "object", "properties": { "items": { "type": "array", "items": { - "$ref": "#/definitions/main.User" + "$ref": "#/definitions/models.User" } }, "next": { @@ -1008,29 +1507,20 @@ const docTemplate = `{ } } }, - "main.RegisterDto": { + "main.ServerInfo": { "type": "object", - "required": [ - "email", - "password", - "username" - ], "properties": { - "email": { - "description": "Valid email that could be used for forgotten password requests. Can be used for login.", - "type": "string", - "format": "email", - "example": "kyoo@zoriya.dev" + "allowRegister": { + "type": "boolean" }, - "password": { - "description": "Password to use.", - "type": "string", - "example": "password1234" + "oidc": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/main.OidcInfo" + } }, - "username": { - "description": "Username of the new account, can't contain @ signs. Can be used for login.", - "type": "string", - "example": "zoriya" + "publicUrl": { + "type": "string" } } }, @@ -1059,6 +1549,34 @@ const docTemplate = `{ } } }, + "main.SessionWCurrent": { + "type": "object", + "properties": { + "createdDate": { + "description": "When was the session first opened", + "type": "string", + "example": "2025-03-29T18:20:05.267Z" + }, + "current": { + "type": "boolean" + }, + "device": { + "description": "Device that created the session.", + "type": "string", + "example": "Web - Firefox" + }, + "id": { + "description": "Unique id of this session. Can be used for calls to DELETE", + "type": "string", + "example": "e05089d6-9179-4b5b-a63e-94dd5fc2a397" + }, + "lastUsed": { + "description": "Last date this session was used to access a service.", + "type": "string", + "example": "2025-03-29T18:20:05.267Z" + } + } + }, "main.SessionWToken": { "type": "object", "properties": { @@ -1088,7 +1606,92 @@ const docTemplate = `{ } } }, - "main.User": { + "models.EditPasswordDto": { + "type": "object", + "required": [ + "newPassword" + ], + "properties": { + "newPassword": { + "type": "string", + "example": "password1234" + }, + "oldPassword": { + "type": "string", + "example": "password1234" + } + } + }, + "models.EditUserDto": { + "type": "object", + "properties": { + "claims": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "example": { + "preferOriginal": " true" + } + }, + "email": { + "type": "string", + "example": "kyoo@zoriya.dev" + }, + "username": { + "type": "string", + "example": "zoriya" + } + } + }, + "models.OidcHandle": { + "type": "object", + "properties": { + "id": { + "description": "Id of this oidc handle.", + "type": "string", + "example": "e05089d6-9179-4b5b-a63e-94dd5fc2a397" + }, + "profileUrl": { + "description": "Link to the profile of the user on the external service. Null if unknown or irrelevant.", + "type": "string", + "format": "url", + "example": "https://myanimelist.net/profile/zoriya" + }, + "username": { + "description": "Username of the user on the external service.", + "type": "string", + "example": "zoriya" + } + } + }, + "models.RegisterDto": { + "type": "object", + "required": [ + "email", + "password", + "username" + ], + "properties": { + "email": { + "description": "Valid email that could be used for forgotten password requests. Can be used for login.", + "type": "string", + "format": "email", + "example": "kyoo@zoriya.dev" + }, + "password": { + "description": "Password to use.", + "type": "string", + "example": "password1234" + }, + "username": { + "description": "Username of the new account, can't contain @ signs. Can be used for login.", + "type": "string", + "example": "zoriya" + } + } + }, + "models.User": { "type": "object", "properties": { "claims": { @@ -1112,6 +1715,10 @@ const docTemplate = `{ "format": "email", "example": "kyoo@zoriya.dev" }, + "hasPassword": { + "description": "False if the user has never setup a password and only used oidc.", + "type": "boolean" + }, "id": { "description": "Id of the user.", "type": "string", @@ -1126,7 +1733,7 @@ const docTemplate = `{ "description": "List of other login method available for this user. Access tokens wont be returned here.", "type": "object", "additionalProperties": { - "$ref": "#/definitions/main.OidcHandle" + "$ref": "#/definitions/models.OidcHandle" } }, "username": { diff --git a/auth/docs/swagger.json b/auth/docs/swagger.json index fb41edc9..6bb6c614 100644 --- a/auth/docs/swagger.json +++ b/auth/docs/swagger.json @@ -36,6 +36,26 @@ } } }, + "/info": { + "get": { + "description": "List keibi's settings (oidc providers, public url...)", + "produces": [ + "application/json" + ], + "tags": [ + "oidc" + ], + "summary": "Auth info", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.ServerInfo" + } + } + } + } + }, "/jwt": { "get": { "security": [ @@ -193,7 +213,252 @@ } } }, + "/oidc/callback/{provider}": { + "get": { + "description": "Exchange an opaque OIDC token for a local session.", + "produces": [ + "application/json" + ], + "tags": [ + "oidc" + ], + "summary": "OIDC callback", + "parameters": [ + { + "type": "string", + "example": "google", + "description": "OIDC provider id", + "name": "provider", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Opaque token returned by /oidc/logged/:provider", + "name": "token", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Optional tenant passthrough for federated setups", + "name": "tenant", + "in": "query" + }, + { + "type": "string", + "description": "Bearer token to link provider to current account", + "name": "Authorization", + "in": "header" + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/main.SessionWToken" + } + }, + "404": { + "description": "Unknown OIDC provider", + "schema": { + "$ref": "#/definitions/main.KError" + } + }, + "410": { + "description": "Login token expired or already used", + "schema": { + "$ref": "#/definitions/main.KError" + } + } + } + } + }, + "/oidc/logged/{provider}": { + "get": { + "description": "Callback endpoint called by OIDC providers after login.", + "produces": [ + "application/json" + ], + "tags": [ + "oidc" + ], + "summary": "OIDC logged callback", + "parameters": [ + { + "type": "string", + "example": "google", + "description": "OIDC provider id", + "name": "provider", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "State value returned by the provider", + "name": "state", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Authorization code", + "name": "code", + "in": "query" + }, + { + "type": "string", + "description": "Provider callback error", + "name": "error", + "in": "query" + } + ], + "responses": { + "302": { + "description": "Found" + }, + "400": { + "description": "Invalid state", + "schema": { + "$ref": "#/definitions/main.KError" + } + }, + "404": { + "description": "Unknown OIDC provider", + "schema": { + "$ref": "#/definitions/main.KError" + } + } + } + } + }, + "/oidc/login/{provider}": { + "get": { + "description": "Start an OIDC login with a provider.", + "produces": [ + "application/json" + ], + "tags": [ + "oidc" + ], + "summary": "OIDC login", + "parameters": [ + { + "type": "string", + "example": "google", + "description": "OIDC provider id", + "name": "provider", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "URL to redirect the browser to after provider callback", + "name": "redirectUrl", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Optional tenant passthrough for federated setups", + "name": "tenant", + "in": "query" + } + ], + "responses": { + "302": { + "description": "Found" + }, + "400": { + "description": "Missing redirectUrl", + "schema": { + "$ref": "#/definitions/main.KError" + } + }, + "404": { + "description": "Unknown OIDC provider", + "schema": { + "$ref": "#/definitions/main.KError" + } + } + } + }, + "delete": { + "security": [ + { + "Jwt": [] + } + ], + "description": "Remove an OIDC provider from the current account.", + "produces": [ + "application/json" + ], + "tags": [ + "oidc" + ], + "summary": "OIDC unlink provider", + "parameters": [ + { + "type": "string", + "example": "google", + "description": "OIDC provider id", + "name": "provider", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "404": { + "description": "Unknown OIDC provider", + "schema": { + "$ref": "#/definitions/main.KError" + } + } + } + } + }, "/sessions": { + "get": { + "security": [ + { + "Jwt": [] + } + ], + "description": "List all active sessions for the currently connected user", + "produces": [ + "application/json" + ], + "tags": [ + "sessions" + ], + "summary": "List my sessions", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/main.SessionWCurrent" + } + } + }, + "401": { + "description": "Missing jwt token", + "schema": { + "$ref": "#/definitions/main.KError" + } + }, + "403": { + "description": "Invalid jwt token (or expired)", + "schema": { + "$ref": "#/definitions/main.KError" + } + } + } + }, "post": { "description": "Login to your account and open a session", "consumes": [ @@ -368,7 +633,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/main.Page-main_User" + "$ref": "#/definitions/main.Page-models_User" } }, "422": { @@ -404,7 +669,7 @@ "name": "user", "in": "body", "schema": { - "$ref": "#/definitions/main.RegisterDto" + "$ref": "#/definitions/models.RegisterDto" } } ], @@ -415,6 +680,12 @@ "$ref": "#/definitions/main.SessionWToken" } }, + "403": { + "description": "Registrations are disabled", + "schema": { + "$ref": "#/definitions/main.KError" + } + }, "409": { "description": "Duplicated email or username", "schema": { @@ -449,7 +720,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/main.User" + "$ref": "#/definitions/models.User" } }, "401": { @@ -487,7 +758,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/main.User" + "$ref": "#/definitions/models.User" } } } @@ -515,7 +786,7 @@ "name": "user", "in": "body", "schema": { - "$ref": "#/definitions/main.EditUserDto" + "$ref": "#/definitions/models.EditUserDto" } } ], @@ -523,7 +794,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/main.User" + "$ref": "#/definitions/models.User" } }, "403": { @@ -541,6 +812,137 @@ } } }, + "/users/me/logo": { + "get": { + "security": [ + { + "Jwt": [] + } + ], + "description": "Get the current user's logo (manual upload if available, gravatar otherwise)", + "produces": [ + "image/*" + ], + "tags": [ + "users" + ], + "summary": "Get my logo", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "file" + } + }, + "401": { + "description": "Missing jwt token", + "schema": { + "$ref": "#/definitions/main.KError" + } + }, + "403": { + "description": "Invalid jwt token (or expired)", + "schema": { + "$ref": "#/definitions/main.KError" + } + }, + "404": { + "description": "No gravatar image found for this user", + "schema": { + "$ref": "#/definitions/main.KError" + } + } + } + }, + "post": { + "security": [ + { + "Jwt": [] + } + ], + "description": "Upload a manual profile picture for the current user", + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Upload my logo", + "parameters": [ + { + "type": "file", + "description": "Profile picture image (jpeg/png/gif/webp, max 5MB)", + "name": "logo", + "in": "formData", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "401": { + "description": "Missing jwt token", + "schema": { + "$ref": "#/definitions/main.KError" + } + }, + "403": { + "description": "Invalid jwt token (or expired)", + "schema": { + "$ref": "#/definitions/main.KError" + } + }, + "413": { + "description": "File too large", + "schema": { + "$ref": "#/definitions/main.KError" + } + }, + "422": { + "description": "Missing or invalid logo file", + "schema": { + "$ref": "#/definitions/main.KError" + } + } + } + }, + "delete": { + "security": [ + { + "Jwt": [] + } + ], + "description": "Delete the current user's manually uploaded profile picture", + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Delete my logo", + "responses": { + "204": { + "description": "No Content" + }, + "401": { + "description": "Missing jwt token", + "schema": { + "$ref": "#/definitions/main.KError" + } + }, + "403": { + "description": "Invalid jwt token (or expired)", + "schema": { + "$ref": "#/definitions/main.KError" + } + } + } + } + }, "/users/me/password": { "patch": { "security": [ @@ -572,7 +974,7 @@ "name": "user", "in": "body", "schema": { - "$ref": "#/definitions/main.EditPasswordDto" + "$ref": "#/definitions/models.EditPasswordDto" } } ], @@ -589,6 +991,49 @@ } } }, + "/users/me/{id}": { + "delete": { + "security": [ + { + "Jwt": [] + } + ], + "description": "Delete the user's manually uploaded profile picture", + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Delete user logo", + "parameters": [ + { + "type": "string", + "description": "The id or username of the user", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "401": { + "description": "Missing jwt token", + "schema": { + "$ref": "#/definitions/main.KError" + } + }, + "403": { + "description": "Invalid jwt token (or expired)", + "schema": { + "$ref": "#/definitions/main.KError" + } + } + } + } + }, "/users/{id}": { "get": { "security": [ @@ -620,7 +1065,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/main.User" + "$ref": "#/definitions/models.User" } }, "404": { @@ -669,7 +1114,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/main.User" + "$ref": "#/definitions/models.User" } }, "404": { @@ -718,7 +1163,7 @@ "name": "user", "in": "body", "schema": { - "$ref": "#/definitions/main.EditUserDto" + "$ref": "#/definitions/models.EditUserDto" } } ], @@ -726,7 +1171,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/main.User" + "$ref": "#/definitions/models.User" } }, "403": { @@ -743,6 +1188,104 @@ } } } + }, + "/users/{id}/logo": { + "get": { + "security": [ + { + "Jwt": [ + "users.read" + ] + } + ], + "description": "Get a user's logo (manual upload if available, gravatar otherwise)", + "produces": [ + "image/*" + ], + "tags": [ + "users" + ], + "summary": "Get user logo", + "parameters": [ + { + "type": "string", + "description": "The id or username of the user", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "file" + } + }, + "404": { + "description": "No gravatar image found for this user", + "schema": { + "$ref": "#/definitions/main.KError" + } + } + } + } + }, + "/users/{id}/sessions": { + "get": { + "security": [ + { + "Jwt": [] + } + ], + "description": "List all active sessions for a user. Listing someone else's sessions requires users.read.", + "produces": [ + "application/json" + ], + "tags": [ + "sessions" + ], + "summary": "List user sessions", + "parameters": [ + { + "type": "string", + "example": "e05089d6-9179-4b5b-a63e-94dd5fc2a397", + "description": "The id or username of the user", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/main.Session" + } + } + }, + "401": { + "description": "Missing jwt token", + "schema": { + "$ref": "#/definitions/main.KError" + } + }, + "403": { + "description": "Missing permissions: users.read.", + "schema": { + "$ref": "#/definitions/main.KError" + } + }, + "404": { + "description": "No user found with id or username", + "schema": { + "$ref": "#/definitions/main.KError" + } + } + } + } } }, "definitions": { @@ -828,40 +1371,6 @@ } } }, - "main.EditPasswordDto": { - "type": "object", - "required": [ - "password" - ], - "properties": { - "password": { - "type": "string", - "example": "password1234" - } - } - }, - "main.EditUserDto": { - "type": "object", - "properties": { - "claims": { - "type": "object", - "additionalProperties": { - "type": "string" - }, - "example": { - "preferOriginal": " true" - } - }, - "email": { - "type": "string", - "example": "kyoo@zoriya.dev" - }, - "username": { - "type": "string", - "example": "zoriya" - } - } - }, "main.JwkSet": { "type": "object", "properties": { @@ -943,24 +1452,14 @@ } } }, - "main.OidcHandle": { + "main.OidcInfo": { "type": "object", "properties": { - "id": { - "description": "Id of this oidc handle.", - "type": "string", - "example": "e05089d6-9179-4b5b-a63e-94dd5fc2a397" + "logo": { + "type": "string" }, - "profileUrl": { - "description": "Link to the profile of the user on the external service. Null if unknown or irrelevant.", - "type": "string", - "format": "url", - "example": "https://myanimelist.net/profile/zoriya" - }, - "username": { - "description": "Username of the user on the external service.", - "type": "string", - "example": "zoriya" + "name": { + "type": "string" } } }, @@ -983,13 +1482,13 @@ } } }, - "main.Page-main_User": { + "main.Page-models_User": { "type": "object", "properties": { "items": { "type": "array", "items": { - "$ref": "#/definitions/main.User" + "$ref": "#/definitions/models.User" } }, "next": { @@ -1002,29 +1501,20 @@ } } }, - "main.RegisterDto": { + "main.ServerInfo": { "type": "object", - "required": [ - "email", - "password", - "username" - ], "properties": { - "email": { - "description": "Valid email that could be used for forgotten password requests. Can be used for login.", - "type": "string", - "format": "email", - "example": "kyoo@zoriya.dev" + "allowRegister": { + "type": "boolean" }, - "password": { - "description": "Password to use.", - "type": "string", - "example": "password1234" + "oidc": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/main.OidcInfo" + } }, - "username": { - "description": "Username of the new account, can't contain @ signs. Can be used for login.", - "type": "string", - "example": "zoriya" + "publicUrl": { + "type": "string" } } }, @@ -1053,6 +1543,34 @@ } } }, + "main.SessionWCurrent": { + "type": "object", + "properties": { + "createdDate": { + "description": "When was the session first opened", + "type": "string", + "example": "2025-03-29T18:20:05.267Z" + }, + "current": { + "type": "boolean" + }, + "device": { + "description": "Device that created the session.", + "type": "string", + "example": "Web - Firefox" + }, + "id": { + "description": "Unique id of this session. Can be used for calls to DELETE", + "type": "string", + "example": "e05089d6-9179-4b5b-a63e-94dd5fc2a397" + }, + "lastUsed": { + "description": "Last date this session was used to access a service.", + "type": "string", + "example": "2025-03-29T18:20:05.267Z" + } + } + }, "main.SessionWToken": { "type": "object", "properties": { @@ -1082,7 +1600,92 @@ } } }, - "main.User": { + "models.EditPasswordDto": { + "type": "object", + "required": [ + "newPassword" + ], + "properties": { + "newPassword": { + "type": "string", + "example": "password1234" + }, + "oldPassword": { + "type": "string", + "example": "password1234" + } + } + }, + "models.EditUserDto": { + "type": "object", + "properties": { + "claims": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "example": { + "preferOriginal": " true" + } + }, + "email": { + "type": "string", + "example": "kyoo@zoriya.dev" + }, + "username": { + "type": "string", + "example": "zoriya" + } + } + }, + "models.OidcHandle": { + "type": "object", + "properties": { + "id": { + "description": "Id of this oidc handle.", + "type": "string", + "example": "e05089d6-9179-4b5b-a63e-94dd5fc2a397" + }, + "profileUrl": { + "description": "Link to the profile of the user on the external service. Null if unknown or irrelevant.", + "type": "string", + "format": "url", + "example": "https://myanimelist.net/profile/zoriya" + }, + "username": { + "description": "Username of the user on the external service.", + "type": "string", + "example": "zoriya" + } + } + }, + "models.RegisterDto": { + "type": "object", + "required": [ + "email", + "password", + "username" + ], + "properties": { + "email": { + "description": "Valid email that could be used for forgotten password requests. Can be used for login.", + "type": "string", + "format": "email", + "example": "kyoo@zoriya.dev" + }, + "password": { + "description": "Password to use.", + "type": "string", + "example": "password1234" + }, + "username": { + "description": "Username of the new account, can't contain @ signs. Can be used for login.", + "type": "string", + "example": "zoriya" + } + } + }, + "models.User": { "type": "object", "properties": { "claims": { @@ -1106,6 +1709,10 @@ "format": "email", "example": "kyoo@zoriya.dev" }, + "hasPassword": { + "description": "False if the user has never setup a password and only used oidc.", + "type": "boolean" + }, "id": { "description": "Id of the user.", "type": "string", @@ -1120,7 +1727,7 @@ "description": "List of other login method available for this user. Access tokens wont be returned here.", "type": "object", "additionalProperties": { - "$ref": "#/definitions/main.OidcHandle" + "$ref": "#/definitions/models.OidcHandle" } }, "username": { diff --git a/auth/docs/swagger.yaml b/auth/docs/swagger.yaml new file mode 100644 index 00000000..62a29f31 --- /dev/null +++ b/auth/docs/swagger.yaml @@ -0,0 +1,1164 @@ +basePath: /auth +definitions: + main.ApiKey: + properties: + claims: + additionalProperties: + type: string + example: + isAdmin: ' true' + type: object + createAt: + example: "2025-03-29T18:20:05.267Z" + type: string + id: + example: e05089d6-9179-4b5b-a63e-94dd5fc2a397 + type: string + lastUsed: + example: "2025-03-29T18:20:05.267Z" + type: string + name: + example: myapp + type: string + type: object + main.ApiKeyDto: + properties: + claims: + additionalProperties: + type: string + example: + isAdmin: ' true' + type: object + name: + example: myapp + type: string + type: object + main.ApiKeyWToken: + properties: + claims: + additionalProperties: + type: string + example: + isAdmin: ' true' + type: object + createAt: + example: "2025-03-29T18:20:05.267Z" + type: string + id: + example: e05089d6-9179-4b5b-a63e-94dd5fc2a397 + type: string + lastUsed: + example: "2025-03-29T18:20:05.267Z" + type: string + name: + example: myapp + type: string + token: + example: myapp-lyHzTYm9yi+pkEv3m2tamAeeK7Dj7N3QRP7xv7dPU5q9MAe8tU4ySwYczE0RaMr4fijsA== + type: string + type: object + main.JwkSet: + properties: + keys: + items: + properties: + e: + example: AQAB + type: string + key_ops: + example: + - '[verify]' + items: + type: string + type: array + kty: + example: RSA + type: string + "n": + example: oBcXcJUR-Sb8_b4qIj28LRAPxdF_6odRr52K5-ymiEkR2DOlEuXBtM-biWxPESW-U-zhfHzdVLf6ioy5xL0bJTh8BMIorkrDliN3vb81jCvyOMgZ7ATMJpMAQMmSDN7sL3U45r22FaoQufCJMQHmUsZPecdQSgj2aFBiRXxsLleYlSezdBVT_gKH-coqeYXSC_hk-ezSq4aDZ10BlDnZ-FA7-ES3T7nBmJEAU7KDAGeSvbYAfYimOW0r-Vc0xQNuwGCfzZtSexKXDbYbNwOVo3SjfCabq-gMfap_owcHbKicGBZu1LDlh7CpkmLQf_kv6GihM2LWFFh6Vwg2cltiwF22EIPlUDtYTkUR0qRkdNJaNkwV5Vv_6r3pzSmu5ovRriKtlrvJMjlTnLb4_ltsge3fw5Z34cJrsp094FbUc2O6Or4FGEXUldieJCnVRhs2_h6SDcmeMXs1zfvE5GlDnq8tZV6WMJ5Sb4jNO7rs_hTkr23_E6mVg-DdtozGfqzRzhIjPym6D_jVfR6dZv5W0sKwOHRmT7nYq-C7b2sAwmNNII296M4Rq-jn0b5pgSeMDYbIpbIA4thU8LYU0lBZp_ZVwWKG1RFZDxz3k9O5UVth2kTpTWlwn0hB1aAvgXHo6in1CScITGA72p73RbDieNnLFaCK4xUVstkWAKLqPxs + type: string + use: + example: sig + type: string + type: object + type: array + type: object + main.Jwt: + properties: + token: + description: The jwt token you can use for all authorized call to either keibi + or other services. + example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30 + type: string + type: object + main.KError: + properties: + details: {} + message: + example: No user found with this id + type: string + status: + example: 404 + type: integer + type: object + main.LoginDto: + properties: + login: + description: Either the email or the username. + example: zoriya + type: string + password: + description: Password of the account. + example: password1234 + type: string + required: + - login + - password + type: object + main.OidcInfo: + properties: + logo: + type: string + name: + type: string + type: object + main.Page-main_ApiKey: + properties: + items: + items: + $ref: '#/definitions/main.ApiKey' + type: array + next: + example: https://kyoo.zoriya.dev/auth/users?after=aoeusth + type: string + this: + example: https://kyoo.zoriya.dev/auth/users + type: string + type: object + main.Page-models_User: + properties: + items: + items: + $ref: '#/definitions/models.User' + type: array + next: + example: https://kyoo.zoriya.dev/auth/users?after=aoeusth + type: string + this: + example: https://kyoo.zoriya.dev/auth/users + type: string + type: object + main.ServerInfo: + properties: + allowRegister: + type: boolean + oidc: + additionalProperties: + $ref: '#/definitions/main.OidcInfo' + type: object + publicUrl: + type: string + type: object + main.Session: + properties: + createdDate: + description: When was the session first opened + example: "2025-03-29T18:20:05.267Z" + type: string + device: + description: Device that created the session. + example: Web - Firefox + type: string + id: + description: Unique id of this session. Can be used for calls to DELETE + example: e05089d6-9179-4b5b-a63e-94dd5fc2a397 + type: string + lastUsed: + description: Last date this session was used to access a service. + example: "2025-03-29T18:20:05.267Z" + type: string + type: object + main.SessionWCurrent: + properties: + createdDate: + description: When was the session first opened + example: "2025-03-29T18:20:05.267Z" + type: string + current: + type: boolean + device: + description: Device that created the session. + example: Web - Firefox + type: string + id: + description: Unique id of this session. Can be used for calls to DELETE + example: e05089d6-9179-4b5b-a63e-94dd5fc2a397 + type: string + lastUsed: + description: Last date this session was used to access a service. + example: "2025-03-29T18:20:05.267Z" + type: string + type: object + main.SessionWToken: + properties: + createdDate: + description: When was the session first opened + example: "2025-03-29T18:20:05.267Z" + type: string + device: + description: Device that created the session. + example: Web - Firefox + type: string + id: + description: Unique id of this session. Can be used for calls to DELETE + example: e05089d6-9179-4b5b-a63e-94dd5fc2a397 + type: string + lastUsed: + description: Last date this session was used to access a service. + example: "2025-03-29T18:20:05.267Z" + type: string + token: + example: lyHzTYm9yi+pkEv3m2tamAeeK7Dj7N3QRP7xv7dPU5q9MAe8tU4ySwYczE0RaMr4fijsA== + type: string + type: object + models.EditPasswordDto: + properties: + newPassword: + example: password1234 + type: string + oldPassword: + example: password1234 + type: string + required: + - newPassword + type: object + models.EditUserDto: + properties: + claims: + additionalProperties: + type: string + example: + preferOriginal: ' true' + type: object + email: + example: kyoo@zoriya.dev + type: string + username: + example: zoriya + type: string + type: object + models.OidcHandle: + properties: + id: + description: Id of this oidc handle. + example: e05089d6-9179-4b5b-a63e-94dd5fc2a397 + type: string + profileUrl: + description: Link to the profile of the user on the external service. Null + if unknown or irrelevant. + example: https://myanimelist.net/profile/zoriya + format: url + type: string + username: + description: Username of the user on the external service. + example: zoriya + type: string + type: object + models.RegisterDto: + properties: + email: + description: Valid email that could be used for forgotten password requests. + Can be used for login. + example: kyoo@zoriya.dev + format: email + type: string + password: + description: Password to use. + example: password1234 + type: string + username: + description: Username of the new account, can't contain @ signs. Can be used + for login. + example: zoriya + type: string + required: + - email + - password + - username + type: object + models.User: + properties: + claims: + additionalProperties: + type: string + description: List of custom claims JWT created via get /jwt will have + example: + isAdmin: ' true' + type: object + createdDate: + description: When was this account created? + example: "2025-03-29T18:20:05.267Z" + type: string + email: + description: Email of the user. Can be used as a login. + example: kyoo@zoriya.dev + format: email + type: string + hasPassword: + description: False if the user has never setup a password and only used oidc. + type: boolean + id: + description: Id of the user. + example: e05089d6-9179-4b5b-a63e-94dd5fc2a397 + type: string + lastSeen: + description: When was the last time this account made any authorized request? + example: "2025-03-29T18:20:05.267Z" + type: string + oidc: + additionalProperties: + $ref: '#/definitions/models.OidcHandle' + description: List of other login method available for this user. Access tokens + wont be returned here. + type: object + username: + description: Username of the user. Can be used as a login. + example: zoriya + type: string + type: object +host: kyoo.zoriya.dev +info: + contact: + name: Repository + url: https://github.com/zoriya/kyoo + description: Auth system made for kyoo. + license: + name: GPL-3.0 + url: https://www.gnu.org/licenses/gpl-3.0.en.html + title: Keibi - Kyoo's auth + version: "1.0" +paths: + /.well-known/jwks.json: + get: + description: Get the jwks info, used to validate jwts. + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/main.JwkSet' + summary: Jwks + tags: + - jwt + /info: + get: + description: List keibi's settings (oidc providers, public url...) + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/main.ServerInfo' + summary: Auth info + tags: + - oidc + /jwt: + get: + description: Convert a session token or an API key to a short lived JWT. + produces: + - application/json + responses: + "200": + description: OK + headers: + Authorization: + description: Jwt (same value as the returned token) + type: string + schema: + $ref: '#/definitions/main.Jwt' + "403": + description: Invalid session token (or expired) + schema: + $ref: '#/definitions/main.KError' + security: + - Token: [] + summary: Get JWT + tags: + - jwt + /keys: + delete: + consumes: + - application/json + description: Delete an existing API key + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/main.ApiKey' + "404": + description: Invalid id + schema: + $ref: '#/definitions/main.KError' + "422": + description: Invalid id format + schema: + $ref: '#/definitions/main.KError' + security: + - Jwt: + - apikeys.write + summary: Delete API key + tags: + - apikeys + get: + consumes: + - application/json + description: List all api keys + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/main.Page-main_ApiKey' + security: + - Jwt: + - apikeys.read + summary: List API keys + tags: + - apikeys + post: + consumes: + - application/json + description: Create a new API key + parameters: + - description: Api key info + in: body + name: key + schema: + $ref: '#/definitions/main.ApiKeyDto' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/main.ApiKeyWToken' + "409": + description: Duplicated api key + schema: + $ref: '#/definitions/main.KError' + "422": + description: Invalid create body + schema: + $ref: '#/definitions/main.KError' + security: + - Jwt: + - apikeys.write + summary: Create API key + tags: + - apikeys + /oidc/callback/{provider}: + get: + description: Exchange an opaque OIDC token for a local session. + parameters: + - description: OIDC provider id + example: google + in: path + name: provider + required: true + type: string + - description: Opaque token returned by /oidc/logged/:provider + in: query + name: token + required: true + type: string + - description: Optional tenant passthrough for federated setups + in: query + name: tenant + type: string + - description: Bearer token to link provider to current account + in: header + name: Authorization + type: string + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/main.SessionWToken' + "404": + description: Unknown OIDC provider + schema: + $ref: '#/definitions/main.KError' + "410": + description: Login token expired or already used + schema: + $ref: '#/definitions/main.KError' + summary: OIDC callback + tags: + - oidc + /oidc/logged/{provider}: + get: + description: Callback endpoint called by OIDC providers after login. + parameters: + - description: OIDC provider id + example: google + in: path + name: provider + required: true + type: string + - description: State value returned by the provider + in: query + name: state + required: true + type: string + - description: Authorization code + in: query + name: code + type: string + - description: Provider callback error + in: query + name: error + type: string + produces: + - application/json + responses: + "302": + description: Found + "400": + description: Invalid state + schema: + $ref: '#/definitions/main.KError' + "404": + description: Unknown OIDC provider + schema: + $ref: '#/definitions/main.KError' + summary: OIDC logged callback + tags: + - oidc + /oidc/login/{provider}: + delete: + description: Remove an OIDC provider from the current account. + parameters: + - description: OIDC provider id + example: google + in: path + name: provider + required: true + type: string + produces: + - application/json + responses: + "204": + description: No Content + "404": + description: Unknown OIDC provider + schema: + $ref: '#/definitions/main.KError' + security: + - Jwt: [] + summary: OIDC unlink provider + tags: + - oidc + get: + description: Start an OIDC login with a provider. + parameters: + - description: OIDC provider id + example: google + in: path + name: provider + required: true + type: string + - description: URL to redirect the browser to after provider callback + in: query + name: redirectUrl + required: true + type: string + - description: Optional tenant passthrough for federated setups + in: query + name: tenant + type: string + produces: + - application/json + responses: + "302": + description: Found + "400": + description: Missing redirectUrl + schema: + $ref: '#/definitions/main.KError' + "404": + description: Unknown OIDC provider + schema: + $ref: '#/definitions/main.KError' + summary: OIDC login + tags: + - oidc + /sessions: + get: + description: List all active sessions for the currently connected user + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/main.SessionWCurrent' + type: array + "401": + description: Missing jwt token + schema: + $ref: '#/definitions/main.KError' + "403": + description: Invalid jwt token (or expired) + schema: + $ref: '#/definitions/main.KError' + security: + - Jwt: [] + summary: List my sessions + tags: + - sessions + post: + consumes: + - application/json + description: Login to your account and open a session + parameters: + - description: The device the created session will be used on + example: android tv + in: query + name: device + type: string + - description: Account informations + in: body + name: login + schema: + $ref: '#/definitions/main.LoginDto' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/main.SessionWToken' + "403": + description: Invalid password + schema: + $ref: '#/definitions/main.KError' + "404": + description: Account does not exists + schema: + $ref: '#/definitions/main.KError' + "422": + description: User does not have a password (registered via oidc, please + login via oidc) + schema: + $ref: '#/definitions/main.KError' + summary: Login + tags: + - sessions + /sessions/{id}: + delete: + description: Delete a session and logout + parameters: + - description: The id of the session to delete + example: e05089d6-9179-4b5b-a63e-94dd5fc2a397 + format: uuid + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/main.Session' + "404": + description: Session not found with specified id (if not using the /current + route) + schema: + $ref: '#/definitions/main.KError' + "422": + description: Invalid session id + schema: + $ref: '#/definitions/main.KError' + security: + - Jwt: [] + summary: Delete other session + tags: + - sessions + /sessions/current: + delete: + description: Delete a session and logout + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/main.Session' + "401": + description: Missing jwt token + schema: + $ref: '#/definitions/main.KError' + "403": + description: Invalid jwt token (or expired) + schema: + $ref: '#/definitions/main.KError' + security: + - Jwt: [] + summary: Logout + tags: + - sessions + /users: + get: + consumes: + - application/json + description: List all users existing in this instance. + parameters: + - description: used for pagination. + in: query + name: after + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/main.Page-models_User' + "422": + description: Invalid after id + schema: + $ref: '#/definitions/main.KError' + security: + - Jwt: + - users.read + summary: List all users + tags: + - users + post: + consumes: + - application/json + description: Register as a new user and open a session for it + parameters: + - description: The device the created session will be used on + example: android + in: query + name: device + type: string + - description: Registration informations + in: body + name: user + schema: + $ref: '#/definitions/models.RegisterDto' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/main.SessionWToken' + "403": + description: Registrations are disabled + schema: + $ref: '#/definitions/main.KError' + "409": + description: Duplicated email or username + schema: + $ref: '#/definitions/main.KError' + "422": + description: Invalid register body + schema: + $ref: '#/definitions/main.KError' + summary: Register + tags: + - users + /users/{id}: + delete: + consumes: + - application/json + description: Delete an account and all it's sessions. + parameters: + - description: User id of the user to delete + format: uuid + in: path + name: id + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.User' + "404": + description: Invalid user id + schema: + $ref: '#/definitions/main.KError' + "422": + description: Invalid id format + schema: + $ref: '#/definitions/main.KError' + security: + - Jwt: + - users.delete + summary: Delete user + tags: + - users + get: + description: Get informations about a user from it's id + parameters: + - description: The id of the user + format: uuid + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.User' + "404": + description: No user with the given id found + schema: + $ref: '#/definitions/main.KError' + "422": + description: Invalid id (not a uuid) + schema: + $ref: '#/definitions/main.KError' + security: + - Jwt: + - users.read + summary: Get user + tags: + - users + patch: + consumes: + - application/json + description: Edit an account info or permissions + parameters: + - description: User id of the user to edit + format: uuid + in: path + name: id + type: string + - description: Edited user info + in: body + name: user + schema: + $ref: '#/definitions/models.EditUserDto' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.User' + "403": + description: You don't have permissions to edit another account + schema: + $ref: '#/definitions/main.KError' + "422": + description: Invalid body + schema: + $ref: '#/definitions/main.KError' + security: + - Jwt: + - users.write + summary: Edit user + tags: + - users + /users/{id}/logo: + get: + description: Get a user's logo (manual upload if available, gravatar otherwise) + parameters: + - description: The id or username of the user + in: path + name: id + required: true + type: string + produces: + - image/* + responses: + "200": + description: OK + schema: + type: file + "404": + description: No gravatar image found for this user + schema: + $ref: '#/definitions/main.KError' + security: + - Jwt: + - users.read + summary: Get user logo + tags: + - users + /users/{id}/sessions: + get: + description: List all active sessions for a user. Listing someone else's sessions + requires users.read. + parameters: + - description: The id or username of the user + example: e05089d6-9179-4b5b-a63e-94dd5fc2a397 + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/main.Session' + type: array + "401": + description: Missing jwt token + schema: + $ref: '#/definitions/main.KError' + "403": + description: 'Missing permissions: users.read.' + schema: + $ref: '#/definitions/main.KError' + "404": + description: No user found with id or username + schema: + $ref: '#/definitions/main.KError' + security: + - Jwt: [] + summary: List user sessions + tags: + - sessions + /users/me: + delete: + consumes: + - application/json + description: Delete your account and all your sessions + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.User' + security: + - Jwt: [] + summary: Delete self + tags: + - users + get: + description: Get informations about the currently connected user + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.User' + "401": + description: Missing jwt token + schema: + $ref: '#/definitions/main.KError' + "403": + description: Invalid jwt token (or expired) + schema: + $ref: '#/definitions/main.KError' + security: + - Jwt: [] + summary: Get me + tags: + - users + patch: + consumes: + - application/json + description: Edit your account's info + parameters: + - description: Edited user info + in: body + name: user + schema: + $ref: '#/definitions/models.EditUserDto' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.User' + "403": + description: You can't edit a protected claim + schema: + $ref: '#/definitions/main.KError' + "422": + description: Invalid body + schema: + $ref: '#/definitions/main.KError' + security: + - Jwt: [] + summary: Edit self + tags: + - users + /users/me/{id}: + delete: + description: Delete the user's manually uploaded profile picture + parameters: + - description: The id or username of the user + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "204": + description: No Content + "401": + description: Missing jwt token + schema: + $ref: '#/definitions/main.KError' + "403": + description: Invalid jwt token (or expired) + schema: + $ref: '#/definitions/main.KError' + security: + - Jwt: [] + summary: Delete user logo + tags: + - users + /users/me/logo: + delete: + description: Delete the current user's manually uploaded profile picture + produces: + - application/json + responses: + "204": + description: No Content + "401": + description: Missing jwt token + schema: + $ref: '#/definitions/main.KError' + "403": + description: Invalid jwt token (or expired) + schema: + $ref: '#/definitions/main.KError' + security: + - Jwt: [] + summary: Delete my logo + tags: + - users + get: + description: Get the current user's logo (manual upload if available, gravatar + otherwise) + produces: + - image/* + responses: + "200": + description: OK + schema: + type: file + "401": + description: Missing jwt token + schema: + $ref: '#/definitions/main.KError' + "403": + description: Invalid jwt token (or expired) + schema: + $ref: '#/definitions/main.KError' + "404": + description: No gravatar image found for this user + schema: + $ref: '#/definitions/main.KError' + security: + - Jwt: [] + summary: Get my logo + tags: + - users + post: + consumes: + - multipart/form-data + description: Upload a manual profile picture for the current user + parameters: + - description: Profile picture image (jpeg/png/gif/webp, max 5MB) + in: formData + name: logo + required: true + type: file + produces: + - application/json + responses: + "204": + description: No Content + "401": + description: Missing jwt token + schema: + $ref: '#/definitions/main.KError' + "403": + description: Invalid jwt token (or expired) + schema: + $ref: '#/definitions/main.KError' + "413": + description: File too large + schema: + $ref: '#/definitions/main.KError' + "422": + description: Missing or invalid logo file + schema: + $ref: '#/definitions/main.KError' + security: + - Jwt: [] + summary: Upload my logo + tags: + - users + /users/me/password: + patch: + consumes: + - application/json + description: Edit your password + parameters: + - default: true + description: Invalidate other sessions + in: query + name: invalidate + type: boolean + - description: New password + in: body + name: user + schema: + $ref: '#/definitions/models.EditPasswordDto' + produces: + - application/json + responses: + "204": + description: No Content + "422": + description: Invalid body + schema: + $ref: '#/definitions/main.KError' + security: + - Jwt: [] + summary: Edit password + tags: + - users +securityDefinitions: + Jwt: + in: header + name: Authorization + type: apiKey + Token: + in: header + name: Authorization + type: apiKey +swagger: "2.0" diff --git a/auth/go.mod b/auth/go.mod index 8cb35e78..bfaa1f0e 100644 --- a/auth/go.mod +++ b/auth/go.mod @@ -7,15 +7,16 @@ require ( github.com/exaring/otelpgx v0.10.0 github.com/golang-jwt/jwt/v5 v5.3.1 github.com/google/uuid v1.6.0 - github.com/jackc/pgx/v5 v5.8.0 + github.com/jackc/pgx/v5 v5.9.1 github.com/labstack/echo-jwt/v5 v5.0.1 github.com/labstack/echo-opentelemetry v0.0.2 github.com/labstack/echo/v5 v5.0.4 github.com/lestrrat-go/jwx/v3 v3.0.13 - github.com/swaggo/echo-swagger v1.5.0 + github.com/mileusna/useragent v1.3.5 + github.com/swaggo/echo-swagger/v2 v2.0.1 github.com/swaggo/swag v1.16.6 go.opentelemetry.io/contrib/bridges/otelslog v0.17.0 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 go.opentelemetry.io/otel v1.42.0 go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0 go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.18.0 @@ -31,47 +32,47 @@ require ( require ( github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-openapi/swag/conv v0.25.4 // indirect - github.com/go-openapi/swag/jsonname v0.25.4 // indirect - github.com/go-openapi/swag/jsonutils v0.25.4 // indirect - github.com/go-openapi/swag/loading v0.25.4 // indirect - github.com/go-openapi/swag/stringutils v0.25.4 // indirect - github.com/go-openapi/swag/typeutils v0.25.4 // indirect - github.com/go-openapi/swag/yamlutils v0.25.4 // indirect - github.com/goccy/go-json v0.10.5 // indirect + github.com/go-openapi/swag/conv v0.25.5 // indirect + github.com/go-openapi/swag/jsonname v0.25.5 // indirect + github.com/go-openapi/swag/jsonutils v0.25.5 // indirect + github.com/go-openapi/swag/loading v0.25.5 // indirect + github.com/go-openapi/swag/stringutils v0.25.5 // indirect + github.com/go-openapi/swag/typeutils v0.25.5 // indirect + github.com/go-openapi/swag/yamlutils v0.25.5 // indirect + github.com/goccy/go-json v0.10.6 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/lestrrat-go/blackmagic v1.0.4 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect - github.com/lestrrat-go/httprc/v3 v3.0.3 // indirect + github.com/lestrrat-go/httprc/v3 v3.0.4 // indirect github.com/lestrrat-go/option/v2 v2.0.0 // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/segmentio/asm v1.2.1 // indirect - github.com/sv-tools/openapi v0.2.1 // indirect - github.com/swaggo/swag/v2 v2.0.0-rc4 // indirect + github.com/sv-tools/openapi v0.4.0 // indirect + github.com/swaggo/swag/v2 v2.0.0-rc5 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/propagators/b3 v1.40.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 // indirect - go.opentelemetry.io/proto/otlp v1.9.0 // indirect + go.opentelemetry.io/proto/otlp v1.10.0 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/mod v0.32.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect + golang.org/x/mod v0.34.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7 // indirect google.golang.org/grpc v1.79.3 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - sigs.k8s.io/yaml v1.3.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect ) require ( github.com/KyleBanks/depth v1.2.1 // indirect - github.com/gabriel-vasile/mimetype v1.4.12 // indirect - github.com/go-openapi/jsonpointer v0.22.4 // indirect - github.com/go-openapi/jsonreference v0.21.4 // indirect - github.com/go-openapi/spec v0.22.3 // indirect + github.com/gabriel-vasile/mimetype v1.4.13 // indirect + github.com/go-openapi/jsonpointer v0.22.5 // indirect + github.com/go-openapi/jsonreference v0.21.5 // indirect + github.com/go-openapi/spec v0.22.4 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.30.1 @@ -81,17 +82,15 @@ require ( github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/leodido/go-urn v1.4.0 // indirect - github.com/mileusna/useragent v1.3.5 github.com/swaggo/files/v2 v2.0.2 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0 - golang.org/x/crypto v0.48.0 // indirect - golang.org/x/net v0.51.0 // indirect - golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.41.0 // indirect - golang.org/x/text v0.34.0 // indirect - golang.org/x/time v0.14.0 // indirect - golang.org/x/tools v0.41.0 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect + golang.org/x/crypto v0.49.0 // indirect + golang.org/x/net v0.52.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/text v0.35.0 // indirect + golang.org/x/time v0.15.0 // indirect + golang.org/x/tools v0.43.0 // indirect ) diff --git a/auth/go.sum b/auth/go.sum index 063ed7f4..af6acff9 100644 --- a/auth/go.sum +++ b/auth/go.sum @@ -15,11 +15,10 @@ github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/dhui/dktest v0.4.6 h1:+DPKyScKSEp3VLtbMDHcUq6V5Lm5zfZZVb0Sk7Ahom4= github.com/dhui/dktest v0.4.6/go.mod h1:JHTSYDtKkvFNFHJKqCzVzqXecyv+tKt8EzceOmQOgbU= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= @@ -34,40 +33,40 @@ github.com/exaring/otelpgx v0.10.0 h1:NGGegdoBQM3jNZDKG8ENhigUcgBN7d7943L0YlcIpZ github.com/exaring/otelpgx v0.10.0/go.mod h1:R5/M5LWsPPBZc1SrRE5e0DiU48bI78C1/GPTWs6I66U= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= -github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= +github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= -github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= -github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8= -github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= -github.com/go-openapi/spec v0.22.3 h1:qRSmj6Smz2rEBxMnLRBMeBWxbbOvuOoElvSvObIgwQc= -github.com/go-openapi/spec v0.22.3/go.mod h1:iIImLODL2loCh3Vnox8TY2YWYJZjMAKYyLH2Mu8lOZs= +github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA= +github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0= +github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE= +github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw= +github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ= +github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ= github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= -github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= -github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= -github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= -github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= -github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= -github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= -github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= -github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= -github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= -github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= -github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= -github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= -github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= -github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= -github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= -github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= -github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= -github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= +github.com/go-openapi/swag/conv v0.25.5 h1:wAXBYEXJjoKwE5+vc9YHhpQOFj2JYBMF2DUi+tGu97g= +github.com/go-openapi/swag/conv v0.25.5/go.mod h1:CuJ1eWvh1c4ORKx7unQnFGyvBbNlRKbnRyAvDvzWA4k= +github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo= +github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU= +github.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo= +github.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5 h1:SX6sE4FrGb4sEnnxbFL/25yZBb5Hcg1inLeErd86Y1U= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5/go.mod h1:/2KvOTrKWjVA5Xli3DZWdMCZDzz3uV/T7bXwrKWPquo= +github.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU= +github.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g= +github.com/go-openapi/swag/stringutils v0.25.5 h1:NVkoDOA8YBgtAR/zvCx5rhJKtZF3IzXcDdwOsYzrB6M= +github.com/go-openapi/swag/stringutils v0.25.5/go.mod h1:PKK8EZdu4QJq8iezt17HM8RXnLAzY7gW0O1KKarrZII= +github.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzzr9u4rJk7L7E= +github.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc= +github.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ= +github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ= +github.com/go-openapi/testify/enable/yaml/v2 v2.4.0 h1:7SgOMTvJkM8yWrQlU8Jm18VeDPuAvB/xWrdxFJkoFag= +github.com/go-openapi/testify/enable/yaml/v2 v2.4.0/go.mod h1:14iV8jyyQlinc9StD7w1xVPW3CO3q1Gj04Jy//Kw4VM= +github.com/go-openapi/testify/v2 v2.4.0 h1:8nsPrHVCWkQ4p8h1EsRVymA2XABB4OT40gcvAu+voFM= +github.com/go-openapi/testify/v2 v2.4.0/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -76,8 +75,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= -github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= -github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= +github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= @@ -98,8 +97,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo= -github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= +github.com/jackc/pgx/v5 v5.9.1 h1:uwrxJXBnx76nyISkhr33kQLlUqjv7et7b9FjCen/tdc= +github.com/jackc/pgx/v5 v5.9.1/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -122,8 +121,8 @@ github.com/lestrrat-go/dsig-secp256k1 v1.0.0 h1:JpDe4Aybfl0soBvoVwjqDbp+9S1Y2OM7 github.com/lestrrat-go/dsig-secp256k1 v1.0.0/go.mod h1:CxUgAhssb8FToqbL8NjSPoGQlnO4w3LG1P0qPWQm/NU= github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= -github.com/lestrrat-go/httprc/v3 v3.0.3 h1:WjLHWkDkgWXeIUrKi/7lS/sGq2DjkSAwdTbH5RHXAKs= -github.com/lestrrat-go/httprc/v3 v3.0.3/go.mod h1:mSMtkZW92Z98M5YoNNztbRGxbXHql7tSitCvaxvo9l0= +github.com/lestrrat-go/httprc/v3 v3.0.4 h1:pXyH2ppK8GYYggygxJ3TvxpCZnbEUWc9qSwRTTApaLA= +github.com/lestrrat-go/httprc/v3 v3.0.4/go.mod h1:mSMtkZW92Z98M5YoNNztbRGxbXHql7tSitCvaxvo9l0= github.com/lestrrat-go/jwx/v3 v3.0.13 h1:AdHKiPIYeCSnOJtvdpipPg/0SuFh9rdkN+HF3O0VdSk= github.com/lestrrat-go/jwx/v3 v3.0.13/go.mod h1:2m0PV1A9tM4b/jVLMx8rh6rBl7F6WGb3EG2hufN9OQU= github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss= @@ -157,16 +156,16 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/sv-tools/openapi v0.2.1 h1:ES1tMQMJFGibWndMagvdoo34T1Vllxr1Nlm5wz6b1aA= -github.com/sv-tools/openapi v0.2.1/go.mod h1:k5VuZamTw1HuiS9p2Wl5YIDWzYnHG6/FgPOSFXLAhGg= -github.com/swaggo/echo-swagger v1.5.0 h1:nkHxOaBy0SkbJMtMeXZC64KHSa0mJdZFQhVqwEcMres= -github.com/swaggo/echo-swagger v1.5.0/go.mod h1:TzO363X1ZG/MSbjrG2IX6m65Yd3/zpqh5KM6lPctAhk= +github.com/sv-tools/openapi v0.4.0 h1:UhD9DVnGox1hfTePNclpUzUFgos57FvzT2jmcAuTOJ4= +github.com/sv-tools/openapi v0.4.0/go.mod h1:kD/dG+KP0+Fom1r6nvcj/ORtLus8d8enXT6dyRZDirE= +github.com/swaggo/echo-swagger/v2 v2.0.1 h1:jKR3QiK+ciGjxE0+7qZ/azjtlx/pTVls7pJFJqdJoJI= +github.com/swaggo/echo-swagger/v2 v2.0.1/go.mod h1:BbgiO9XKX6yYU5Rq4ejqVlQI0mVRv6ziFKd0XgdztnQ= github.com/swaggo/files/v2 v2.0.2 h1:Bq4tgS/yxLB/3nwOMcul5oLEUKa877Ykgz3CJMVbQKU= github.com/swaggo/files/v2 v2.0.2/go.mod h1:TVqetIzZsO9OhHX1Am9sRf9LdrFZqoK49N37KON/jr0= github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= -github.com/swaggo/swag/v2 v2.0.0-rc4 h1:SZ8cK68gcV6cslwrJMIOqPkJELRwq4gmjvk77MrvHvY= -github.com/swaggo/swag/v2 v2.0.0-rc4/go.mod h1:Ow7Y8gF16BTCDn8YxZbyKn8FkMLRUHekv1kROJZpbvE= +github.com/swaggo/swag/v2 v2.0.0-rc5 h1:fK7d6ET9rrEsdB8IyuwXREWMcyQN3N7gawGFbbrjgHk= +github.com/swaggo/swag/v2 v2.0.0-rc5/go.mod h1:kCL8Fu4Zl8d5tB2Bgj96b8wRowwrwk175bZHXfuGVFI= github.com/valyala/fastjson v1.6.7 h1:ZE4tRy0CIkh+qDc5McjatheGX2czdn8slQjomexVpBM= github.com/valyala/fastjson v1.6.7/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -174,8 +173,8 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/bridges/otelslog v0.17.0 h1:NFIS6x7wyObQ7cR84x7bt1sr8nYBx89s3x3GwRjw40k= go.opentelemetry.io/contrib/bridges/otelslog v0.17.0/go.mod h1:39SaByOyDMRMe872AE7uelMuQZidIw7LLFAnQi0FWTE= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg= go.opentelemetry.io/contrib/propagators/b3 v1.40.0 h1:xariChe8OOVF3rNlfzGFgQc61npQmXhzZj/i82mxMfg= go.opentelemetry.io/contrib/propagators/b3 v1.40.0/go.mod h1:72WvbdxbOfXaELEQfonFfOL6osvcVjI7uJEE8C2nkrs= go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= @@ -208,33 +207,35 @@ go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9 go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc= go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= -go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= -go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= +go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= +go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= -golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= -golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= -golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -243,8 +244,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -256,23 +257,23 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= -golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= -golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= -golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= +golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0= -google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 h1:41r6JMbpzBMen0R/4TZeeAmGXSJC7DftGINUodzTkPI= +google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:EIQZ5bFCfRQDV4MhRle7+OgjNtZ6P1PiZBgAKuxXu/Y= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7 h1:ndE4FoJqsIceKP2oYSnUZqhTdYufCYYkqwtFzfrhI7w= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= @@ -280,10 +281,8 @@ google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= -sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/auth/main.go b/auth/main.go index e346c9c6..e02ed133 100644 --- a/auth/main.go +++ b/auth/main.go @@ -25,7 +25,7 @@ import ( echojwt "github.com/labstack/echo-jwt/v5" "github.com/labstack/echo/v5" "github.com/labstack/echo/v5/middleware" - echoSwagger "github.com/swaggo/echo-swagger" + echoSwagger "github.com/swaggo/echo-swagger/v2" "github.com/exaring/otelpgx" ) diff --git a/auth/oidc.go b/auth/oidc.go index 619cb2b9..e0ebb4c9 100644 --- a/auth/oidc.go +++ b/auth/oidc.go @@ -544,7 +544,7 @@ type OidcInfo struct { // @Description List keibi's settings (oidc providers, public url...) // @Tags oidc // @Produce json -// @Success 200 ServerInfo +// @Success 200 {object} ServerInfo // @Router /info [get] func (h *Handler) Info(c *echo.Context) error { ret := ServerInfo{ From 04c48b6f8adfe483fb10aeb3843b4310fcc66494 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sat, 28 Mar 2026 15:33:41 +0100 Subject: [PATCH 14/14] Fix duplicate audio & variants order --- front/src/ui/details/staff.tsx | 2 +- front/src/ui/player/controls/tracks-menu.tsx | 1 + transcoder/src/filestream.go | 12 ++++++++---- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/front/src/ui/details/staff.tsx b/front/src/ui/details/staff.tsx index 57b520a0..e7109d33 100644 --- a/front/src/ui/details/staff.tsx +++ b/front/src/ui/details/staff.tsx @@ -1,7 +1,7 @@ import { useTranslation } from "react-i18next"; import { View } from "react-native"; import { Role } from "~/models"; -import { Container, H2, Image, P, Poster, Skeleton, SubP } from "~/primitives"; +import { Container, H2, P, Poster, Skeleton, SubP } from "~/primitives"; import { InfiniteGrid, type QueryIdentifier } from "~/query"; import { EmptyView } from "../empty-view"; diff --git a/front/src/ui/player/controls/tracks-menu.tsx b/front/src/ui/player/controls/tracks-menu.tsx index 9ed05a90..6f41181f 100644 --- a/front/src/ui/player/controls/tracks-menu.tsx +++ b/front/src/ui/player/controls/tracks-menu.tsx @@ -176,6 +176,7 @@ export const QualityMenu = ({ }} /> {lvls + .reverse() .map((x) => ( = transcode_count { - for _, audio_quality := range []AudioQuality{AOriginal, matchAudioQuality(def_video.Quality())} { + for _, quality := range slices.Backward(qualities) { + if quality == Original || quality == NoResize { + audios := []AudioQuality{AOriginal} + if def_audio != nil && (def_audio.MimeCodec == nil || *def_audio.MimeCodec != transcode_audio_codec) { + audios = append(audios, matchAudioQuality(def_video.Quality())) + } + for _, audio_quality := range audios { // original & noresize streams bitrate := float64(def_video.Bitrate) master += "#EXT-X-STREAM-INF:"