Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in / Register
Toggle navigation
Y
yinzhuang
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
xuchentao
yinzhuang
Commits
61fd98b5
Commit
61fd98b5
authored
Aug 06, 2026
by
xuchentao
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: support admin password changes
parent
74052cff
Pipeline
#456
passed with stage
in 21 seconds
Changes
8
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
290 additions
and
9 deletions
+290
-9
.env.example
.env.example
+1
-0
CMS_README.md
CMS_README.md
+2
-1
app.js
public/admin/app.js
+40
-0
style.css
public/admin/style.css
+6
-0
cms-api.ts
src/lib/cms-api.ts
+29
-8
cms-auth.ts
src/lib/cms-auth.ts
+89
-0
index.astro
src/pages/admin/index.astro
+14
-0
auth.test.ts
tests/auth.test.ts
+109
-0
No files found.
.env.example
View file @
61fd98b5
# 复制为 .env 后使用。生产环境请生成独立密钥。
CMS_PORT=8791
# 后台主密码始终有效,用于登录和找回;在后台修改的用户密码会另存于 CMS_DATA_DIR。
CMS_PASSWORD=replace-with-a-strong-password
CMS_SECRET=replace-with-openssl-rand-hex-32
CMS_API_KEY=replace-with-openssl-rand-hex-32
...
...
CMS_README.md
View file @
61fd98b5
...
...
@@ -35,7 +35,7 @@ npm start
启动命令会自动读取项目根目录的
`.env`
,其中包含:
-
`CMS_PORT`
:官网与后台共用的服务端口,默认
`8791`
-
`CMS_PASSWORD`
:后台
登录密码
-
`CMS_PASSWORD`
:后台
主密码;用户在后台修改密码后,该密码仍可用于登录和找回(修改
`.env`
后需重启应用)
-
`CMS_SECRET`
:后台会话签名密钥
-
`CMS_API_KEY`
:外部程序调用管理 API 时使用的 Bearer Token
-
`CMS_DATA_DIR`
:生产环境的用户数据目录,建议设置为仓库外的绝对路径
...
...
@@ -50,6 +50,7 @@ npm start
| 会话 |
`GET /api/cms/session`
| 查询登录状态 |
| 会话 |
`POST /api/cms/session`
| 登录后台 |
| 会话 |
`DELETE /api/cms/session`
| 退出登录 |
| 账号 |
`PUT /api/cms/account/password`
| 修改后台用户密码并注销现有会话 |
| 分类 |
`GET /api/cms/categories`
| 分类列表与文章数量 |
| 分类 |
`POST /api/cms/categories`
| 新建分类 |
| 分类 |
`PATCH /api/cms/categories`
| 重命名分类 |
...
...
public/admin/app.js
View file @
61fd98b5
...
...
@@ -66,6 +66,46 @@ $("#login-form").addEventListener("submit", async (event) => {
$
(
"#logout"
).
addEventListener
(
"click"
,
async
()
=>
{
await
api
(
"/session"
,
{
method
:
"DELETE"
});
location
.
reload
();
});
function
closePasswordModal
()
{
hide
(
"#password-modal"
);
$
(
"#password-form"
).
reset
();
$
(
"#password-error"
).
textContent
=
""
;
}
$
(
"#change-password"
).
addEventListener
(
"click"
,
()
=>
{
$
(
"#password-form"
).
reset
();
$
(
"#password-error"
).
textContent
=
""
;
show
(
"#password-modal"
);
$
(
"#current-password"
).
focus
();
});
$
(
"#close-password-modal"
).
addEventListener
(
"click"
,
closePasswordModal
);
$
(
"#cancel-password"
).
addEventListener
(
"click"
,
closePasswordModal
);
$
(
"#password-form"
).
addEventListener
(
"submit"
,
async
(
event
)
=>
{
event
.
preventDefault
();
const
currentPassword
=
$
(
"#current-password"
).
value
;
const
newPassword
=
$
(
"#new-password"
).
value
;
const
confirmPassword
=
$
(
"#confirm-password"
).
value
;
const
submit
=
event
.
submitter
;
$
(
"#password-error"
).
textContent
=
""
;
if
(
newPassword
!==
confirmPassword
)
{
$
(
"#password-error"
).
textContent
=
"两次输入的新密码不一致"
;
return
;
}
if
(
submit
)
submit
.
disabled
=
true
;
try
{
await
api
(
"/account/password"
,
{
method
:
"PUT"
,
body
:
{
currentPassword
,
newPassword
}
});
closePasswordModal
();
alert
(
"密码修改成功,请重新登录后台"
);
showLogin
();
}
catch
(
error
)
{
$
(
"#password-error"
).
textContent
=
error
.
message
;
}
finally
{
if
(
submit
)
submit
.
disabled
=
false
;
}
});
const
statusMap
=
{
"new-unsaved"
:
[
"尚未保存"
,
"new-draft"
,
"填写完成后先保存草稿,线上网站不会发生变化。"
],
"new-draft"
:
[
"新建未发布"
,
"new-draft"
,
"这篇文章目前只有草稿版本,发布后才会出现在官网。"
],
...
...
public/admin/style.css
View file @
61fd98b5
...
...
@@ -123,6 +123,12 @@ input:focus, textarea:focus, select:focus { outline: 2px solid rgba(209,37,26,.1
.modal-card
>
.modal-head
{
padding
:
18px
20px
;
}
.modal-head
>
div
{
display
:
grid
;
gap
:
4px
;
}
.modal-head
small
{
color
:
var
(
--muted
);
font-size
:
12px
;
font-weight
:
400
;
}
.password-card
{
width
:
min
(
500px
,
100%
);
}
.modal-card
>
.password-form
{
padding
:
20px
;
display
:
grid
;
gap
:
14px
;
border
:
0
;
}
.password-form
label
{
display
:
grid
;
gap
:
7px
;
color
:
var
(
--muted
);
font-size
:
13px
;
}
.password-hint
{
margin
:
0
;
color
:
var
(
--muted
);
font-size
:
12px
;
line-height
:
1.6
;
}
.password-form
.error
{
margin
:
-4px
0
0
;
}
.password-actions
{
display
:
flex
;
justify-content
:
flex-end
;
gap
:
9px
;
}
.modal-card
>
.category-manager-body
{
max-height
:
68vh
;
padding
:
20px
;
display
:
block
;
overflow-y
:
auto
;
border
:
0
;
}
.manager-create
{
display
:
flex
;
gap
:
9px
;
}
.manager-create
input
{
min-width
:
0
;
}
...
...
src/lib/cms-api.ts
View file @
61fd98b5
...
...
@@ -22,9 +22,9 @@ import {
writeArticle
,
}
from
"./article-store"
;
import
{
convertWordToMarkdown
,
decodeWordDataUrl
}
from
"./word-import"
;
import
{
currentSessionVersion
,
savePassword
,
verifyPassword
}
from
"./cms-auth"
;
import
{
buildSiteAtomic
,
tryAcquireSiteBuildLock
,
withSiteBuildLock
}
from
"../../scripts/site-build.mjs"
;
const
PASSWORD
=
process
.
env
.
CMS_PASSWORD
||
"admin"
;
const
SECRET
=
process
.
env
.
CMS_SECRET
||
crypto
.
randomBytes
(
32
).
toString
(
"hex"
);
const
API_KEY
=
process
.
env
.
CMS_API_KEY
||
""
;
const
CAPTCHA_TTL
=
5
*
60
*
1000
;
...
...
@@ -50,7 +50,9 @@ const safeEqual = (leftValue: unknown, rightValue: unknown): boolean => {
return
left
.
length
===
right
.
length
&&
crypto
.
timingSafeEqual
(
left
,
right
);
};
const
sessionToken
=
()
=>
crypto
.
createHmac
(
"sha256"
,
SECRET
).
update
(
"yinzhuang-cms-v1"
).
digest
(
"hex"
);
const
sessionToken
=
async
()
=>
crypto
.
createHmac
(
"sha256"
,
SECRET
)
.
update
(
`yinzhuang-cms-v2:
${
await
currentSessionVersion
()}
`
)
.
digest
(
"hex"
);
function
cookies
(
request
:
Request
):
Record
<
string
,
string
>
{
return
Object
.
fromEntries
((
request
.
headers
.
get
(
"cookie"
)
||
""
).
split
(
";"
).
filter
(
Boolean
).
map
((
part
)
=>
{
...
...
@@ -66,8 +68,12 @@ function apiKeyValid(request: Request): boolean {
return
key
?
safeEqual
(
key
,
API_KEY
)
:
false
;
}
const
authed
=
(
request
:
Request
):
boolean
=>
Boolean
(
cookies
(
request
).
cms_session
&&
safeEqual
(
cookies
(
request
).
cms_session
,
sessionToken
()))
||
apiKeyValid
(
request
);
const
sessionAuthed
=
async
(
request
:
Request
):
Promise
<
boolean
>
=>
{
const
token
=
cookies
(
request
).
cms_session
;
return
Boolean
(
token
&&
safeEqual
(
token
,
await
sessionToken
()));
};
const
authed
=
async
(
request
:
Request
):
Promise
<
boolean
>
=>
await
sessionAuthed
(
request
)
||
apiKeyValid
(
request
);
async
function
bodyOf
(
request
:
Request
):
Promise
<
Record
<
string
,
unknown
>>
{
try
{
return
await
request
.
json
();
}
catch
{
return
{};
}
...
...
@@ -212,7 +218,7 @@ export async function handleCmsApi(request: Request, routeValue: string, clientA
}
if
(
route
===
"session"
)
{
if
(
method
===
"GET"
)
return
json
({
authed
:
authed
(
request
)
});
if
(
method
===
"GET"
)
return
json
({
authed
:
a
wait
a
uthed
(
request
)
});
if
(
method
===
"POST"
)
{
const
blockedFor
=
loginBlockedFor
(
clientAddress
);
if
(
blockedFor
)
return
json
({
error
:
`尝试过于频繁,请
${
Math
.
ceil
(
blockedFor
/
60
)}
分钟后再试`
},
429
);
...
...
@@ -221,20 +227,35 @@ export async function handleCmsApi(request: Request, routeValue: string, clientA
if
(
!
captchaValid
(
request
,
body
.
captcha
))
{
return
json
({
error
:
"验证码错误或已过期"
},
400
,
{
"Set-Cookie"
:
clearCaptcha
});
}
if
(
!
safeEqual
(
body
.
password
||
""
,
PASSWORD
))
{
if
(
!
await
verifyPassword
(
body
.
password
))
{
loginFailed
(
clientAddress
);
return
json
({
error
:
"管理密码错误"
},
401
,
{
"Set-Cookie"
:
clearCaptcha
});
}
loginAttempts
.
delete
(
clientAddress
);
const
headers
=
new
Headers
();
headers
.
append
(
"Set-Cookie"
,
clearCaptcha
);
headers
.
append
(
"Set-Cookie"
,
cookieHeader
(
request
,
"cms_session"
,
sessionToken
(),
86400
));
headers
.
append
(
"Set-Cookie"
,
cookieHeader
(
request
,
"cms_session"
,
await
sessionToken
(),
86400
));
return
json
({
ok
:
true
},
200
,
headers
);
}
if
(
method
===
"DELETE"
)
return
json
({
ok
:
true
},
200
,
{
"Set-Cookie"
:
cookieHeader
(
request
,
"cms_session"
,
""
,
0
)
});
}
if
(
!
authed
(
request
))
throw
new
StoreError
(
"请先登录文章后台"
,
401
);
if
(
route
===
"account/password"
&&
method
===
"PUT"
)
{
if
(
!
await
sessionAuthed
(
request
))
throw
new
StoreError
(
"请先登录文章后台"
,
401
);
const
body
=
await
bodyOf
(
request
);
const
currentPassword
=
String
(
body
.
currentPassword
||
""
);
const
newPassword
=
String
(
body
.
newPassword
||
""
);
if
(
!
await
verifyPassword
(
currentPassword
))
throw
new
StoreError
(
"当前密码错误"
,
401
);
if
(
newPassword
.
length
<
10
)
throw
new
StoreError
(
"新密码至少需要 10 个字符"
);
if
(
newPassword
.
length
>
128
)
throw
new
StoreError
(
"新密码不能超过 128 个字符"
);
if
(
safeEqual
(
currentPassword
,
newPassword
))
throw
new
StoreError
(
"新密码不能与当前密码相同"
);
await
savePassword
(
newPassword
);
return
json
({
ok
:
true
,
requiresLogin
:
true
},
200
,
{
"Set-Cookie"
:
cookieHeader
(
request
,
"cms_session"
,
""
,
0
),
});
}
if
(
!
await
authed
(
request
))
throw
new
StoreError
(
"请先登录文章后台"
,
401
);
if
(
route
===
"categories"
)
{
if
(
method
===
"GET"
)
return
json
({
categories
:
await
getCategoryNames
(),
stats
:
await
getCategoryStats
()
});
...
...
src/lib/cms-auth.ts
0 → 100644
View file @
61fd98b5
import
crypto
from
"node:crypto"
;
import
fs
from
"node:fs/promises"
;
import
path
from
"node:path"
;
const
ROOT
=
process
.
cwd
();
const
DATA_ROOT
=
process
.
env
.
CMS_DATA_DIR
?
path
.
resolve
(
process
.
env
.
CMS_DATA_DIR
)
:
path
.
join
(
ROOT
,
".runtime"
);
const
AUTH_DIR
=
path
.
join
(
DATA_ROOT
,
"auth"
);
const
PASSWORD_FILE
=
path
.
join
(
AUTH_DIR
,
"password.json"
);
const
ENV_PASSWORD
=
process
.
env
.
CMS_PASSWORD
||
"admin"
;
const
HASH_LENGTH
=
64
;
const
SCRYPT_OPTIONS
=
{
N
:
16384
,
r
:
8
,
p
:
1
,
maxmem
:
32
*
1024
*
1024
};
type
PasswordState
=
{
version
:
1
;
salt
:
string
;
hash
:
string
;
sessionVersion
:
number
;
updatedAt
:
string
;
};
const
safeEqual
=
(
leftValue
:
unknown
,
rightValue
:
unknown
):
boolean
=>
{
const
left
=
Buffer
.
from
(
String
(
leftValue
));
const
right
=
Buffer
.
from
(
String
(
rightValue
));
return
left
.
length
===
right
.
length
&&
crypto
.
timingSafeEqual
(
left
,
right
);
};
async
function
readPasswordState
():
Promise
<
PasswordState
|
null
>
{
try
{
const
parsed
=
JSON
.
parse
(
await
fs
.
readFile
(
PASSWORD_FILE
,
"utf8"
))
as
Partial
<
PasswordState
>
;
if
(
parsed
.
version
!==
1
||
typeof
parsed
.
salt
!==
"string"
||
typeof
parsed
.
hash
!==
"string"
||
!
Number
.
isInteger
(
parsed
.
sessionVersion
))
{
throw
new
Error
(
"后台密码文件格式无效"
);
}
return
parsed
as
PasswordState
;
}
catch
(
error
)
{
if
((
error
as
NodeJS
.
ErrnoException
).
code
===
"ENOENT"
)
return
null
;
throw
error
;
}
}
async
function
hashPassword
(
password
:
string
,
salt
:
Buffer
):
Promise
<
Buffer
>
{
return
new
Promise
((
resolve
,
reject
)
=>
{
crypto
.
scrypt
(
password
,
salt
,
HASH_LENGTH
,
SCRYPT_OPTIONS
,
(
error
,
derivedKey
)
=>
{
if
(
error
)
reject
(
error
);
else
resolve
(
derivedKey
);
});
});
}
export
async
function
verifyPassword
(
password
:
unknown
):
Promise
<
boolean
>
{
// Check the .env credential first so it remains a recovery password even if
// the optional persisted password file is missing or damaged.
if
(
safeEqual
(
password
||
""
,
ENV_PASSWORD
))
return
true
;
const
state
=
await
readPasswordState
();
if
(
!
state
)
return
false
;
const
hash
=
await
hashPassword
(
String
(
password
||
""
),
Buffer
.
from
(
state
.
salt
,
"base64"
));
return
safeEqual
(
hash
.
toString
(
"base64"
),
state
.
hash
);
}
export
async
function
currentSessionVersion
():
Promise
<
number
>
{
try
{
return
(
await
readPasswordState
())?.
sessionVersion
||
0
;
}
catch
(
error
)
{
// Keep .env login and recovery available if the optional file is corrupt.
console
.
error
(
"读取后台密码文件失败:"
,
error
);
return
0
;
}
}
export
async
function
savePassword
(
password
:
string
):
Promise
<
void
>
{
const
previous
=
await
readPasswordState
().
catch
(()
=>
null
);
const
salt
=
crypto
.
randomBytes
(
16
);
const
hash
=
await
hashPassword
(
password
,
salt
);
const
state
:
PasswordState
=
{
version
:
1
,
salt
:
salt
.
toString
(
"base64"
),
hash
:
hash
.
toString
(
"base64"
),
sessionVersion
:
(
previous
?.
sessionVersion
||
0
)
+
1
,
updatedAt
:
new
Date
().
toISOString
(),
};
await
fs
.
mkdir
(
AUTH_DIR
,
{
recursive
:
true
,
mode
:
0o700
});
const
temporaryFile
=
`
${
PASSWORD_FILE
}
.
${
process
.
pid
}
.
${
crypto
.
randomBytes
(
6
).
toString
(
"hex"
)}
.tmp`
;
try
{
await
fs
.
writeFile
(
temporaryFile
,
`
${
JSON
.
stringify
(
state
,
null
,
2
)}
\n`
,
{
mode
:
0o600
,
flag
:
"wx"
});
await
fs
.
rename
(
temporaryFile
,
PASSWORD_FILE
);
}
catch
(
error
)
{
await
fs
.
rm
(
temporaryFile
,
{
force
:
true
}).
catch
(()
=>
{});
throw
error
;
}
}
src/pages/admin/index.astro
View file @
61fd98b5
...
...
@@ -30,6 +30,7 @@
<div
class=
"top-actions"
>
<span
id=
"pending-badge"
class=
"pending-badge hidden"
></span>
<a
class=
"button ghost"
href=
"/articles/"
target=
"_blank"
rel=
"noopener"
>
查看前台
</a>
<button
id=
"change-password"
class=
"ghost"
type=
"button"
>
修改密码
</button>
<button
id=
"logout"
class=
"ghost"
>
退出
</button>
</div>
</header>
...
...
@@ -101,6 +102,19 @@
</div>
</div>
</div>
<div
id=
"password-modal"
class=
"modal hidden"
>
<div
class=
"modal-card password-card"
>
<div
class=
"modal-head"
><div><strong>
修改后台密码
</strong><small>
修改成功后需要使用新密码重新登录
</small></div><button
id=
"close-password-modal"
class=
"ghost small"
type=
"button"
>
关闭
</button></div>
<form
id=
"password-form"
class=
"password-form"
>
<label>
当前密码
<input
id=
"current-password"
type=
"password"
autocomplete=
"current-password"
required
/></label>
<label>
新密码
<input
id=
"new-password"
type=
"password"
autocomplete=
"new-password"
minlength=
"10"
maxlength=
"128"
required
/></label>
<label>
确认新密码
<input
id=
"confirm-password"
type=
"password"
autocomplete=
"new-password"
minlength=
"10"
maxlength=
"128"
required
/></label>
<p
class=
"password-hint"
>
新密码至少 10 个字符。.env 中的管理密码仍可用于登录和找回。
</p>
<p
id=
"password-error"
class=
"error"
></p>
<div
class=
"password-actions"
><button
id=
"cancel-password"
class=
"ghost"
type=
"button"
>
取消
</button><button
class=
"primary"
type=
"submit"
>
确认修改
</button></div>
</form>
</div>
</div>
<div
id=
"build-modal"
class=
"modal hidden"
>
<div
class=
"modal-card build-card"
>
<div><strong
id=
"build-title"
>
正在处理
</strong><button
id=
"close-modal"
class=
"ghost small hidden"
type=
"button"
>
关闭
</button></div>
...
...
tests/auth.test.ts
0 → 100644
View file @
61fd98b5
import
assert
from
"node:assert/strict"
;
import
fs
from
"node:fs/promises"
;
import
os
from
"node:os"
;
import
path
from
"node:path"
;
import
test
,
{
after
}
from
"node:test"
;
const
dataDirectory
=
await
fs
.
mkdtemp
(
path
.
join
(
os
.
tmpdir
(),
"yinzhuang-auth-"
));
process
.
env
.
CMS_DATA_DIR
=
dataDirectory
;
process
.
env
.
CMS_PASSWORD
=
"environment-master-password"
;
process
.
env
.
CMS_SECRET
=
"auth-test-session-secret"
;
process
.
env
.
CMS_API_KEY
=
"auth-test-api-key"
;
const
{
handleCmsApi
}
=
await
import
(
"../src/lib/cms-api"
);
let
requestNumber
=
0
;
after
(
async
()
=>
{
await
fs
.
rm
(
dataDirectory
,
{
recursive
:
true
,
force
:
true
});
});
function
cookieFrom
(
response
:
Response
,
name
:
string
):
string
{
const
header
=
response
.
headers
.
get
(
"set-cookie"
)
||
""
;
const
match
=
header
.
match
(
new
RegExp
(
`(?:^|,\\s*)
${
name
}
=([^;]*)`
));
assert
.
ok
(
match
,
`missing
${
name
}
cookie in
${
header
}
`
);
return
`
${
name
}
=
${
match
[
1
]}
`
;
}
async
function
login
(
password
:
string
):
Promise
<
{
response
:
Response
;
cookie
?:
string
}
>
{
requestNumber
+=
1
;
const
captchaResponse
=
await
handleCmsApi
(
new
Request
(
"http://localhost/api/cms/captcha"
),
"captcha"
,
`captcha-
${
requestNumber
}
`
,
);
const
code
=
[...(
await
captchaResponse
.
text
()).
matchAll
(
/<text
\b[^
>
]
*>
([^
<
])
<
\/
text>/g
)]
.
map
((
match
)
=>
match
[
1
])
.
join
(
""
);
assert
.
equal
(
code
.
length
,
4
);
const
response
=
await
handleCmsApi
(
new
Request
(
"http://localhost/api/cms/session"
,
{
method
:
"POST"
,
headers
:
{
"Content-Type"
:
"application/json"
,
Cookie
:
cookieFrom
(
captchaResponse
,
"cms_captcha"
),
},
body
:
JSON
.
stringify
({
password
,
captcha
:
code
}),
}),
"session"
,
`login-
${
requestNumber
}
`
);
return
{
response
,
cookie
:
response
.
ok
?
cookieFrom
(
response
,
"cms_session"
)
:
undefined
,
};
}
async
function
sessionStatus
(
cookie
:
string
):
Promise
<
boolean
>
{
const
response
=
await
handleCmsApi
(
new
Request
(
"http://localhost/api/cms/session"
,
{
headers
:
{
Cookie
:
cookie
},
}),
"session"
);
return
Boolean
((
await
response
.
json
()).
authed
);
}
async
function
changePassword
(
cookie
:
string
,
currentPassword
:
string
,
newPassword
:
string
):
Promise
<
Response
>
{
return
handleCmsApi
(
new
Request
(
"http://localhost/api/cms/account/password"
,
{
method
:
"PUT"
,
headers
:
{
"Content-Type"
:
"application/json"
,
Cookie
:
cookie
},
body
:
JSON
.
stringify
({
currentPassword
,
newPassword
}),
}),
"account/password"
);
}
test
(
"users can change the persisted password while the .env password remains valid"
,
async
()
=>
{
const
initialLogin
=
await
login
(
"environment-master-password"
);
assert
.
equal
(
initialLogin
.
response
.
status
,
200
);
assert
.
ok
(
initialLogin
.
cookie
);
const
firstPassword
=
"first-user-password"
;
const
changed
=
await
changePassword
(
initialLogin
.
cookie
,
"environment-master-password"
,
firstPassword
);
assert
.
equal
(
changed
.
status
,
200
);
assert
.
match
(
changed
.
headers
.
get
(
"set-cookie"
)
||
""
,
/cms_session=;/
);
assert
.
equal
((
await
changed
.
json
()).
requiresLogin
,
true
);
assert
.
equal
(
await
sessionStatus
(
initialLogin
.
cookie
),
false
);
const
stored
=
await
fs
.
readFile
(
path
.
join
(
dataDirectory
,
"auth"
,
"password.json"
),
"utf8"
);
assert
.
doesNotMatch
(
stored
,
new
RegExp
(
firstPassword
));
const
userLogin
=
await
login
(
firstPassword
);
assert
.
equal
(
userLogin
.
response
.
status
,
200
);
assert
.
ok
(
userLogin
.
cookie
);
assert
.
equal
((
await
login
(
"environment-master-password"
)).
response
.
status
,
200
);
const
secondPassword
=
"second-user-password"
;
const
changedAgain
=
await
changePassword
(
userLogin
.
cookie
,
firstPassword
,
secondPassword
);
assert
.
equal
(
changedAgain
.
status
,
200
);
assert
.
equal
(
await
sessionStatus
(
userLogin
.
cookie
),
false
);
assert
.
equal
((
await
login
(
firstPassword
)).
response
.
status
,
401
);
assert
.
equal
((
await
login
(
secondPassword
)).
response
.
status
,
200
);
assert
.
equal
((
await
login
(
"environment-master-password"
)).
response
.
status
,
200
);
});
test
(
"an API key alone cannot change the browser login password"
,
async
()
=>
{
const
response
=
await
handleCmsApi
(
new
Request
(
"http://localhost/api/cms/account/password"
,
{
method
:
"PUT"
,
headers
:
{
Authorization
:
"Bearer auth-test-api-key"
,
"Content-Type"
:
"application/json"
,
},
body
:
JSON
.
stringify
({
currentPassword
:
"environment-master-password"
,
newPassword
:
"api-key-must-not-change-this"
,
}),
}),
"account/password"
);
assert
.
equal
(
response
.
status
,
401
);
});
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment