fix: preserve login session across refreshes
This commit is contained in:
+17
-4
@@ -122,17 +122,30 @@ func main() {
|
|||||||
} else if !filepath.IsAbs(userFile) {
|
} else if !filepath.IsAbs(userFile) {
|
||||||
userFile = filepath.Join(root, userFile)
|
userFile = filepath.Join(root, userFile)
|
||||||
}
|
}
|
||||||
|
if err := os.MkdirAll(filepath.Dir(userFile), 0700); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
users := &auth.UserStore{Path: userFile}
|
users := &auth.UserStore{Path: userFile}
|
||||||
if err := users.Load(mgr, cfg.UserHome); err != nil {
|
if err := users.Load(mgr, cfg.UserHome); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
jwtSecret := cfg.JWTSecret
|
jwtSecret := cfg.JWTSecret
|
||||||
if jwtSecret == "" {
|
if jwtSecret == "" {
|
||||||
buf := make([]byte, 32)
|
// Keep sessions valid across process restarts. A random in-memory secret
|
||||||
if _, err := rand.Read(buf); err != nil {
|
// would invalidate every login whenever the service is restarted.
|
||||||
log.Fatal(err)
|
secretPath := userFile + ".jwt-secret"
|
||||||
|
if saved, err := os.ReadFile(secretPath); err == nil && len(strings.TrimSpace(string(saved))) >= 32 {
|
||||||
|
jwtSecret = strings.TrimSpace(string(saved))
|
||||||
|
} else {
|
||||||
|
buf := make([]byte, 32)
|
||||||
|
if _, err := rand.Read(buf); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
jwtSecret = hex.EncodeToString(buf)
|
||||||
|
if err := os.WriteFile(secretPath, []byte(jwtSecret+"\n"), 0600); err != nil {
|
||||||
|
log.Printf("warning: could not persist JWT secret at %s: %v", secretPath, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
jwtSecret = hex.EncodeToString(buf)
|
|
||||||
}
|
}
|
||||||
for _, a := range mgr.Accounts {
|
for _, a := range mgr.Accounts {
|
||||||
if a.Home != "" {
|
if a.Home != "" {
|
||||||
|
|||||||
@@ -146,11 +146,20 @@ func clientIPString(r *http.Request) string {
|
|||||||
|
|
||||||
func writeJSON(w http.ResponseWriter, v any) {
|
func writeJSON(w http.ResponseWriter, v any) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Header().Set("Cache-Control", "no-store")
|
||||||
|
w.Header().Set("Vary", "Cookie")
|
||||||
json.NewEncoder(w).Encode(v)
|
json.NewEncoder(w).Encode(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func noStore(w http.ResponseWriter) {
|
||||||
|
w.Header().Set("Cache-Control", "no-store")
|
||||||
|
w.Header().Set("Vary", "Cookie")
|
||||||
|
}
|
||||||
|
|
||||||
func writeJSONError(w http.ResponseWriter, status int, message string) {
|
func writeJSONError(w http.ResponseWriter, status int, message string) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Header().Set("Cache-Control", "no-store")
|
||||||
|
w.Header().Set("Vary", "Cookie")
|
||||||
w.WriteHeader(status)
|
w.WriteHeader(status)
|
||||||
_ = json.NewEncoder(w).Encode(map[string]string{"error": message})
|
_ = json.NewEncoder(w).Encode(map[string]string{"error": message})
|
||||||
}
|
}
|
||||||
@@ -511,6 +520,7 @@ func (s *Server) exist(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) session(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) session(w http.ResponseWriter, r *http.Request) {
|
||||||
|
noStore(w)
|
||||||
if r.Method == http.MethodDelete {
|
if r.Method == http.MethodDelete {
|
||||||
http.SetCookie(w, &http.Cookie{Name: "JWT", Value: "", Path: "/", MaxAge: -1, Expires: time.Unix(1, 0), HttpOnly: true, SameSite: http.SameSiteLaxMode})
|
http.SetCookie(w, &http.Cookie{Name: "JWT", Value: "", Path: "/", MaxAge: -1, Expires: time.Unix(1, 0), HttpOnly: true, SameSite: http.SameSiteLaxMode})
|
||||||
http.SetCookie(w, &http.Cookie{Name: "user", Value: "", Path: "/", MaxAge: -1, Expires: time.Unix(1, 0), SameSite: http.SameSiteLaxMode})
|
http.SetCookie(w, &http.Cookie{Name: "user", Value: "", Path: "/", MaxAge: -1, Expires: time.Unix(1, 0), SameSite: http.SameSiteLaxMode})
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ button.ghost{background:var(--surface2);color:var(--text)}button.danger{backgrou
|
|||||||
<div class="modal" id="modal"><div class="dialog" id="dialog"></div></div>
|
<div class="modal" id="modal"><div class="dialog" id="dialog"></div></div>
|
||||||
<script>
|
<script>
|
||||||
const $=id=>document.getElementById(id);let current="",me=null,currentMask="",navigationRoot="";
|
const $=id=>document.getElementById(id);let current="",me=null,currentMask="",navigationRoot="";
|
||||||
async function api(url,opt={}){const r=await fetch(url,{cache:"no-store",...opt});if(!r.ok){let t="请求失败";try{t=(await r.json()).error||t}catch{};const e=Error(t);e.status=r.status;throw e}return r.status===204?null:r.json()}
|
async function api(url,opt={}){const r=await fetch(url,{credentials:"same-origin",cache:"no-store",...opt});if(!r.ok){let t="请求失败";try{t=(await r.json()).error||t}catch{};const e=Error(t);e.status=r.status;throw e}return r.status===204?null:r.json()}
|
||||||
function esc(s){return String(s).replace(/[&<>"']/g,c=>({"&":"&","<":"<",">":">","\"":""","'":"'"}[c]))}
|
function esc(s){return String(s).replace(/[&<>"']/g,c=>({"&":"&","<":"<",">":">","\"":""","'":"'"}[c]))}
|
||||||
function fmt(n){if(n<1024)return n+" B";if(n<1048576)return (n/1024).toFixed(1)+" KB";if(n<1073741824)return (n/1048576).toFixed(1)+" MB";return (n/1073741824).toFixed(1)+" GB"}
|
function fmt(n){if(n<1024)return n+" B";if(n<1048576)return (n/1024).toFixed(1)+" KB";if(n<1073741824)return (n/1048576).toFixed(1)+" MB";return (n/1073741824).toFixed(1)+" GB"}
|
||||||
async function init(){try{me=await api("/chfs/me")}catch{me={username:"",home:"",admin:false}};updateUser();current=me.home||"";navigationRoot=me.home||"";const h=new URLSearchParams(location.hash.slice(1));const shared=h.get("folder");if(shared!==null)current=shared;await refresh()}
|
async function init(){try{me=await api("/chfs/me")}catch{me={username:"",home:"",admin:false}};updateUser();current=me.home||"";navigationRoot=me.home||"";const h=new URLSearchParams(location.hash.slice(1));const shared=h.get("folder");if(shared!==null)current=shared;await refresh()}
|
||||||
@@ -168,7 +168,7 @@ async function previewFile(p){const n=p.split('/').pop(),ext=n.toLowerCase().spl
|
|||||||
function changePassword(){show('<h2>修改密码</h2><div class="field"><input id="cp" type="password" placeholder="当前密码"><input id="npw" type="password" placeholder="新密码"></div><div class="row"><button class="ghost" onclick="closeModal()">取消</button><button onclick="savePassword()">保存</button></div>')}
|
function changePassword(){show('<h2>修改密码</h2><div class="field"><input id="cp" type="password" placeholder="当前密码"><input id="npw" type="password" placeholder="新密码"></div><div class="row"><button class="ghost" onclick="closeModal()">取消</button><button onclick="savePassword()">保存</button></div>')}
|
||||||
async function savePassword(){try{await api("/chfs/password",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({currentPassword:$("cp").value,password:$("npw").value})});toast("密码已更新");closeModal()}catch(e){fail(e.message)}}
|
async function savePassword(){try{await api("/chfs/password",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({currentPassword:$("cp").value,password:$("npw").value})});toast("密码已更新");closeModal()}catch(e){fail(e.message)}}
|
||||||
function openLogin(){show('<h2>登录</h2><div class="field"><label>用户名</label><input id="u" autocomplete="username"></div><div class="field"><label>密码</label><input id="p" type="password" autocomplete="current-password"></div><div class="row"><button class="ghost" onclick="closeModal()">取消</button><button onclick="login()">登录</button></div>')}
|
function openLogin(){show('<h2>登录</h2><div class="field"><label>用户名</label><input id="u" autocomplete="username"></div><div class="field"><label>密码</label><input id="p" type="password" autocomplete="current-password"></div><div class="row"><button class="ghost" onclick="closeModal()">取消</button><button onclick="login()">登录</button></div>')}
|
||||||
async function login(){try{await fetch("/chfs/session",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({user:$("u").value,pwd:$("p").value})}).then(async r=>{if(!r.ok)throw Error((await r.json()).error||"登录失败")});closeModal();await init()}catch(e){fail(e.message)}}
|
async function login(){try{await fetch("/chfs/session",{method:"POST",credentials:"same-origin",cache:"no-store",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({user:$("u").value,pwd:$("p").value})}).then(async r=>{if(!r.ok)throw Error((await r.json()).error||"登录失败")});closeModal();await init()}catch(e){fail(e.message)}}
|
||||||
async function logout(){try{await api("/chfs/session",{method:"DELETE"})}finally{me={username:"",home:"",admin:false,managed:false};current="";navigationRoot="";selected.clear();updateUser();await refresh()}}
|
async function logout(){try{await api("/chfs/session",{method:"DELETE"})}finally{me={username:"",home:"",admin:false,managed:false};current="";navigationRoot="";selected.clear();updateUser();await refresh()}}
|
||||||
function toast(message,error=false){const host=$("toastHost"),el=document.createElement("div");el.className='toast'+(error?' error':'');el.textContent=message;host.appendChild(el);setTimeout(()=>el.remove(),2800)}
|
function toast(message,error=false){const host=$("toastHost"),el=document.createElement("div");el.className='toast'+(error?' error':'');el.textContent=message;host.appendChild(el);setTimeout(()=>el.remove(),2800)}
|
||||||
function fail(message){toast(message,true)}
|
function fail(message){toast(message,true)}
|
||||||
|
|||||||
Reference in New Issue
Block a user