@@ -58,8 +58,10 @@ func (s *Server) apiGetStreams(w http.ResponseWriter, r *http.Request) {
5858 }
5959
6060 var result []streamInfo
61+ seen := make (map [string ]bool )
6162 for _ , st := range allStreams {
6263 if s .hasAccess (user , st .MountName ) {
64+ seen [st .MountName ] = true
6365 result = append (result , streamInfo {
6466 Mount : st .MountName ,
6567 ContentType : st .ContentType ,
@@ -75,6 +77,35 @@ func (s *Server) apiGetStreams(w http.ResponseWriter, r *http.Request) {
7577 })
7678 }
7779 }
80+
81+ // Add configured mounts that are not currently active (offline)
82+ for mount := range s .Config .Mounts {
83+ if ! seen [mount ] && s .hasAccess (user , mount ) {
84+ seen [mount ] = true
85+ disabled := s .Config .DisabledMounts [mount ]
86+ visible := s .Config .VisibleMounts [mount ]
87+ result = append (result , streamInfo {
88+ Mount : mount ,
89+ Visible : visible ,
90+ Enabled : ! disabled ,
91+ })
92+ }
93+ }
94+ for _ , u := range s .Config .Users {
95+ for mount := range u .Mounts {
96+ if ! seen [mount ] && s .hasAccess (user , mount ) {
97+ seen [mount ] = true
98+ disabled := s .Config .DisabledMounts [mount ]
99+ visible := s .Config .VisibleMounts [mount ]
100+ result = append (result , streamInfo {
101+ Mount : mount ,
102+ Visible : visible ,
103+ Enabled : ! disabled ,
104+ })
105+ }
106+ }
107+ }
108+
78109 if result == nil {
79110 result = []streamInfo {}
80111 }
@@ -612,7 +643,7 @@ func (s *Server) apiAddToPlaylist(w http.ResponseWriter, r *http.Request) {
612643 }
613644 streamer .SavePlaylist ()
614645 for _ , adj := range s .Config .AutoDJs {
615- if adj .Mount == body . Mount {
646+ if adj .Mount == mount {
616647 adj .Playlist = playlistCopy
617648 adj .LastPlaylist = lastPl
618649 s .Config .SaveConfig ()
@@ -633,8 +664,19 @@ func (s *Server) apiRemoveFromPlaylist(w http.ResponseWriter, r *http.Request) {
633664 }
634665
635666 mount := r .URL .Query ().Get ("mount" )
636- var idx int
637- fmt .Sscanf (r .URL .Query ().Get ("id" ), "%d" , & idx )
667+
668+ var body struct {
669+ Mount string `json:"mount"`
670+ ID int `json:"id"`
671+ }
672+ json .NewDecoder (r .Body ).Decode (& body )
673+ if mount == "" {
674+ mount = body .Mount
675+ }
676+ idx := body .ID
677+ if idx == 0 {
678+ fmt .Sscanf (r .URL .Query ().Get ("id" ), "%d" , & idx )
679+ }
638680
639681 streamer := s .StreamerM .GetStreamer (mount )
640682 if streamer == nil {
@@ -1514,6 +1556,204 @@ func (s *Server) apiUpdateBranding(w http.ResponseWriter, r *http.Request) {
15141556 jsonResponse (w , map [string ]string {"status" : "updated" })
15151557}
15161558
1559+ // ---------------------------------------------------------------------------
1560+ // Logo upload + serve
1561+ // ---------------------------------------------------------------------------
1562+
1563+ func (s * Server ) apiUploadLogo (w http.ResponseWriter , r * http.Request ) {
1564+ if ! s .isCSRFSafe (r ) {
1565+ jsonError (w , "Forbidden" , http .StatusForbidden )
1566+ return
1567+ }
1568+ if _ , ok := s .checkAuth (r ); ! ok {
1569+ jsonError (w , "Unauthorized" , http .StatusUnauthorized )
1570+ return
1571+ }
1572+
1573+ if err := r .ParseMultipartForm (2 << 20 ); err != nil {
1574+ jsonError (w , "File too large (max 2MB)" , http .StatusBadRequest )
1575+ return
1576+ }
1577+ file , header , err := r .FormFile ("logo" )
1578+ if err != nil {
1579+ jsonError (w , "No file uploaded" , http .StatusBadRequest )
1580+ return
1581+ }
1582+ defer file .Close ()
1583+
1584+ ext := strings .ToLower (filepath .Ext (header .Filename ))
1585+ if ext == "" {
1586+ ext = ".png"
1587+ }
1588+ destPath := filepath .Join ("branding" , "logo" + ext )
1589+
1590+ os .MkdirAll ("branding" , 0755 )
1591+ dst , err := os .Create (destPath )
1592+ if err != nil {
1593+ jsonError (w , "Failed to save logo" , http .StatusInternalServerError )
1594+ return
1595+ }
1596+ defer dst .Close ()
1597+
1598+ if _ , err := dst .ReadFrom (file ); err != nil {
1599+ jsonError (w , "Failed to write logo" , http .StatusInternalServerError )
1600+ return
1601+ }
1602+
1603+ s .Config .LogoPath = destPath
1604+ s .Config .SaveConfig ()
1605+ jsonResponse (w , map [string ]string {"status" : "ok" , "path" : destPath })
1606+ }
1607+
1608+ func (s * Server ) handleServeLogo (w http.ResponseWriter , r * http.Request ) {
1609+ if s .Config .LogoPath == "" {
1610+ http .NotFound (w , r )
1611+ return
1612+ }
1613+ http .ServeFile (w , r , s .Config .LogoPath )
1614+ }
1615+
1616+ // ---------------------------------------------------------------------------
1617+ // API Tokens
1618+ // ---------------------------------------------------------------------------
1619+
1620+ func (s * Server ) apiGetTokens (w http.ResponseWriter , r * http.Request ) {
1621+ user , ok := s .checkAuth (r )
1622+ if ! ok {
1623+ jsonError (w , "Unauthorized" , http .StatusUnauthorized )
1624+ return
1625+ }
1626+
1627+ type tokenInfo struct {
1628+ ID string `json:"id"`
1629+ Name string `json:"name"`
1630+ Username string `json:"username"`
1631+ CreatedAt string `json:"created_at"`
1632+ LastUsedAt string `json:"last_used_at"`
1633+ LastUsedIP string `json:"last_used_ip"`
1634+ ExpiresAt string `json:"expires_at"`
1635+ Prefix string `json:"prefix"`
1636+ }
1637+
1638+ var result []tokenInfo
1639+ for _ , tok := range s .Config .APITokens {
1640+ if user .Role != config .RoleSuperAdmin && tok .Username != user .Username {
1641+ continue
1642+ }
1643+ prefix := "ti_XXXX..."
1644+ if len (tok .TokenHash ) >= 8 {
1645+ prefix = "ti_" + tok .TokenHash [:4 ] + "..."
1646+ }
1647+ result = append (result , tokenInfo {
1648+ ID : tok .ID ,
1649+ Name : tok .Name ,
1650+ Username : tok .Username ,
1651+ CreatedAt : tok .CreatedAt ,
1652+ LastUsedAt : tok .LastUsedAt ,
1653+ LastUsedIP : tok .LastUsedIP ,
1654+ ExpiresAt : tok .ExpiresAt ,
1655+ Prefix : prefix ,
1656+ })
1657+ }
1658+ if result == nil {
1659+ result = []tokenInfo {}
1660+ }
1661+ jsonResponse (w , result )
1662+ }
1663+
1664+ func (s * Server ) apiCreateToken (w http.ResponseWriter , r * http.Request ) {
1665+ if ! s .isCSRFSafe (r ) {
1666+ jsonError (w , "Forbidden" , http .StatusForbidden )
1667+ return
1668+ }
1669+ user , ok := s .checkAuth (r )
1670+ if ! ok {
1671+ jsonError (w , "Unauthorized" , http .StatusUnauthorized )
1672+ return
1673+ }
1674+
1675+ var body struct {
1676+ Name string `json:"name"`
1677+ ExpiresAt string `json:"expires_at"`
1678+ }
1679+ if err := json .NewDecoder (r .Body ).Decode (& body ); err != nil {
1680+ jsonError (w , "Invalid request body" , http .StatusBadRequest )
1681+ return
1682+ }
1683+ if body .Name == "" {
1684+ jsonError (w , "Name is required" , http .StatusBadRequest )
1685+ return
1686+ }
1687+
1688+ raw , err := generateToken ()
1689+ if err != nil {
1690+ jsonError (w , "Failed to generate token" , http .StatusInternalServerError )
1691+ return
1692+ }
1693+
1694+ hash := hashToken (raw )
1695+ id := hash [:16 ]
1696+
1697+ tok := & config.APIToken {
1698+ ID : id ,
1699+ Name : body .Name ,
1700+ TokenHash : hash ,
1701+ Username : user .Username ,
1702+ Role : user .Role ,
1703+ CreatedAt : time .Now ().Format (time .RFC3339 ),
1704+ ExpiresAt : body .ExpiresAt ,
1705+ }
1706+
1707+ s .Config .APITokens = append (s .Config .APITokens , tok )
1708+ s .Config .SaveConfig ()
1709+
1710+ jsonResponse (w , map [string ]string {
1711+ "id" : id ,
1712+ "token" : raw ,
1713+ "name" : body .Name ,
1714+ })
1715+ }
1716+
1717+ func (s * Server ) apiDeleteToken (w http.ResponseWriter , r * http.Request ) {
1718+ if ! s .isCSRFSafe (r ) {
1719+ jsonError (w , "Forbidden" , http .StatusForbidden )
1720+ return
1721+ }
1722+ user , ok := s .checkAuth (r )
1723+ if ! ok {
1724+ jsonError (w , "Unauthorized" , http .StatusUnauthorized )
1725+ return
1726+ }
1727+
1728+ id := r .URL .Query ().Get ("id" )
1729+ if id == "" {
1730+ jsonError (w , "ID is required" , http .StatusBadRequest )
1731+ return
1732+ }
1733+
1734+ newTokens := make ([]* config.APIToken , 0 , len (s .Config .APITokens ))
1735+ found := false
1736+ for _ , tok := range s .Config .APITokens {
1737+ if tok .ID == id {
1738+ if user .Role != config .RoleSuperAdmin && tok .Username != user .Username {
1739+ jsonError (w , "Forbidden" , http .StatusForbidden )
1740+ return
1741+ }
1742+ found = true
1743+ continue
1744+ }
1745+ newTokens = append (newTokens , tok )
1746+ }
1747+ if ! found {
1748+ jsonError (w , "Token not found" , http .StatusNotFound )
1749+ return
1750+ }
1751+
1752+ s .Config .APITokens = newTokens
1753+ s .Config .SaveConfig ()
1754+ jsonResponse (w , map [string ]string {"status" : "deleted" })
1755+ }
1756+
15171757// ---------------------------------------------------------------------------
15181758// Settings
15191759// ---------------------------------------------------------------------------
0 commit comments