Fix: move keys and model-load routes under /api/v1/admin prefix
CI / test (push) Failing after 12m38s

Routes now consistently under admin session auth:
- /api/v1/admin/keys (GET, POST, DELETE)
- /api/v1/admin/models/:name/load (POST)

Rebuilt frontend assets.
This commit is contained in:
2026-07-31 17:17:23 -04:00
parent 4d34c6d31a
commit ddc73957cb
18 changed files with 62 additions and 58 deletions
+4 -12
View File
@@ -55,13 +55,6 @@ func New(cfg *config.Config, db *gorm.DB, llamaManager *llama.Manager) *gin.Engi
chat.Use(middleware.APIKeyAuth(authService))
chat.POST("/completions", chatHandler.ChatCompletions)
// Keys management (admin session required)
keys := v1.Group("/keys")
keys.Use(middleware.AdminSessionAuth(adminJWTService))
keys.GET("", keysHandler.ListKeys)
keys.POST("", keysHandler.CreateKey)
keys.DELETE("/:id", keysHandler.RevokeKey)
// Models management
models := v1.Group("/models")
models.Use(middleware.APIKeyAuth(authService))
@@ -70,11 +63,6 @@ func New(cfg *config.Config, db *gorm.DB, llamaManager *llama.Manager) *gin.Engi
models.GET("/active", modelsHandler.GetActiveModel)
models.GET("/:name", modelsHandler.GetActiveModel) // alias for compatibility
// Model load (admin session required)
modelLoad := v1.Group("/models")
modelLoad.Use(middleware.AdminSessionAuth(adminJWTService))
modelLoad.POST("/:name/load", modelsHandler.LoadModel)
// Usage (requires API key auth)
usage := v1.Group("/usage")
usage.Use(middleware.APIKeyAuth(authService))
@@ -125,6 +113,10 @@ func New(cfg *config.Config, db *gorm.DB, llamaManager *llama.Manager) *gin.Engi
adminSession.POST("/users", adminUsersHandler.Create)
adminSession.PUT("/users/:id", adminUsersHandler.Update)
adminSession.DELETE("/users/:id", adminUsersHandler.Delete)
adminSession.GET("/keys", keysHandler.ListKeys)
adminSession.POST("/keys", keysHandler.CreateKey)
adminSession.DELETE("/keys/:id", keysHandler.RevokeKey)
adminSession.POST("/models/:name/load", modelsHandler.LoadModel)
return r
}
+6
View File
@@ -0,0 +1,6 @@
import{d as D,q as L,c as n,a as e,b as k,e as h,x as M,g as f,F as $,i as S,w as U,f as y,v as _,t as u,y as j,r as l,s as w,o,n as z}from"./index-DGy3EgY7.js";import{P as E}from"./plus-DEUh38TR.js";import{c as I}from"./createLucideIcon-C8BmASko.js";import{T}from"./trash-2-LJ7Ux6P7.js";/**
* @license lucide-vue-next v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const q=I("PenIcon",[["path",{d:"M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z",key:"1a8usu"}]]),B={class:"flex items-center justify-between mb-6"},G={key:0,class:"text-text-muted"},H={class:"card"},J={class:"table"},K={class:"flex gap-2"},O=["onClick"],Q=["onClick"],R={key:0},W={key:1,class:"fixed inset-0 bg-black/50 flex items-center justify-center z-50"},X={class:"card w-full max-w-md"},Y={class:"mb-4"},Z={class:"mb-4"},ee={class:"flex gap-3 justify-end"},te={key:2,class:"fixed inset-0 bg-black/50 flex items-center justify-center z-50"},se={class:"card w-full max-w-md"},ae={class:"text-lg font-semibold mb-4"},le={class:"mb-4"},ne={class:"mb-4"},oe={class:"flex items-center gap-2"},ie={class:"flex gap-3 justify-end"},ve=D({__name:"AdminUsers",setup(re){const x=l([]),g=l(!0),d=l(!1),c=l(!1),m=l(""),v=l(""),i=l(null),r=l(""),b=l(!0);async function p(){g.value=!0;try{const s=await w.get("/api/v1/admin/users");x.value=s.data}catch(s){console.error("Failed to fetch users:",s)}finally{g.value=!1}}async function F(){try{await w.post("/api/v1/admin/users",{username:m.value,password:v.value}),d.value=!1,m.value="",v.value="",await p()}catch(s){console.error("Failed to create user:",s),alert("Failed to create user")}}async function A(){if(i.value)try{const s={};r.value&&(s.password=r.value),s.is_active=b.value,await w.put(`/api/v1/admin/users/${i.value.id}`,s),c.value=!1,i.value=null,r.value="",await p()}catch(s){console.error("Failed to update user:",s),alert("Failed to update user")}}async function N(s){if(confirm("Are you sure you want to delete this user?"))try{await w.delete(`/api/v1/admin/users/${s}`),await p()}catch(t){console.error("Failed to delete user:",t),alert("Failed to delete user")}}function P(s){i.value=s,b.value=s.is_active,r.value="",c.value=!0}function C(s){return s?new Date(s).toLocaleString():"Never"}return L(p),(s,t)=>(o(),n("div",null,[e("div",B,[t[8]||(t[8]=e("h1",{class:"text-2xl font-bold"},"Admin Users",-1)),e("button",{onClick:t[0]||(t[0]=a=>d.value=!0),class:"btn btn-primary"},[k(h(E),{class:"w-4 h-4 mr-2"}),t[7]||(t[7]=M(" New User ",-1))])]),g.value?(o(),n("div",G,"Loading...")):f("",!0),e("div",H,[e("table",J,[t[10]||(t[10]=e("thead",null,[e("tr",null,[e("th",null,"Username"),e("th",null,"Status"),e("th",null,"Created"),e("th",null,"Last Login"),e("th",null,"Actions")])],-1)),e("tbody",null,[(o(!0),n($,null,S(x.value,a=>(o(),n("tr",{key:a.id},[e("td",null,u(a.username),1),e("td",null,[e("span",{class:z([a.is_active?"badge-success":"badge-error","badge"])},u(a.is_active?"Active":"Inactive"),3)]),e("td",null,u(C(a.created_at)),1),e("td",null,u(C(a.last_login_at)),1),e("td",null,[e("div",K,[e("button",{onClick:V=>P(a),class:"btn btn-secondary btn-sm"},[k(h(q),{class:"w-4 h-4"})],8,O),e("button",{onClick:V=>N(a.id),class:"btn btn-danger btn-sm"},[k(h(T),{class:"w-4 h-4"})],8,Q)])])]))),128)),x.value.length===0?(o(),n("tr",R,[...t[9]||(t[9]=[e("td",{colspan:"5",class:"text-center text-text-muted py-8"}," No admin users yet. Create one to get started. ",-1)])])):f("",!0)])])]),d.value?(o(),n("div",W,[e("div",X,[t[14]||(t[14]=e("h2",{class:"text-lg font-semibold mb-4"},"Create Admin User",-1)),e("form",{onSubmit:U(F,["prevent"])},[e("div",Y,[t[11]||(t[11]=e("label",{class:"block text-sm font-medium mb-2"},"Username",-1)),y(e("input",{"onUpdate:modelValue":t[1]||(t[1]=a=>m.value=a),type:"text",class:"input",placeholder:"admin",required:"",minlength:"3",maxlength:"64"},null,512),[[_,m.value]])]),e("div",Z,[t[12]||(t[12]=e("label",{class:"block text-sm font-medium mb-2"},"Password",-1)),y(e("input",{"onUpdate:modelValue":t[2]||(t[2]=a=>v.value=a),type:"password",class:"input",placeholder:"Min 8 characters",required:"",minlength:"8"},null,512),[[_,v.value]])]),e("div",ee,[e("button",{type:"button",onClick:t[3]||(t[3]=a=>d.value=!1),class:"btn btn-secondary"}," Cancel "),t[13]||(t[13]=e("button",{type:"submit",class:"btn btn-primary"},"Create",-1))])],32)])])):f("",!0),c.value&&i.value?(o(),n("div",te,[e("div",se,[e("h2",ae,"Edit User: "+u(i.value.username),1),e("form",{onSubmit:U(A,["prevent"])},[e("div",le,[t[15]||(t[15]=e("label",{class:"block text-sm font-medium mb-2"},"New Password (leave blank to keep current)",-1)),y(e("input",{"onUpdate:modelValue":t[4]||(t[4]=a=>r.value=a),type:"password",class:"input",placeholder:"Min 8 characters",minlength:"8"},null,512),[[_,r.value]])]),e("div",ne,[e("label",oe,[y(e("input",{"onUpdate:modelValue":t[5]||(t[5]=a=>b.value=a),type:"checkbox",class:"rounded"},null,512),[[j,b.value]]),t[16]||(t[16]=e("span",{class:"text-sm font-medium"},"Active",-1))])]),e("div",ie,[e("button",{type:"button",onClick:t[6]||(t[6]=a=>c.value=!1),class:"btn btn-secondary"}," Cancel "),t[17]||(t[17]=e("button",{type:"submit",class:"btn btn-primary"},"Save",-1))])],32)])])):f("",!0)]))}});export{ve as default};
-11
View File
@@ -1,11 +0,0 @@
import{d as I,q as M,c as a,a as e,b as p,e as x,x as w,g as c,t as o,F as g,i as C,w as P,f as V,v as j,r,s as k,o as l}from"./index-r2SG-Kf3.js";import{P as T}from"./plus-Bfa9PGWP.js";import{c as _}from"./createLucideIcon-CUrbWv4G.js";/**
* @license lucide-vue-next v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const $=_("CopyIcon",[["rect",{width:"14",height:"14",x:"8",y:"8",rx:"2",ry:"2",key:"17jyea"}],["path",{d:"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2",key:"zix9uf"}]]);/**
* @license lucide-vue-next v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const D=_("Trash2Icon",[["path",{d:"M3 6h18",key:"d0wm0j"}],["path",{d:"M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6",key:"4alrt4"}],["path",{d:"M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2",key:"v07s0e"}],["line",{x1:"10",x2:"10",y1:"11",y2:"17",key:"1uufr5"}],["line",{x1:"14",x2:"14",y1:"11",y2:"17",key:"xtxkd"}]]),F={class:"flex items-center justify-between mb-6"},L={key:0,class:"text-text-muted"},S={key:1,class:"card mb-6 bg-success/5 border-success/20"},B={class:"flex items-center justify-between"},q={class:"mt-4 p-3 bg-background rounded-lg font-mono text-sm break-all"},z={class:"card"},U={class:"table"},E={class:"font-mono text-text-muted"},H=["onClick"],O={key:1,class:"badge badge-info"},Y={key:0},G={key:2,class:"fixed inset-0 bg-black/50 flex items-center justify-center z-50"},J={class:"card w-full max-w-md"},Q={class:"mb-4"},R={class:"flex gap-3 justify-end"},te=I({__name:"ApiKeys",setup(W){const y=r([]),m=r(!0),u=r(!1),d=r(""),i=r(null),v=r(!1);async function b(){m.value=!0;try{const s=await k.get("/api/v1/admin/keys");y.value=s.data}catch(s){console.error("Failed to fetch keys:",s)}finally{m.value=!1}}async function K(){try{const s=await k.post("/api/v1/admin/keys",{name:d.value});i.value=s.data,u.value=!1,d.value="",await b()}catch(s){console.error("Failed to create key:",s)}}async function N(s){if(confirm("Are you sure you want to revoke this key?"))try{await k.delete(`/api/v1/admin/keys/${s}`),await b()}catch(t){console.error("Failed to revoke key:",t)}}async function A(s){await navigator.clipboard.writeText(s),v.value=!0,setTimeout(()=>v.value=!1,2e3)}function h(s){return s?new Date(s).toLocaleString():"Never"}return M(b),(s,t)=>(l(),a("div",null,[e("div",F,[t[6]||(t[6]=e("h1",{class:"text-2xl font-bold"},"API Keys",-1)),e("button",{onClick:t[0]||(t[0]=n=>u.value=!0),class:"btn btn-primary"},[p(x(T),{class:"w-4 h-4 mr-2"}),t[5]||(t[5]=w(" New Key ",-1))])]),m.value?(l(),a("div",L,"Loading...")):c("",!0),i.value?(l(),a("div",S,[e("div",B,[t[7]||(t[7]=e("div",null,[e("h3",{class:"font-semibold text-success"},"API Key Created"),e("p",{class:"text-sm text-text-muted mt-1"}," Copy this key now. You won't be able to see it again. ")],-1)),e("button",{onClick:t[1]||(t[1]=n=>A(i.value.key)),class:"btn btn-secondary"},[p(x($),{class:"w-4 h-4 mr-2"}),w(" "+o(v.value?"Copied!":"Copy"),1)])]),e("div",q,o(i.value.key),1),e("button",{onClick:t[2]||(t[2]=n=>i.value=null),class:"mt-4 text-sm text-text-muted hover:text-text"}," Close ")])):c("",!0),e("div",z,[e("table",U,[t[9]||(t[9]=e("thead",null,[e("tr",null,[e("th",null,"Name"),e("th",null,"Prefix"),e("th",null,"Scopes"),e("th",null,"Owner"),e("th",null,"Created"),e("th",null,"Last Used"),e("th",null,"Actions")])],-1)),e("tbody",null,[(l(!0),a(g,null,C(y.value,n=>(l(),a("tr",{key:n.id},[e("td",null,o(n.name),1),e("td",E,o(n.key_prefix)+"...",1),e("td",null,[(l(!0),a(g,null,C(n.scopes,f=>(l(),a("span",{key:f,class:"badge mr-1"},o(f),1))),128))]),e("td",null,o(n.owner_label||"-"),1),e("td",null,o(h(n.created_at)),1),e("td",null,o(h(n.last_used_at)),1),e("td",null,[n.is_admin?(l(),a("span",O,"Admin")):(l(),a("button",{key:0,onClick:f=>N(n.id),class:"btn btn-danger btn-sm"},[p(x(D),{class:"w-4 h-4"})],8,H))])]))),128)),y.value.length===0?(l(),a("tr",Y,[...t[8]||(t[8]=[e("td",{colspan:"7",class:"text-center text-text-muted py-8"}," No API keys yet. Create one to get started. ",-1)])])):c("",!0)])])]),u.value?(l(),a("div",G,[e("div",J,[t[12]||(t[12]=e("h2",{class:"text-lg font-semibold mb-4"},"Create API Key",-1)),e("form",{onSubmit:P(K,["prevent"])},[e("div",Q,[t[10]||(t[10]=e("label",{class:"block text-sm font-medium mb-2"},"Key Name",-1)),V(e("input",{"onUpdate:modelValue":t[3]||(t[3]=n=>d.value=n),type:"text",class:"input",placeholder:"My API Key",required:""},null,512),[[j,d.value]])]),e("div",R,[e("button",{type:"button",onClick:t[4]||(t[4]=n=>u.value=!1),class:"btn btn-secondary"}," Cancel "),t[11]||(t[11]=e("button",{type:"submit",class:"btn btn-primary"},"Create",-1))])],32)])])):c("",!0)]))}});export{te as default};
+6
View File
@@ -0,0 +1,6 @@
import{d as A,q as P,c as a,a as e,b as p,e as x,x as g,g as c,t as o,F as h,i as C,w as I,f as T,v as V,r,s as k,o as l}from"./index-DGy3EgY7.js";import{P as $}from"./plus-DEUh38TR.js";import{c as j}from"./createLucideIcon-C8BmASko.js";import{T as D}from"./trash-2-LJ7Ux6P7.js";/**
* @license lucide-vue-next v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const F=j("CopyIcon",[["rect",{width:"14",height:"14",x:"8",y:"8",rx:"2",ry:"2",key:"17jyea"}],["path",{d:"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2",key:"zix9uf"}]]),L={class:"flex items-center justify-between mb-6"},M={key:0,class:"text-text-muted"},S={key:1,class:"card mb-6 bg-success/5 border-success/20"},B={class:"flex items-center justify-between"},q={class:"mt-4 p-3 bg-background rounded-lg font-mono text-sm break-all"},z={class:"card"},U={class:"table"},E={class:"font-mono text-text-muted"},O=["onClick"],Y={key:1,class:"badge badge-info"},G={key:0},H={key:2,class:"fixed inset-0 bg-black/50 flex items-center justify-center z-50"},J={class:"card w-full max-w-md"},Q={class:"mb-4"},R={class:"flex gap-3 justify-end"},se=A({__name:"ApiKeys",setup(W){const m=r([]),y=r(!0),u=r(!1),d=r(""),i=r(null),v=r(!1);async function b(){y.value=!0;try{const s=await k.get("/api/v1/admin/keys");m.value=s.data}catch(s){console.error("Failed to fetch keys:",s)}finally{y.value=!1}}async function _(){try{const s=await k.post("/api/v1/admin/keys",{name:d.value});i.value=s.data,u.value=!1,d.value="",await b()}catch(s){console.error("Failed to create key:",s)}}async function K(s){if(confirm("Are you sure you want to revoke this key?"))try{await k.delete(`/api/v1/admin/keys/${s}`),await b()}catch(t){console.error("Failed to revoke key:",t)}}async function N(s){await navigator.clipboard.writeText(s),v.value=!0,setTimeout(()=>v.value=!1,2e3)}function w(s){return s?new Date(s).toLocaleString():"Never"}return P(b),(s,t)=>(l(),a("div",null,[e("div",L,[t[6]||(t[6]=e("h1",{class:"text-2xl font-bold"},"API Keys",-1)),e("button",{onClick:t[0]||(t[0]=n=>u.value=!0),class:"btn btn-primary"},[p(x($),{class:"w-4 h-4 mr-2"}),t[5]||(t[5]=g(" New Key ",-1))])]),y.value?(l(),a("div",M,"Loading...")):c("",!0),i.value?(l(),a("div",S,[e("div",B,[t[7]||(t[7]=e("div",null,[e("h3",{class:"font-semibold text-success"},"API Key Created"),e("p",{class:"text-sm text-text-muted mt-1"}," Copy this key now. You won't be able to see it again. ")],-1)),e("button",{onClick:t[1]||(t[1]=n=>N(i.value.key)),class:"btn btn-secondary"},[p(x(F),{class:"w-4 h-4 mr-2"}),g(" "+o(v.value?"Copied!":"Copy"),1)])]),e("div",q,o(i.value.key),1),e("button",{onClick:t[2]||(t[2]=n=>i.value=null),class:"mt-4 text-sm text-text-muted hover:text-text"}," Close ")])):c("",!0),e("div",z,[e("table",U,[t[9]||(t[9]=e("thead",null,[e("tr",null,[e("th",null,"Name"),e("th",null,"Prefix"),e("th",null,"Scopes"),e("th",null,"Owner"),e("th",null,"Created"),e("th",null,"Last Used"),e("th",null,"Actions")])],-1)),e("tbody",null,[(l(!0),a(h,null,C(m.value,n=>(l(),a("tr",{key:n.id},[e("td",null,o(n.name),1),e("td",E,o(n.key_prefix)+"...",1),e("td",null,[(l(!0),a(h,null,C(n.scopes,f=>(l(),a("span",{key:f,class:"badge mr-1"},o(f),1))),128))]),e("td",null,o(n.owner_label||"-"),1),e("td",null,o(w(n.created_at)),1),e("td",null,o(w(n.last_used_at)),1),e("td",null,[n.is_admin?(l(),a("span",Y,"Admin")):(l(),a("button",{key:0,onClick:f=>K(n.id),class:"btn btn-danger btn-sm"},[p(x(D),{class:"w-4 h-4"})],8,O))])]))),128)),m.value.length===0?(l(),a("tr",G,[...t[8]||(t[8]=[e("td",{colspan:"7",class:"text-center text-text-muted py-8"}," No API keys yet. Create one to get started. ",-1)])])):c("",!0)])])]),u.value?(l(),a("div",H,[e("div",J,[t[12]||(t[12]=e("h2",{class:"text-lg font-semibold mb-4"},"Create API Key",-1)),e("form",{onSubmit:I(_,["prevent"])},[e("div",Q,[t[10]||(t[10]=e("label",{class:"block text-sm font-medium mb-2"},"Key Name",-1)),T(e("input",{"onUpdate:modelValue":t[3]||(t[3]=n=>d.value=n),type:"text",class:"input",placeholder:"My API Key",required:""},null,512),[[V,d.value]])]),e("div",R,[e("button",{type:"button",onClick:t[4]||(t[4]=n=>u.value=!1),class:"btn btn-secondary"}," Cancel "),t[11]||(t[11]=e("button",{type:"submit",class:"btn btn-primary"},"Create",-1))])],32)])])):c("",!0)]))}});export{se as default};
@@ -1,4 +1,4 @@
import{d as A,q as C,c as e,a as t,t as d,F as h,b as _,e as m,n as L,g as b,i as M,r as u,s as v,o as a}from"./index-r2SG-Kf3.js";import{c as y}from"./createLucideIcon-CUrbWv4G.js";import{C as N}from"./cpu-ed4VmMFm.js";import{K as S}from"./key-D7ygKuN6.js";/**
import{d as A,q as C,c as e,a as t,t as d,F as h,b as _,e as m,n as L,g as b,i as M,r as u,s as v,o as a}from"./index-DGy3EgY7.js";import{c as y}from"./createLucideIcon-C8BmASko.js";import{C as N}from"./cpu-D29LpvoC.js";import{K as S}from"./key-BRxFGImj.js";/**
* @license lucide-vue-next v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
-16
View File
@@ -1,16 +0,0 @@
import{d as u,u as m,c as d,a as t,F as x,i as y,b as s,e as o,R as g,h as b,o as r,j as f,n as k,k as _,l as v,m as L,p as C,t as w}from"./index-r2SG-Kf3.js";import{c as n}from"./createLucideIcon-CUrbWv4G.js";import{K as I}from"./key-D7ygKuN6.js";import{C as M}from"./cpu-ed4VmMFm.js";/**
* @license lucide-vue-next v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const V=n("ChartColumnIcon",[["path",{d:"M3 3v16a2 2 0 0 0 2 2h16",key:"c24i48"}],["path",{d:"M18 17V9",key:"2bz60n"}],["path",{d:"M13 17V5",key:"1frdt8"}],["path",{d:"M8 17v-3",key:"17ska0"}]]);/**
* @license lucide-vue-next v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const D=n("LayoutDashboardIcon",[["rect",{width:"7",height:"9",x:"3",y:"3",rx:"1",key:"10lvy0"}],["rect",{width:"7",height:"5",x:"14",y:"3",rx:"1",key:"16une8"}],["rect",{width:"7",height:"9",x:"14",y:"12",rx:"1",key:"1hutg5"}],["rect",{width:"7",height:"5",x:"3",y:"16",rx:"1",key:"ldoo1y"}]]);/**
* @license lucide-vue-next v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const R=n("LogOutIcon",[["path",{d:"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4",key:"1uf3rs"}],["polyline",{points:"16 17 21 12 16 7",key:"1gabdz"}],["line",{x1:"21",x2:"9",y1:"12",y2:"12",key:"1uyos4"}]]),B={class:"min-h-screen bg-background"},z={class:"fixed left-0 top-0 h-full w-64 bg-surface border-r border-border flex flex-col"},A={class:"flex-1 p-4 space-y-1"},K={class:"p-4 border-t border-border"},S={class:"ml-64 min-h-screen p-8"},H=u({__name:"Layout",setup(F){const l=v(),i=b(),c=m(),h=[{name:"Dashboard",path:"/admin/",icon:D},{name:"API Keys",path:"/admin/keys",icon:I},{name:"Models",path:"/admin/models",icon:M},{name:"Usage",path:"/admin/usage",icon:V}];function p(){c.logout(),i.push("/admin/login")}return(N,a)=>(r(),d("div",B,[t("aside",z,[a[1]||(a[1]=t("div",{class:"p-6 border-b border-border"},[t("h1",{class:"text-xl font-bold text-primary"},"LlamaLink"),t("p",{class:"text-xs text-text-muted mt-1"},"Admin Panel")],-1)),t("nav",A,[(r(),d(x,null,y(h,e=>s(o(_),{key:e.path,to:e.path,class:k(["flex items-center gap-3 px-4 py-3 rounded-lg transition-colors",o(l).path===e.path||e.path!=="/admin/"&&o(l).path.startsWith(e.path)?"bg-primary/10 text-primary":"text-text-muted hover:text-text hover:bg-border"])},{default:f(()=>[(r(),L(C(e.icon),{class:"w-5 h-5"})),t("span",null,w(e.name),1)]),_:2},1032,["to","class"])),64))]),t("div",K,[t("button",{onClick:p,class:"flex items-center gap-3 w-full px-4 py-3 rounded-lg text-text-muted hover:text-error hover:bg-error/10 transition-colors"},[s(o(R),{class:"w-5 h-5"}),a[0]||(a[0]=t("span",null,"Logout",-1))])])]),t("main",S,[s(o(g))])]))}});export{H as default};
+21
View File
@@ -0,0 +1,21 @@
import{d as u,u as m,c,a as e,F as y,i as x,b as r,e as a,R as k,h as g,o as n,j as b,n as f,k as v,l as _,m as L,p as C,t as w}from"./index-DGy3EgY7.js";import{c as s}from"./createLucideIcon-C8BmASko.js";import{K as M}from"./key-BRxFGImj.js";import{C as I}from"./cpu-D29LpvoC.js";/**
* @license lucide-vue-next v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const V=s("ChartColumnIcon",[["path",{d:"M3 3v16a2 2 0 0 0 2 2h16",key:"c24i48"}],["path",{d:"M18 17V9",key:"2bz60n"}],["path",{d:"M13 17V5",key:"1frdt8"}],["path",{d:"M8 17v-3",key:"17ska0"}]]);/**
* @license lucide-vue-next v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const D=s("LayoutDashboardIcon",[["rect",{width:"7",height:"9",x:"3",y:"3",rx:"1",key:"10lvy0"}],["rect",{width:"7",height:"5",x:"14",y:"3",rx:"1",key:"16une8"}],["rect",{width:"7",height:"9",x:"14",y:"12",rx:"1",key:"1hutg5"}],["rect",{width:"7",height:"5",x:"3",y:"16",rx:"1",key:"ldoo1y"}]]);/**
* @license lucide-vue-next v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const R=s("LogOutIcon",[["path",{d:"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4",key:"1uf3rs"}],["polyline",{points:"16 17 21 12 16 7",key:"1gabdz"}],["line",{x1:"21",x2:"9",y1:"12",y2:"12",key:"1uyos4"}]]);/**
* @license lucide-vue-next v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const A=s("UsersIcon",[["path",{d:"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2",key:"1yyitq"}],["circle",{cx:"9",cy:"7",r:"4",key:"nufk8"}],["path",{d:"M22 21v-2a4 4 0 0 0-3-3.87",key:"kshegd"}],["path",{d:"M16 3.13a4 4 0 0 1 0 7.75",key:"1da9ce"}]]),B={class:"min-h-screen bg-background"},U={class:"fixed left-0 top-0 h-full w-64 bg-surface border-r border-border flex flex-col"},z={class:"flex-1 p-4 space-y-1"},K={class:"p-4 border-t border-border"},S={class:"ml-64 min-h-screen p-8"},q=u({__name:"Layout",setup(F){const d=_(),i=g(),l=m(),h=[{name:"Dashboard",path:"/admin/",icon:D},{name:"API Keys",path:"/admin/keys",icon:M},{name:"Models",path:"/admin/models",icon:I},{name:"Usage",path:"/admin/usage",icon:V},{name:"Admin Users",path:"/admin/users",icon:A}];function p(){l.logout(),i.push("/admin/login")}return(H,o)=>(n(),c("div",B,[e("aside",U,[o[1]||(o[1]=e("div",{class:"p-6 border-b border-border"},[e("h1",{class:"text-xl font-bold text-primary"},"LlamaLink"),e("p",{class:"text-xs text-text-muted mt-1"},"Admin Panel")],-1)),e("nav",z,[(n(),c(y,null,x(h,t=>r(a(v),{key:t.path,to:t.path,class:f(["flex items-center gap-3 px-4 py-3 rounded-lg transition-colors",a(d).path===t.path||t.path!=="/admin/"&&a(d).path.startsWith(t.path)?"bg-primary/10 text-primary":"text-text-muted hover:text-text hover:bg-border"])},{default:b(()=>[(n(),L(C(t.icon),{class:"w-5 h-5"})),e("span",null,w(t.name),1)]),_:2},1032,["to","class"])),64))]),e("div",K,[e("button",{onClick:p,class:"flex items-center gap-3 w-full px-4 py-3 rounded-lg text-text-muted hover:text-error hover:bg-error/10 transition-colors"},[r(a(R),{class:"w-5 h-5"}),o[0]||(o[0]=e("span",null,"Logout",-1))])])]),e("main",S,[r(a(k))])]))}});export{q as default};
-1
View File
@@ -1 +0,0 @@
import{d as p,u as f,c as r,a as e,b as v,e as i,w as _,f as b,v as g,t as x,g as h,r as l,o as a,h as y}from"./index-r2SG-Kf3.js";import{K as k}from"./key-D7ygKuN6.js";import"./createLucideIcon-CUrbWv4G.js";const w={class:"min-h-screen bg-background flex items-center justify-center"},L={class:"w-full max-w-md"},A={class:"card"},S={class:"flex items-center gap-3 mb-6"},V={class:"w-12 h-12 bg-primary/10 rounded-xl flex items-center justify-center"},B={key:0,class:"p-3 bg-error/10 border border-error/20 rounded-lg text-error text-sm"},N=["disabled"],T={key:0},j={key:1},M=p({__name:"Login",setup(C){const d=y(),s=f(),o=l(""),n=l("");async function u(){if(!o.value.trim()){n.value="Admin token is required";return}await s.login(o.value)?d.push("/admin/"):n.value=s.error||"Login failed"}return(c,t)=>(a(),r("div",w,[e("div",L,[e("div",A,[e("div",S,[e("div",V,[v(i(k),{class:"w-6 h-6 text-primary"})]),t[1]||(t[1]=e("div",null,[e("h1",{class:"text-2xl font-bold"},"LlamaLink"),e("p",{class:"text-text-muted text-sm"},"Admin Login")],-1))]),e("form",{onSubmit:_(u,["prevent"]),class:"space-y-4"},[e("div",null,[t[2]||(t[2]=e("label",{class:"block text-sm font-medium mb-2"},"Admin Token",-1)),b(e("input",{"onUpdate:modelValue":t[0]||(t[0]=m=>o.value=m),type:"password",class:"input",placeholder:"Enter your admin token",autocomplete:"current-password"},null,512),[[g,o.value]])]),n.value?(a(),r("div",B,x(n.value),1)):h("",!0),e("button",{type:"submit",class:"btn btn-primary w-full",disabled:i(s).loading},[i(s).loading?(a(),r("span",T,"Logging in...")):(a(),r("span",j,"Login"))],8,N)],32)])])]))}});export{M as default};
+1
View File
@@ -0,0 +1 @@
import{d as b,u as g,c as n,a as e,b as x,e as u,w as y,f as m,v as c,t as _,g as w,r as d,o as l,h}from"./index-DGy3EgY7.js";import{K as k}from"./key-BRxFGImj.js";import"./createLucideIcon-C8BmASko.js";const L={class:"min-h-screen bg-background flex items-center justify-center"},V={class:"w-full max-w-md"},S={class:"card"},U={class:"flex items-center gap-3 mb-6"},B={class:"w-12 h-12 bg-primary/10 rounded-xl flex items-center justify-center"},E={key:0,class:"p-3 bg-error/10 border border-error/20 rounded-lg text-error text-sm"},N=["disabled"],j={key:0},q={key:1},M=b({__name:"Login",setup(A){const p=h(),o=g(),r=d(""),a=d(""),t=d("");async function v(){if(!r.value.trim()){t.value="Username is required";return}if(!a.value){t.value="Password is required";return}await o.login(r.value,a.value)?p.push("/admin/"):t.value=o.error||"Login failed"}return(f,s)=>(l(),n("div",L,[e("div",V,[e("div",S,[e("div",U,[e("div",B,[x(u(k),{class:"w-6 h-6 text-primary"})]),s[2]||(s[2]=e("div",null,[e("h1",{class:"text-2xl font-bold"},"LlamaLink"),e("p",{class:"text-text-muted text-sm"},"Admin Login")],-1))]),e("form",{onSubmit:y(v,["prevent"]),class:"space-y-4"},[e("div",null,[s[3]||(s[3]=e("label",{class:"block text-sm font-medium mb-2"},"Username",-1)),m(e("input",{"onUpdate:modelValue":s[0]||(s[0]=i=>r.value=i),type:"text",class:"input",placeholder:"Enter your username",autocomplete:"username"},null,512),[[c,r.value]])]),e("div",null,[s[4]||(s[4]=e("label",{class:"block text-sm font-medium mb-2"},"Password",-1)),m(e("input",{"onUpdate:modelValue":s[1]||(s[1]=i=>a.value=i),type:"password",class:"input",placeholder:"Enter your password",autocomplete:"current-password"},null,512),[[c,a.value]])]),t.value?(l(),n("div",E,_(t.value),1)):w("",!0),e("button",{type:"submit",class:"btn btn-primary w-full",disabled:u(o).loading},[u(o).loading?(l(),n("span",j,"Logging in...")):(l(),n("span",q,"Login"))],8,N)],32)])])]))}});export{M as default};
@@ -1,4 +1,4 @@
import{d as w,q as h,c as a,a as t,b as y,e as x,x as g,g as v,F as M,i as C,w as U,f as d,v as i,y as V,r as m,s as f,o as n,t as u}from"./index-r2SG-Kf3.js";import{P as L}from"./plus-Bfa9PGWP.js";import{c as N}from"./createLucideIcon-CUrbWv4G.js";/**
import{d as w,q as h,c as a,a as t,b as y,e as x,x as g,g as v,F as M,i as C,w as U,f as d,v as i,y as V,r as m,s as f,o as n,t as u}from"./index-DGy3EgY7.js";import{P as L}from"./plus-DEUh38TR.js";import{c as N}from"./createLucideIcon-C8BmASko.js";/**
* @license lucide-vue-next v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
var On=Object.defineProperty;var Dn=(i,t,e)=>t in i?On(i,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):i[t]=e;var D=(i,t,e)=>Dn(i,typeof t!="symbol"?t+"":t,e);import{d as ci,A as zs,z as Qe,B as Je,C as Cn,q as Es,D as Tn,r as ke,E as ti,G as Bs,H as An,c as lt,a as M,f as Ln,I as Fn,F as Si,t as q,g as ce,J as Rn,n as Mi,b as In,e as zn,i as En,s as Bn,o as ct}from"./index-r2SG-Kf3.js";/*!
var On=Object.defineProperty;var Dn=(i,t,e)=>t in i?On(i,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):i[t]=e;var D=(i,t,e)=>Dn(i,typeof t!="symbol"?t+"":t,e);import{d as ci,z as zs,A as Qe,B as Je,C as Cn,q as Es,D as Tn,r as ke,E as ti,G as Bs,H as An,c as lt,a as M,f as Ln,I as Fn,F as Si,t as q,g as ce,J as Rn,n as Mi,b as In,e as zn,i as En,s as Bn,o as ct}from"./index-DGy3EgY7.js";/*!
* @kurkle/color v0.3.4
* https://github.com/kurkle/color#readme
* (c) 2024 Jukka Kurkela
@@ -1,4 +1,4 @@
import{c as e}from"./createLucideIcon-CUrbWv4G.js";/**
import{c as e}from"./createLucideIcon-C8BmASko.js";/**
* @license lucide-vue-next v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{z as a}from"./index-r2SG-Kf3.js";/**
import{A as a}from"./index-DGy3EgY7.js";/**
* @license lucide-vue-next v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
import{c}from"./createLucideIcon-CUrbWv4G.js";/**
import{c}from"./createLucideIcon-C8BmASko.js";/**
* @license lucide-vue-next v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c as e}from"./createLucideIcon-CUrbWv4G.js";/**
import{c as e}from"./createLucideIcon-C8BmASko.js";/**
* @license lucide-vue-next v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
+6
View File
@@ -0,0 +1,6 @@
import{c as e}from"./createLucideIcon-C8BmASko.js";/**
* @license lucide-vue-next v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const a=e("Trash2Icon",[["path",{d:"M3 6h18",key:"d0wm0j"}],["path",{d:"M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6",key:"4alrt4"}],["path",{d:"M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2",key:"v07s0e"}],["line",{x1:"10",x2:"10",y1:"11",y2:"17",key:"1uufr5"}],["line",{x1:"14",x2:"14",y1:"11",y2:"17",key:"xtxkd"}]]);export{a as T};
+1 -1
View File
@@ -8,7 +8,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<script type="module" crossorigin src="/assets/index-r2SG-Kf3.js"></script>
<script type="module" crossorigin src="/assets/index-DGy3EgY7.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DkKprt_C.css">
</head>
<body class="bg-background text-text">