- 后端:Lua 实现 Token 验证、权限控制、限流、审计日志 - API:创建/列出/查看/更新/删除 Token - Web UI:Token 管理界面(列表、创建、详情、编辑、删除弹框) - 向后兼容:保留 X-Password 超级管理员认证 - 安全增强:失败次数限制、过期检测、双因素验证(敏感操作) 需求文档:TOKEN_AUTH_REQUIREMENT.md
951 lines
41 KiB
Nginx Configuration File
951 lines
41 KiB
Nginx Configuration File
worker_processes 1;
|
||
error_log stderr warn;
|
||
pid /tmp/nginx.pid;
|
||
|
||
events {
|
||
worker_connections 1024;
|
||
}
|
||
|
||
http {
|
||
include /usr/local/openresty/nginx/conf/mime.types;
|
||
default_type application/json;
|
||
|
||
access_log /dev/stdout;
|
||
|
||
sendfile on;
|
||
keepalive_timeout 65;
|
||
client_max_body_size 10M;
|
||
|
||
# Lua 共享字典用于缓存
|
||
lua_shared_dict data_cache 10m;
|
||
|
||
server {
|
||
listen 80;
|
||
server_name _;
|
||
|
||
# CORS 配置
|
||
add_header 'Access-Control-Allow-Origin' '*' always;
|
||
add_header 'Access-Control-Allow-Methods' 'GET, POST, DELETE, OPTIONS' always;
|
||
add_header 'Access-Control-Allow-Headers' 'Content-Type, X-Password, X-Token' always;
|
||
|
||
# OPTIONS 请求
|
||
if ($request_method = 'OPTIONS') {
|
||
return 204;
|
||
}
|
||
|
||
# 健康检查
|
||
location = /health {
|
||
access_log off;
|
||
return 200 '{"status":"ok"}';
|
||
add_header Content-Type application/json;
|
||
}
|
||
|
||
# 登录页面
|
||
location = /login {
|
||
root /usr/local/openresty/nginx/html;
|
||
try_files /login.html =404;
|
||
}
|
||
|
||
# 登录API
|
||
location = /api/login {
|
||
content_by_lua_block {
|
||
local cjson = require "cjson"
|
||
ngx.req.read_body()
|
||
local body = ngx.req.get_body_data()
|
||
local ok, data = pcall(cjson.decode, body)
|
||
|
||
if ok and data.password == "admin123123123" then
|
||
ngx.header["Set-Cookie"] = "auth=verified; Path=/; Max-Age=604800"
|
||
ngx.status = 200
|
||
ngx.say(cjson.encode({success = true}))
|
||
else
|
||
ngx.status = 401
|
||
ngx.say(cjson.encode({error = "Invalid password"}))
|
||
end
|
||
}
|
||
}
|
||
|
||
# 静态文件(需要认证)
|
||
location / {
|
||
access_by_lua_block {
|
||
local cookie = ngx.var.http_cookie
|
||
if not cookie or not string.match(cookie, "auth=verified") then
|
||
return ngx.redirect("/login")
|
||
end
|
||
}
|
||
root /usr/local/openresty/nginx/html;
|
||
index index.html;
|
||
}
|
||
|
||
# POST /api/data - 上传/更新数据
|
||
location = /api/data {
|
||
content_by_lua_block {
|
||
local cjson = require "cjson"
|
||
local method = ngx.req.get_method()
|
||
|
||
-- GET 请求:列出所有 tag
|
||
if method == "GET" then
|
||
-- 读取索引文件
|
||
local index_file = io.open("/data/index.json", "r")
|
||
if not index_file then
|
||
ngx.status = 200
|
||
ngx.say(cjson.encode({tags = {}}))
|
||
return
|
||
end
|
||
|
||
local index_content = index_file:read("*all")
|
||
index_file:close()
|
||
|
||
local ok, index = pcall(cjson.decode, index_content)
|
||
if not ok then
|
||
ngx.status = 500
|
||
ngx.say(cjson.encode({error = "Invalid index file"}))
|
||
return
|
||
end
|
||
|
||
-- 计算状态
|
||
local now = os.time()
|
||
for tag, item in pairs(index.tags) do
|
||
if item.expires_at then
|
||
local expires_time = os.time({
|
||
year = tonumber(item.expires_at:sub(1, 4)),
|
||
month = tonumber(item.expires_at:sub(6, 7)),
|
||
day = tonumber(item.expires_at:sub(9, 10)),
|
||
hour = tonumber(item.expires_at:sub(12, 13)),
|
||
min = tonumber(item.expires_at:sub(15, 16)),
|
||
sec = tonumber(item.expires_at:sub(18, 19))
|
||
})
|
||
|
||
if expires_time < now then
|
||
item.status = "expired"
|
||
elseif (expires_time - now) < 172800 then -- 2 days
|
||
item.status = "expiring_soon"
|
||
else
|
||
item.status = "valid"
|
||
end
|
||
else
|
||
item.status = "no_expiry"
|
||
end
|
||
end
|
||
|
||
ngx.status = 200
|
||
ngx.say(cjson.encode(index))
|
||
return
|
||
end
|
||
|
||
-- POST 请求:上传数据
|
||
if method == "POST" then
|
||
-- 读取请求体
|
||
ngx.req.read_body()
|
||
local body = ngx.req.get_body_data()
|
||
|
||
-- 如果 body 为空,可能是因为请求体被写入了临时文件
|
||
if not body then
|
||
local body_file = ngx.req.get_body_file()
|
||
if body_file then
|
||
local file = io.open(body_file, "r")
|
||
if file then
|
||
body = file:read("*all")
|
||
file:close()
|
||
end
|
||
end
|
||
end
|
||
|
||
if not body then
|
||
ngx.status = 400
|
||
ngx.say(cjson.encode({error = "Empty body"}))
|
||
return
|
||
end
|
||
|
||
-- 记录请求体大小,方便调试
|
||
ngx.log(ngx.INFO, "Received body size: ", string.len(body))
|
||
|
||
-- 调试:保存请求体到文件以便分析
|
||
local debug_file = io.open("/tmp/last_request_body.json", "w")
|
||
if debug_file then
|
||
debug_file:write(body)
|
||
debug_file:close()
|
||
ngx.log(ngx.INFO, "Request body saved to /tmp/last_request_body.json")
|
||
end
|
||
|
||
-- 解析 JSON
|
||
local ok, data = pcall(cjson.decode, body)
|
||
if not ok then
|
||
-- 记录解析错误的详细信息
|
||
ngx.log(ngx.ERR, "JSON parse error: ", data)
|
||
ngx.log(ngx.ERR, "Body preview (first 500 chars): ", string.sub(body, 1, 500))
|
||
|
||
-- 保存出错的位置附近的内容
|
||
local error_pos = tonumber(string.match(tostring(data), "character (%d+)"))
|
||
if error_pos then
|
||
local start_pos = math.max(1, error_pos - 100)
|
||
local end_pos = math.min(string.len(body), error_pos + 100)
|
||
ngx.log(ngx.ERR, "Error context (chars ", start_pos, "-", end_pos, "): ", string.sub(body, start_pos, end_pos))
|
||
end
|
||
|
||
ngx.status = 400
|
||
ngx.say(cjson.encode({
|
||
error = "Invalid JSON",
|
||
details = tostring(data),
|
||
body_size = string.len(body),
|
||
body_preview = string.sub(body, 1, 200),
|
||
error_position = error_pos
|
||
}))
|
||
return
|
||
end
|
||
|
||
-- 验证密码
|
||
if data.password ~= "admin123123123" then
|
||
ngx.status = 401
|
||
ngx.say(cjson.encode({error = "Invalid password"}))
|
||
return
|
||
end
|
||
|
||
-- 验证必填字段
|
||
if not data.tag or not data.type or not data.data then
|
||
ngx.status = 400
|
||
ngx.say(cjson.encode({error = "Missing required fields: tag, type, data"}))
|
||
return
|
||
end
|
||
|
||
-- 生成文件名(tag 中的特殊字符替换为 _)
|
||
local filename = data.tag:gsub("[^%w%-]", "_") .. ".json"
|
||
local filepath = "/data/" .. filename
|
||
|
||
-- 添加时间戳
|
||
local timestamp = os.date("%Y-%m-%d %H:%M:%S")
|
||
if not data.metadata then
|
||
data.metadata = {}
|
||
end
|
||
if not data.metadata.created_at then
|
||
data.metadata.created_at = timestamp
|
||
end
|
||
data.metadata.updated_at = timestamp
|
||
|
||
-- 计算过期时间
|
||
if data.metadata.expires_in then
|
||
local expires_at = os.time() + data.metadata.expires_in
|
||
data.metadata.expires_at = os.date("%Y-%m-%d %H:%M:%S", expires_at)
|
||
end
|
||
|
||
-- 移除密码字段
|
||
data.password = nil
|
||
|
||
-- 保存数据文件
|
||
local file = io.open(filepath, "w")
|
||
if not file then
|
||
ngx.status = 500
|
||
ngx.say(cjson.encode({error = "Failed to save file"}))
|
||
return
|
||
end
|
||
file:write(cjson.encode(data))
|
||
file:close()
|
||
|
||
-- 更新索引
|
||
local index_file = io.open("/data/index.json", "r")
|
||
local index = {tags = {}}
|
||
if index_file then
|
||
local index_content = index_file:read("*all")
|
||
index_file:close()
|
||
local ok, parsed = pcall(cjson.decode, index_content)
|
||
if ok then
|
||
index = parsed
|
||
end
|
||
end
|
||
|
||
-- 更新或添加 tag(tags是对象,直接赋值)
|
||
if not index.tags then
|
||
index.tags = {}
|
||
end
|
||
|
||
index.tags[data.tag] = {
|
||
tag = data.tag,
|
||
type = data.type,
|
||
file = filename,
|
||
created_at = index.tags[data.tag] and index.tags[data.tag].created_at or data.metadata.created_at,
|
||
updated_at = timestamp,
|
||
expires_at = data.metadata.expires_at
|
||
}
|
||
|
||
-- 保存索引
|
||
index_file = io.open("/data/index.json", "w")
|
||
if index_file then
|
||
index_file:write(cjson.encode(index))
|
||
index_file:close()
|
||
end
|
||
|
||
ngx.status = 200
|
||
ngx.say(cjson.encode({
|
||
success = true,
|
||
message = "Data saved",
|
||
tag = data.tag,
|
||
timestamp = timestamp
|
||
}))
|
||
return
|
||
end
|
||
|
||
-- 其他方法不支持
|
||
ngx.status = 405
|
||
ngx.say(cjson.encode({error = "Method not allowed"}))
|
||
}
|
||
}
|
||
|
||
# GET /api/data/copy/:tag - 一键复制格式
|
||
location ~ ^/api/data/copy/([^/]+)$ {
|
||
content_by_lua_block {
|
||
local cjson = require "cjson"
|
||
local tag = ngx.var[1]
|
||
|
||
-- 生成文件名
|
||
local filename = tag:gsub("[^%w%-]", "_") .. ".json"
|
||
local filepath = "/data/" .. filename
|
||
|
||
-- 读取数据
|
||
local file = io.open(filepath, "r")
|
||
if not file then
|
||
ngx.status = 404
|
||
ngx.say(cjson.encode({error = "Tag not found"}))
|
||
return
|
||
end
|
||
|
||
local content = file:read("*all")
|
||
file:close()
|
||
|
||
local ok, data = pcall(cjson.decode, content)
|
||
if not ok then
|
||
ngx.status = 500
|
||
ngx.say(cjson.encode({error = "Invalid data file"}))
|
||
return
|
||
end
|
||
|
||
-- 生成复制文本
|
||
local copy_config = {
|
||
serverUrl = "http://47.122.126.244:5001",
|
||
password = "admin123123123",
|
||
tag = tag,
|
||
type = data.type
|
||
}
|
||
|
||
local copy_text = cjson.encode(copy_config)
|
||
|
||
ngx.status = 200
|
||
ngx.say(cjson.encode({
|
||
copy_text = copy_text,
|
||
formatted = "```json\\n" .. copy_text .. "\\n```"
|
||
}))
|
||
}
|
||
}
|
||
|
||
# GET /api/data/:tag - 获取数据
|
||
# DELETE /api/data/:tag - 删除数据
|
||
location ~ ^/api/data/([^/]+)$ {
|
||
content_by_lua_block {
|
||
local cjson = require "cjson"
|
||
local tag = ngx.var[1]
|
||
local method = ngx.req.get_method()
|
||
|
||
-- 生成文件名
|
||
local filename = tag:gsub("[^%w%-]", "_") .. ".json"
|
||
local filepath = "/data/" .. filename
|
||
|
||
-- DELETE 请求
|
||
if method == "DELETE" then
|
||
-- 验证密码或 Token
|
||
local password = ngx.var.http_x_password
|
||
local token = ngx.var.http_x_token
|
||
local authenticated = false
|
||
|
||
-- Try token authentication first
|
||
if token then
|
||
local token_index_file = io.open("/data/tokens.json", "r")
|
||
if token_index_file then
|
||
local token_index_content = token_index_file:read("*all")
|
||
token_index_file:close()
|
||
|
||
local ok, token_index = pcall(cjson.decode, token_index_content)
|
||
if ok and token_index.tokens and token_index.tokens[token] then
|
||
local token_file = io.open("/data/" .. token_index.tokens[token].file, "r")
|
||
if token_file then
|
||
local token_content = token_file:read("*all")
|
||
token_file:close()
|
||
|
||
local ok, token_data = pcall(cjson.decode, token_content)
|
||
if ok then
|
||
if token_data.status == "disabled" then
|
||
ngx.status = 401
|
||
ngx.say(cjson.encode({error = "Token disabled"}))
|
||
return
|
||
end
|
||
|
||
if token_data.expires_at then
|
||
local expires_time = os.time({
|
||
year = tonumber(token_data.expires_at:sub(1, 4)),
|
||
month = tonumber(token_data.expires_at:sub(6, 7)),
|
||
day = tonumber(token_data.expires_at:sub(9, 10)),
|
||
hour = tonumber(token_data.expires_at:sub(12, 13)),
|
||
min = tonumber(token_data.expires_at:sub(15, 16)),
|
||
sec = tonumber(token_data.expires_at:sub(18, 19))
|
||
})
|
||
|
||
if expires_time < os.time() then
|
||
ngx.status = 401
|
||
ngx.say(cjson.encode({error = "Token expired", expires_at = token_data.expires_at}))
|
||
return
|
||
end
|
||
end
|
||
|
||
local limit_key = "rate_limit:" .. token
|
||
local dict = ngx.shared.data_cache
|
||
local count = dict:get(limit_key) or 0
|
||
|
||
if count >= 60 then
|
||
ngx.status = 429
|
||
ngx.say(cjson.encode({error = "Rate limit exceeded", message = "Maximum 60 requests per minute", retry_after = 60}))
|
||
return
|
||
end
|
||
|
||
dict:incr(limit_key, 1, 0, 60)
|
||
|
||
-- Check permission for DELETE (write operation)
|
||
if token_data.permission == "readonly" then
|
||
ngx.status = 403
|
||
ngx.say(cjson.encode({error = "Permission denied", message = "This token has readonly permission", required = "readwrite"}))
|
||
return
|
||
end
|
||
|
||
authenticated = true
|
||
|
||
-- Update stats
|
||
token_data.stats = token_data.stats or {}
|
||
token_data.stats.total_requests = (token_data.stats.total_requests or 0) + 1
|
||
token_data.stats.last_used_at = os.date("%Y-%m-%d %H:%M:%S")
|
||
token_data.stats.last_ip = ngx.var.remote_addr
|
||
token_data.stats.failed_attempts = 0
|
||
|
||
-- Add to access log
|
||
token_data.access_log = token_data.access_log or {}
|
||
table.insert(token_data.access_log, 1, {
|
||
timestamp = os.date("%Y-%m-%d %H:%M:%S"),
|
||
ip = ngx.var.remote_addr,
|
||
method = "DELETE",
|
||
path = ngx.var.uri,
|
||
status = 200
|
||
})
|
||
|
||
while #token_data.access_log > 20 do
|
||
table.remove(token_data.access_log)
|
||
end
|
||
|
||
-- Save updated token data
|
||
local save_file = io.open("/data/" .. token_index.tokens[token].file, "w")
|
||
if save_file then
|
||
save_file:write(cjson.encode(token_data))
|
||
save_file:close()
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
if not authenticated then
|
||
ngx.status = 401
|
||
ngx.say(cjson.encode({error = "Invalid token"}))
|
||
return
|
||
end
|
||
elseif password ~= "admin123123123" then
|
||
ngx.status = 401
|
||
ngx.say(cjson.encode({error = "Invalid password"}))
|
||
return
|
||
end
|
||
|
||
-- 删除文件
|
||
os.remove(filepath)
|
||
|
||
-- 更新索引
|
||
local index_file = io.open("/data/index.json", "r")
|
||
if index_file then
|
||
local index_content = index_file:read("*all")
|
||
index_file:close()
|
||
|
||
local ok, index = pcall(cjson.decode, index_content)
|
||
if ok then
|
||
-- tags 是对象,直接删除key
|
||
if index.tags and index.tags[tag] then
|
||
index.tags[tag] = nil
|
||
end
|
||
|
||
index_file = io.open("/data/index.json", "w")
|
||
if index_file then
|
||
index_file:write(cjson.encode(index))
|
||
index_file:close()
|
||
end
|
||
end
|
||
end
|
||
|
||
ngx.status = 200
|
||
ngx.say(cjson.encode({
|
||
success = true,
|
||
message = "Data deleted",
|
||
tag = tag
|
||
}))
|
||
return
|
||
end
|
||
|
||
-- GET 请求
|
||
if method == "GET" then
|
||
-- 验证密码或 Token
|
||
local password = ngx.var.http_x_password
|
||
local token = ngx.var.http_x_token
|
||
local authenticated = false
|
||
|
||
-- Try token authentication first
|
||
if token then
|
||
local token_index_file = io.open("/data/tokens.json", "r")
|
||
if token_index_file then
|
||
local token_index_content = token_index_file:read("*all")
|
||
token_index_file:close()
|
||
|
||
local ok, token_index = pcall(cjson.decode, token_index_content)
|
||
if ok and token_index.tokens and token_index.tokens[token] then
|
||
local token_file = io.open("/data/" .. token_index.tokens[token].file, "r")
|
||
if token_file then
|
||
local token_content = token_file:read("*all")
|
||
token_file:close()
|
||
|
||
local ok, token_data = pcall(cjson.decode, token_content)
|
||
if ok then
|
||
if token_data.status == "disabled" then
|
||
ngx.status = 401
|
||
ngx.say(cjson.encode({error = "Token disabled"}))
|
||
return
|
||
end
|
||
|
||
if token_data.expires_at then
|
||
local expires_time = os.time({
|
||
year = tonumber(token_data.expires_at:sub(1, 4)),
|
||
month = tonumber(token_data.expires_at:sub(6, 7)),
|
||
day = tonumber(token_data.expires_at:sub(9, 10)),
|
||
hour = tonumber(token_data.expires_at:sub(12, 13)),
|
||
min = tonumber(token_data.expires_at:sub(15, 16)),
|
||
sec = tonumber(token_data.expires_at:sub(18, 19))
|
||
})
|
||
|
||
if expires_time < os.time() then
|
||
ngx.status = 401
|
||
ngx.say(cjson.encode({error = "Token expired", expires_at = token_data.expires_at}))
|
||
return
|
||
end
|
||
end
|
||
|
||
local limit_key = "rate_limit:" .. token
|
||
local dict = ngx.shared.data_cache
|
||
local count = dict:get(limit_key) or 0
|
||
|
||
if count >= 60 then
|
||
ngx.status = 429
|
||
ngx.say(cjson.encode({error = "Rate limit exceeded", message = "Maximum 60 requests per minute", retry_after = 60}))
|
||
return
|
||
end
|
||
|
||
dict:incr(limit_key, 1, 0, 60)
|
||
authenticated = true
|
||
|
||
-- Update stats
|
||
token_data.stats = token_data.stats or {}
|
||
token_data.stats.total_requests = (token_data.stats.total_requests or 0) + 1
|
||
token_data.stats.last_used_at = os.date("%Y-%m-%d %H:%M:%S")
|
||
token_data.stats.last_ip = ngx.var.remote_addr
|
||
token_data.stats.failed_attempts = 0
|
||
|
||
-- Add to access log
|
||
token_data.access_log = token_data.access_log or {}
|
||
table.insert(token_data.access_log, 1, {
|
||
timestamp = os.date("%Y-%m-%d %H:%M:%S"),
|
||
ip = ngx.var.remote_addr,
|
||
method = "GET",
|
||
path = ngx.var.uri,
|
||
status = 200
|
||
})
|
||
|
||
while #token_data.access_log > 20 do
|
||
table.remove(token_data.access_log)
|
||
end
|
||
|
||
-- Save updated token data
|
||
local save_file = io.open("/data/" .. token_index.tokens[token].file, "w")
|
||
if save_file then
|
||
save_file:write(cjson.encode(token_data))
|
||
save_file:close()
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
if not authenticated then
|
||
ngx.status = 401
|
||
ngx.say(cjson.encode({error = "Invalid token"}))
|
||
return
|
||
end
|
||
elseif password ~= "admin123123123" then
|
||
ngx.status = 401
|
||
ngx.say(cjson.encode({error = "Invalid password"}))
|
||
return
|
||
end
|
||
|
||
-- 读取数据
|
||
local file = io.open(filepath, "r")
|
||
if not file then
|
||
ngx.status = 404
|
||
ngx.say(cjson.encode({error = "Tag not found"}))
|
||
return
|
||
end
|
||
|
||
local content = file:read("*all")
|
||
file:close()
|
||
|
||
ngx.status = 200
|
||
ngx.say(content)
|
||
return
|
||
end
|
||
|
||
-- 其他方法不支持
|
||
ngx.status = 405
|
||
ngx.say(cjson.encode({error = "Method not allowed"}))
|
||
}
|
||
}
|
||
|
||
# Token Management API
|
||
location = /api/tokens {
|
||
content_by_lua_block {
|
||
local cjson = require "cjson"
|
||
local method = ngx.req.get_method()
|
||
|
||
-- Verify admin password
|
||
local password = ngx.var.http_x_password
|
||
if password ~= "admin123123123" then
|
||
ngx.status = 401
|
||
ngx.say(cjson.encode({error = "Invalid password"}))
|
||
return
|
||
end
|
||
|
||
if method == "POST" then
|
||
-- Parse request
|
||
ngx.req.read_body()
|
||
local body = ngx.req.get_body_data()
|
||
local ok, data = pcall(cjson.decode, body)
|
||
|
||
if not ok or not data.name or not data.permission then
|
||
ngx.status = 400
|
||
ngx.say(cjson.encode({error = "Missing required fields: name, permission"}))
|
||
return
|
||
end
|
||
|
||
-- Validate permission
|
||
if data.permission ~= "readonly" and data.permission ~= "readwrite" then
|
||
ngx.status = 400
|
||
ngx.say(cjson.encode({error = "Invalid permission, must be readonly or readwrite"}))
|
||
return
|
||
end
|
||
|
||
-- Generate random token (dss_ + 32 hex chars)
|
||
math.randomseed(os.time() * 1000 + os.clock() * 1000000)
|
||
local charset = "0123456789abcdef"
|
||
local token_suffix = ""
|
||
for i = 1, 32 do
|
||
local rand = math.random(1, #charset)
|
||
token_suffix = token_suffix .. charset:sub(rand, rand)
|
||
end
|
||
local token = "dss_" .. token_suffix
|
||
|
||
-- Create token data
|
||
local timestamp = os.date("%Y-%m-%d %H:%M:%S")
|
||
local expires_at = nil
|
||
if data.expires_in and type(data.expires_in) == "number" and data.expires_in > 0 then
|
||
expires_at = os.date("%Y-%m-%d %H:%M:%S", os.time() + data.expires_in)
|
||
end
|
||
|
||
local token_data = {
|
||
token = token,
|
||
name = data.name,
|
||
permission = data.permission,
|
||
status = "active",
|
||
created_at = timestamp,
|
||
expires_at = expires_at,
|
||
stats = {
|
||
total_requests = 0,
|
||
last_used_at = nil,
|
||
last_ip = nil,
|
||
failed_attempts = 0
|
||
},
|
||
access_log = {}
|
||
}
|
||
|
||
-- Create tokens directory if needed
|
||
os.execute("mkdir -p /data/tokens")
|
||
|
||
-- Save token file
|
||
local filename = "tokens/" .. token .. ".json"
|
||
local filepath = "/data/" .. filename
|
||
local file = io.open(filepath, "w")
|
||
if not file then
|
||
ngx.status = 500
|
||
ngx.say(cjson.encode({error = "Failed to save token"}))
|
||
return
|
||
end
|
||
file:write(cjson.encode(token_data))
|
||
file:close()
|
||
|
||
-- Update index
|
||
local index_file = io.open("/data/tokens.json", "r")
|
||
local index = {tokens = {}}
|
||
if index_file then
|
||
local content = index_file:read("*all")
|
||
index_file:close()
|
||
local ok, parsed = pcall(cjson.decode, content)
|
||
if ok then index = parsed end
|
||
end
|
||
|
||
if not index.tokens then index.tokens = {} end
|
||
index.tokens[token] = {
|
||
name = data.name,
|
||
permission = data.permission,
|
||
status = "active",
|
||
created_at = timestamp,
|
||
expires_at = expires_at,
|
||
file = filename
|
||
}
|
||
|
||
local index_file = io.open("/data/tokens.json", "w")
|
||
if index_file then
|
||
index_file:write(cjson.encode(index))
|
||
index_file:close()
|
||
end
|
||
|
||
ngx.status = 200
|
||
ngx.say(cjson.encode({
|
||
success = true,
|
||
token = token,
|
||
name = data.name,
|
||
permission = data.permission,
|
||
created_at = timestamp,
|
||
expires_at = token_data.expires_at
|
||
}))
|
||
return
|
||
end
|
||
|
||
if method == "GET" then
|
||
-- List tokens
|
||
local index_file = io.open("/data/tokens.json", "r")
|
||
if not index_file then
|
||
ngx.status = 200
|
||
ngx.say(cjson.encode({tokens = {}}))
|
||
return
|
||
end
|
||
|
||
local content = index_file:read("*all")
|
||
index_file:close()
|
||
|
||
local ok, index = pcall(cjson.decode, content)
|
||
if not ok then
|
||
ngx.status = 500
|
||
ngx.say(cjson.encode({error = "Invalid token index"}))
|
||
return
|
||
end
|
||
|
||
local tokens_list = {}
|
||
for token, info in pairs(index.tokens or {}) do
|
||
local token_file = io.open("/data/" .. info.file, "r")
|
||
if token_file then
|
||
local token_content = token_file:read("*all")
|
||
token_file:close()
|
||
|
||
local ok, token_data = pcall(cjson.decode, token_content)
|
||
if ok then
|
||
table.insert(tokens_list, {
|
||
token = token:sub(1, 12) .. "...",
|
||
token_full = token,
|
||
name = info.name,
|
||
permission = info.permission,
|
||
status = token_data.status,
|
||
created_at = info.created_at,
|
||
expires_at = info.expires_at,
|
||
stats = token_data.stats
|
||
})
|
||
end
|
||
end
|
||
end
|
||
|
||
ngx.status = 200
|
||
ngx.say(cjson.encode({tokens = tokens_list}))
|
||
return
|
||
end
|
||
|
||
ngx.status = 405
|
||
ngx.say(cjson.encode({error = "Method not allowed"}))
|
||
}
|
||
}
|
||
|
||
# Token detail operations
|
||
location ~ ^/api/tokens/([^/]+)$ {
|
||
content_by_lua_block {
|
||
local cjson = require "cjson"
|
||
local token = ngx.var[1]
|
||
local method = ngx.req.get_method()
|
||
|
||
-- Verify admin password
|
||
local password = ngx.var.http_x_password
|
||
if password ~= "admin123123123" then
|
||
ngx.status = 401
|
||
ngx.say(cjson.encode({error = "Invalid password"}))
|
||
return
|
||
end
|
||
|
||
-- Load token index
|
||
local index_file = io.open("/data/tokens.json", "r")
|
||
if not index_file then
|
||
ngx.status = 404
|
||
ngx.say(cjson.encode({error = "Token not found"}))
|
||
return
|
||
end
|
||
|
||
local index_content = index_file:read("*all")
|
||
index_file:close()
|
||
|
||
local ok, index = pcall(cjson.decode, index_content)
|
||
if not ok or not index.tokens or not index.tokens[token] then
|
||
ngx.status = 404
|
||
ngx.say(cjson.encode({error = "Token not found"}))
|
||
return
|
||
end
|
||
|
||
local token_filepath = "/data/" .. index.tokens[token].file
|
||
|
||
if method == "GET" then
|
||
-- Get token details
|
||
local token_file = io.open(token_filepath, "r")
|
||
if not token_file then
|
||
ngx.status = 404
|
||
ngx.say(cjson.encode({error = "Token file not found"}))
|
||
return
|
||
end
|
||
|
||
local content = token_file:read("*all")
|
||
token_file:close()
|
||
|
||
ngx.status = 200
|
||
ngx.say(content)
|
||
return
|
||
end
|
||
|
||
if method == "PATCH" then
|
||
-- Update token
|
||
ngx.req.read_body()
|
||
local body = ngx.req.get_body_data()
|
||
local ok, update_data = pcall(cjson.decode, body)
|
||
|
||
if not ok then
|
||
ngx.status = 400
|
||
ngx.say(cjson.encode({error = "Invalid JSON"}))
|
||
return
|
||
end
|
||
|
||
-- Load current token data
|
||
local token_file = io.open(token_filepath, "r")
|
||
if not token_file then
|
||
ngx.status = 404
|
||
ngx.say(cjson.encode({error = "Token file not found"}))
|
||
return
|
||
end
|
||
|
||
local content = token_file:read("*all")
|
||
token_file:close()
|
||
|
||
local ok, token_data = pcall(cjson.decode, content)
|
||
if not ok then
|
||
ngx.status = 500
|
||
ngx.say(cjson.encode({error = "Invalid token data"}))
|
||
return
|
||
end
|
||
|
||
-- Update allowed fields
|
||
if update_data.name then
|
||
token_data.name = update_data.name
|
||
index.tokens[token].name = update_data.name
|
||
end
|
||
|
||
if update_data.status then
|
||
if update_data.status == "active" or update_data.status == "disabled" then
|
||
token_data.status = update_data.status
|
||
index.tokens[token].status = update_data.status
|
||
|
||
-- Reset failed attempts when re-enabling
|
||
if update_data.status == "active" and token_data.stats then
|
||
token_data.stats.failed_attempts = 0
|
||
end
|
||
end
|
||
end
|
||
|
||
-- Save updated token
|
||
local file = io.open(token_filepath, "w")
|
||
if file then
|
||
file:write(cjson.encode(token_data))
|
||
file:close()
|
||
end
|
||
|
||
-- Save updated index
|
||
local index_file = io.open("/data/tokens.json", "w")
|
||
if index_file then
|
||
index_file:write(cjson.encode(index))
|
||
index_file:close()
|
||
end
|
||
|
||
ngx.status = 200
|
||
ngx.say(cjson.encode({
|
||
success = true,
|
||
token = token,
|
||
status = token_data.status,
|
||
name = token_data.name
|
||
}))
|
||
return
|
||
end
|
||
|
||
if method == "DELETE" then
|
||
-- Delete token (requires password confirmation in body)
|
||
ngx.req.read_body()
|
||
local body = ngx.req.get_body_data()
|
||
local ok, delete_data = pcall(cjson.decode, body)
|
||
|
||
if not ok or not delete_data.confirm_password or delete_data.confirm_password ~= "admin123123123" then
|
||
ngx.status = 400
|
||
ngx.say(cjson.encode({error = "Confirmation password required"}))
|
||
return
|
||
end
|
||
|
||
-- Delete token file
|
||
os.remove(token_filepath)
|
||
|
||
-- Update index
|
||
index.tokens[token] = nil
|
||
local index_file = io.open("/data/tokens.json", "w")
|
||
if index_file then
|
||
index_file:write(cjson.encode(index))
|
||
index_file:close()
|
||
end
|
||
|
||
ngx.status = 200
|
||
ngx.say(cjson.encode({
|
||
success = true,
|
||
message = "Token deleted",
|
||
token = token
|
||
}))
|
||
return
|
||
end
|
||
|
||
ngx.status = 405
|
||
ngx.say(cjson.encode({error = "Method not allowed"}))
|
||
}
|
||
}
|
||
}
|
||
}
|