mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-06-17 20:24:21 +00:00
Compare commits
2 Commits
issue-1339
...
web-dashbo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8372202d8f | ||
|
|
f1847d1f20 |
33
.github/workflows/build-and-push-docker.yml
vendored
33
.github/workflows/build-and-push-docker.yml
vendored
@@ -1,14 +1,14 @@
|
||||
name: Docker
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- "main"
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- "main"
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
@@ -20,21 +20,15 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
attestations: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||
|
||||
- name: Log in to the Container registry
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
@@ -42,32 +36,21 @@ jobs:
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
# generate Docker tags based on the following events/attributes
|
||||
tags: |
|
||||
type=ref,event=pr
|
||||
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
|
||||
type=raw,value=canary,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
|
||||
- name: Build and push Docker image
|
||||
id: push
|
||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Sign Docker image
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
|
||||
with:
|
||||
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
subject-digest: ${{ steps.push.outputs.digest }}
|
||||
push-to-registry: true
|
||||
|
||||
82
.github/workflows/build-and-test.yml
vendored
82
.github/workflows/build-and-test.yml
vendored
@@ -3,8 +3,6 @@ name: CI
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
@@ -12,51 +10,38 @@ jobs:
|
||||
build-and-test:
|
||||
name: Build & Test (${{ matrix.os }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
continue-on-error: ${{ github.event_name == 'pull_request' }}
|
||||
continue-on-error: true
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: windows-2025
|
||||
artifact: windows
|
||||
debug_preset: windows-msvc-relwithdebinfo
|
||||
- os: ubuntu-24.04
|
||||
artifact: linux
|
||||
debug_preset: linux-gnu-relwithdebinfo
|
||||
- os: macos-15-intel
|
||||
artifact: macos
|
||||
debug_preset: macos-relwithdebinfo
|
||||
os: [ windows-2022, ubuntu-22.04, macos-13 ]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2
|
||||
with:
|
||||
submodules: true
|
||||
- name: Add msbuild to PATH (Windows only)
|
||||
if: ${{ matrix.os == 'windows-2025' }}
|
||||
uses: microsoft/setup-msbuild@30375c66a4eea26614e0d39710365f22f8b0af57 # v3
|
||||
if: ${{ matrix.os == 'windows-2022' }}
|
||||
uses: microsoft/setup-msbuild@767f00a3f09872d96a0cb9fcd5e6a4ff33311330
|
||||
with:
|
||||
vs-version: '[17,18)'
|
||||
msbuild-architecture: x64
|
||||
- name: Get CMake 3.x
|
||||
uses: lukka/get-cmake@591817e96fcad43505fb4eae36172462abb3a42e # v4.3.3
|
||||
with:
|
||||
cmakeVersion: "~3.25.0"
|
||||
- name: cmake
|
||||
uses: lukka/run-cmake@5d55ea7949e25f69f0ecb516d8d572297e03a956 # v10.9
|
||||
with:
|
||||
workflowPreset: "${{ matrix.debug_preset }}"
|
||||
|
||||
- name: Extract Linux debug symbols
|
||||
if: matrix.os == 'ubuntu-24.04'
|
||||
- name: Install libssl and switch to XCode 15.2 (Mac Only)
|
||||
if: ${{ matrix.os == 'macos-13' }}
|
||||
run: |
|
||||
find build -type f -name '*Server' | while read bin; do
|
||||
objcopy --only-keep-debug "$bin" "${bin}.debug"
|
||||
objcopy --strip-debug --add-gnu-debuglink="${bin}.debug" "$bin"
|
||||
done
|
||||
|
||||
- name: artifacts
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
brew install openssl@3
|
||||
sudo xcode-select -s /Applications/Xcode_15.2.app/Contents/Developer
|
||||
- name: Get CMake 3.x
|
||||
uses: lukka/get-cmake@28983e0d3955dba2bb0a6810caae0c6cf268ec0c
|
||||
with:
|
||||
name: build-${{matrix.artifact}}
|
||||
cmakeVersion: "~3.25.0" # <--= optional, use most recent 3.25.x version
|
||||
- name: cmake
|
||||
uses: lukka/run-cmake@67c73a83a46f86c4e0b96b741ac37ff495478c38
|
||||
with:
|
||||
workflowPreset: "ci-${{matrix.os}}"
|
||||
- name: artifacts
|
||||
uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47
|
||||
with:
|
||||
name: build-${{matrix.os}}
|
||||
path: |
|
||||
build/*/*Server*
|
||||
build/*/*.ini
|
||||
@@ -67,30 +52,5 @@ jobs:
|
||||
build/*/navmeshes/
|
||||
build/*/migrations/
|
||||
build/*/*.dcf
|
||||
!build/*/*.pdb
|
||||
!build/*/d*/
|
||||
!build/*/*.dSYM/
|
||||
!build/**/*.debug
|
||||
|
||||
- name: debug symbols (Windows)
|
||||
if: matrix.os == 'windows-2025'
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: debug-${{matrix.artifact}}
|
||||
path: |
|
||||
build/*/*.pdb
|
||||
build/*/d*/
|
||||
retention-days: 30
|
||||
- name: debug symbols (Linux)
|
||||
if: matrix.os == 'ubuntu-24.04'
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: debug-${{matrix.artifact}}
|
||||
path: build/**/*.debug
|
||||
retention-days: 30
|
||||
- name: debug symbols (macOS)
|
||||
if: matrix.os == 'macos-15-intel'
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: debug-${{matrix.artifact}}
|
||||
path: build/**/*.dSYM/
|
||||
retention-days: 30
|
||||
|
||||
93
.github/workflows/canary.yml
vendored
93
.github/workflows/canary.yml
vendored
@@ -1,93 +0,0 @@
|
||||
name: Canary
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["CI"]
|
||||
branches: [main]
|
||||
types: [completed]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
canary-release:
|
||||
name: Publish Canary Release
|
||||
if: github.event.workflow_run.conclusion == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.event.workflow_run.head_sha }}
|
||||
|
||||
- name: Get last release tag
|
||||
id: last_tag
|
||||
run: |
|
||||
tag=$(git describe --tags --abbrev=0 --match "v*.*.*" 2>/dev/null || echo "none")
|
||||
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Generate changelog since last release tag
|
||||
uses: orhun/git-cliff-action@f50e11560dce63f7c33227798f90b924471a88b5 # v4.8.0
|
||||
id: cliff
|
||||
with:
|
||||
config: cliff.toml
|
||||
args: --unreleased --strip header
|
||||
env:
|
||||
OUTPUT: CHANGES.md
|
||||
GITHUB_REPO: ${{ github.repository }}
|
||||
|
||||
- name: Prepend header to changelog
|
||||
run: |
|
||||
last="${{ steps.last_tag.outputs.tag }}"
|
||||
sha="${{ github.event.workflow_run.head_sha }}"
|
||||
short="${sha:0:7}"
|
||||
if [ "$last" != "none" ]; then
|
||||
header="Changes since **$last** ([full diff](https://github.com/${{ github.repository }}/compare/${last}...${sha}))\n\n"
|
||||
else
|
||||
header="Changes up to \`${short}\`\n\n"
|
||||
fi
|
||||
printf "%b" "$header" | cat - CHANGES.md > CHANGES.tmp && mv CHANGES.tmp CHANGES.md
|
||||
|
||||
- name: Download artifacts from CI run
|
||||
uses: dawidd6/action-download-artifact@b6e2e70617bc3265edd6dab6c906732b2f1ae151 # v21
|
||||
with:
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
path: artifacts/
|
||||
|
||||
- name: Package artifacts
|
||||
run: |
|
||||
declare -A platform_map=(
|
||||
["build-windows"]="darkflame-universe-windows"
|
||||
["build-linux"]="darkflame-universe-linux"
|
||||
["build-macos"]="darkflame-universe-macos"
|
||||
)
|
||||
cd artifacts
|
||||
for dir in build-*/; do
|
||||
name="${dir%/}"
|
||||
out="${platform_map[$name]:-$name}"
|
||||
zip -r "../${out}.zip" "$dir"
|
||||
done
|
||||
cd ..
|
||||
ls -lh *.zip
|
||||
|
||||
- name: Delete existing canary release
|
||||
run: gh release delete canary --yes --cleanup-tag 2>/dev/null || true
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create canary pre-release
|
||||
uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1.21.0
|
||||
with:
|
||||
tag: canary
|
||||
name: "Canary ${{ steps.last_tag.outputs.tag }}+${{ github.event.workflow_run.head_sha }}"
|
||||
bodyFile: CHANGES.md
|
||||
artifacts: "*.zip"
|
||||
artifactContentType: application/zip
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
prerelease: true
|
||||
draft: false
|
||||
allowUpdates: true
|
||||
removeArtifacts: true
|
||||
commit: ${{ github.event.workflow_run.head_sha }}
|
||||
38
.github/workflows/pr-title-check.yml
vendored
38
.github/workflows/pr-title-check.yml
vendored
@@ -1,38 +0,0 @@
|
||||
name: PR Title Check
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, edited, synchronize, reopened]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
statuses: write
|
||||
|
||||
jobs:
|
||||
check-title:
|
||||
name: Conventional Commit Title
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check PR title follows Conventional Commits
|
||||
uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
types: |
|
||||
feat
|
||||
fix
|
||||
perf
|
||||
refactor
|
||||
docs
|
||||
chore
|
||||
test
|
||||
ci
|
||||
revert
|
||||
requireScope: false
|
||||
subjectPattern: ^.+$
|
||||
subjectPatternError: |
|
||||
The PR title "{title}" must have a description after the type/scope prefix.
|
||||
Example: "feat: add new login flow" or "fix(auth): handle null token"
|
||||
wip: true
|
||||
validateSingleCommit: false
|
||||
70
.github/workflows/release.yml
vendored
70
.github/workflows/release.yml
vendored
@@ -1,70 +0,0 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["CI"]
|
||||
types: [completed]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Create Release
|
||||
# Only run when CI completed successfully on a tag push (not a PR branch named like a version)
|
||||
if: |
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.event == 'push' &&
|
||||
startsWith(github.event.workflow_run.head_branch, 'v')
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.event.workflow_run.head_sha }}
|
||||
|
||||
- name: Generate changelog
|
||||
uses: orhun/git-cliff-action@f50e11560dce63f7c33227798f90b924471a88b5 # v4.8.0
|
||||
id: cliff
|
||||
with:
|
||||
config: cliff.toml
|
||||
args: --latest --strip header
|
||||
env:
|
||||
OUTPUT: CHANGES.md
|
||||
GITHUB_REPO: ${{ github.repository }}
|
||||
|
||||
- name: Download artifacts from CI run
|
||||
uses: dawidd6/action-download-artifact@b6e2e70617bc3265edd6dab6c906732b2f1ae151 # v21
|
||||
with:
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
path: artifacts/
|
||||
|
||||
- name: Package artifacts
|
||||
run: |
|
||||
declare -A platform_map=(
|
||||
["build-windows"]="darkflame-universe-windows"
|
||||
["build-linux"]="darkflame-universe-linux"
|
||||
["build-macos"]="darkflame-universe-macos"
|
||||
)
|
||||
cd artifacts
|
||||
for dir in build-*/; do
|
||||
name="${dir%/}"
|
||||
out="${platform_map[$name]:-$name}"
|
||||
zip -r "../${out}.zip" "$dir"
|
||||
done
|
||||
cd ..
|
||||
ls -lh *.zip
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1.21.0
|
||||
with:
|
||||
tag: ${{ github.event.workflow_run.head_branch }}
|
||||
name: ${{ github.event.workflow_run.head_branch }}
|
||||
bodyFile: CHANGES.md
|
||||
artifacts: "*.zip"
|
||||
artifactContentType: application/zip
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
prerelease: false
|
||||
draft: false
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -126,5 +126,3 @@ docker-compose.override.yml
|
||||
# CMake scripts
|
||||
!cmake/*
|
||||
!cmake/toolchains/*
|
||||
.mcp.json
|
||||
.claude/
|
||||
|
||||
@@ -15,11 +15,6 @@ set(CMAKE_C_STANDARD 99)
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
if(CMAKE_VERSION VERSION_GREATER_EQUAL "4.0")
|
||||
set(CMAKE_POLICY_VERSION_MINIMUM 3.5)
|
||||
endif()
|
||||
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # Export the compile commands for debugging
|
||||
set(CMAKE_POLICY_DEFAULT_CMP0063 NEW) # Set CMAKE visibility policy to NEW on project and subprojects
|
||||
set(CMAKE_VISIBILITY_INLINES_HIDDEN ON) # Set C and C++ symbol visibility to hide inlined functions
|
||||
@@ -72,11 +67,7 @@ set(RECASTNAVIGATION_EXAMPLES OFF CACHE BOOL "" FORCE)
|
||||
# Disabled no-register
|
||||
# Disabled unknown pragmas because Linux doesn't understand Windows pragmas.
|
||||
if(UNIX)
|
||||
if(APPLE)
|
||||
add_link_options("-Wl,-rpath,@loader_path/")
|
||||
else()
|
||||
add_link_options("-Wl,-rpath,$ORIGIN/")
|
||||
endif()
|
||||
add_link_options("-Wl,-rpath,$ORIGIN/")
|
||||
add_compile_options("-fPIC")
|
||||
add_compile_definitions(_GLIBCXX_USE_CXX11_ABI=0 _GLIBCXX_USE_CXX17_ABI=0)
|
||||
|
||||
@@ -119,6 +110,8 @@ set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
|
||||
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
|
||||
|
||||
find_package(MariaDB)
|
||||
find_package(OpenSSL REQUIRED)
|
||||
|
||||
|
||||
# Create a /resServer directory
|
||||
make_directory(${CMAKE_BINARY_DIR}/resServer)
|
||||
@@ -135,7 +128,7 @@ endif()
|
||||
message(STATUS "Variable: DLU_CONFIG_DIR = ${DLU_CONFIG_DIR}")
|
||||
|
||||
# Copy resource files on first build
|
||||
set(RESOURCE_FILES "sharedconfig.ini" "authconfig.ini" "chatconfig.ini" "worldconfig.ini" "masterconfig.ini" "blocklist.dcf")
|
||||
set(RESOURCE_FILES "sharedconfig.ini" "authconfig.ini" "chatconfig.ini" "worldconfig.ini" "masterconfig.ini" "dashboardconfig.ini" "blocklist.dcf")
|
||||
message(STATUS "Checking resource file integrity")
|
||||
|
||||
include(Utils)
|
||||
@@ -331,6 +324,7 @@ endif()
|
||||
add_subdirectory(dWorldServer)
|
||||
add_subdirectory(dAuthServer)
|
||||
add_subdirectory(dChatServer)
|
||||
add_subdirectory(dDashboardServer)
|
||||
add_subdirectory(dMasterServer) # Add MasterServer last so it can rely on the other binaries
|
||||
|
||||
target_precompile_headers(
|
||||
|
||||
@@ -162,13 +162,6 @@
|
||||
"rhs": "Darwin"
|
||||
},
|
||||
"binaryDir": "${sourceDir}/build/macos"
|
||||
},
|
||||
{
|
||||
"name": "macos-relwithdebinfo",
|
||||
"inherits": ["macos", "relwithdebinfo-config"],
|
||||
"displayName": "[RelWithDebInfo] MacOS",
|
||||
"description": "Create a RelWithDebInfo build for MacOS",
|
||||
"binaryDir": "${sourceDir}/build/macos-relwithdebinfo"
|
||||
}
|
||||
],
|
||||
"buildPresets": [
|
||||
@@ -262,7 +255,7 @@
|
||||
{
|
||||
"name": "macos-relwithdebinfo",
|
||||
"inherits": "default",
|
||||
"configurePreset": "macos-relwithdebinfo",
|
||||
"configurePreset": "macos",
|
||||
"displayName": "[RelWithDebInfo] MacOS",
|
||||
"description": "This preset is used to build in release mode with debug info on MacOS",
|
||||
"configuration": "RelWithDebInfo"
|
||||
@@ -381,7 +374,7 @@
|
||||
{
|
||||
"name": "macos-relwithdebinfo",
|
||||
"inherits": "default",
|
||||
"configurePreset": "macos-relwithdebinfo",
|
||||
"configurePreset": "macos",
|
||||
"displayName": "[RelWithDebInfo] MacOS",
|
||||
"description": "Runs all tests on a MacOS configuration",
|
||||
"configuration": "RelWithDebInfo"
|
||||
@@ -610,7 +603,7 @@
|
||||
"steps": [
|
||||
{
|
||||
"type": "configure",
|
||||
"name": "macos-relwithdebinfo"
|
||||
"name": "macos"
|
||||
},
|
||||
{
|
||||
"type": "build",
|
||||
@@ -623,7 +616,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ci-macos-15-intel",
|
||||
"name": "ci-macos-13",
|
||||
"displayName": "[Release] MacOS",
|
||||
"description": "CI workflow preset for MacOS",
|
||||
"steps": [
|
||||
|
||||
@@ -16,9 +16,7 @@ RUN --mount=type=cache,target=/app/build,id=build-cache \
|
||||
cd /app/build && \
|
||||
cmake .. && \
|
||||
make -j$(nproc --ignore 1) && \
|
||||
cp -r /app/build/* /tmp/persisted-build/ && \
|
||||
mkdir -p /tmp/persisted-build/mariadbcpp && \
|
||||
cp /app/build/thirdparty/mariadb-connector-cpp/src/mariadb_connector_cpp-build/libmariadbcpp.so /tmp/persisted-build/mariadbcpp/
|
||||
cp -r /app/build/* /tmp/persisted-build/
|
||||
|
||||
FROM debian:12 as runtime
|
||||
|
||||
|
||||
35
cliff.toml
35
cliff.toml
@@ -1,35 +0,0 @@
|
||||
[changelog]
|
||||
header = ""
|
||||
body = """
|
||||
{% for group, commits in commits | group_by(attribute="group") %}
|
||||
### {{ group | upper_first }}
|
||||
{% for commit in commits %}
|
||||
- {% if commit.scope %}**{{ commit.scope }}**: {% endif %}{{ commit.message }} ([{{ commit.id | truncate(length=7, end="") }}](https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}/commit/{{ commit.id }}))\
|
||||
{% if commit.github.username %} by [@{{ commit.github.username }}](https://github.com/{{ commit.github.username }}){% endif %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
"""
|
||||
footer = ""
|
||||
trim = true
|
||||
|
||||
[git]
|
||||
conventional_commits = true
|
||||
filter_unconventional = true
|
||||
split_commits = false
|
||||
commit_parsers = [
|
||||
{ message = "^feat", group = "Features" },
|
||||
{ message = "^fix", group = "Bug Fixes" },
|
||||
{ message = "^perf", group = "Performance" },
|
||||
{ message = "^refactor", group = "Refactoring" },
|
||||
{ message = "^docs", group = "Documentation" },
|
||||
{ message = "^chore", group = "Chores" },
|
||||
{ message = "^test", group = "Testing" },
|
||||
{ message = "^ci", group = "CI/CD" },
|
||||
{ message = "^revert", group = "Reverts" },
|
||||
]
|
||||
filter_commits = false
|
||||
tag_pattern = "v[0-9].*"
|
||||
skip_tags = "canary"
|
||||
ignore_tags = ""
|
||||
topo_order = false
|
||||
sort_commits = "oldest"
|
||||
@@ -10,15 +10,14 @@ if(WIN32 AND NOT MARIADB_BUILD_SOURCE)
|
||||
file(MAKE_DIRECTORY "${MARIADB_MSI_DIR}")
|
||||
file(MAKE_DIRECTORY "${MARIADB_CONNECTOR_DIR}")
|
||||
|
||||
# These values track the published Windows MSI packages used by the prebuilt path.
|
||||
# Keep the Connector/C++ package version aligned with the checked out submodule tag when possible.
|
||||
# Go to https://mariadb.com/downloads/connectors/ to find the up-to-date URL parts.
|
||||
set(MARIADB_CONNECTOR_C_VERSION "3.4.8")
|
||||
set(MARIADB_CONNECTOR_C_BUCKET "4516894")
|
||||
set(MARIADB_CONNECTOR_C_MD5 "50f6fc0c77b8d3bacbeac0126e179861")
|
||||
set(MARIADB_CONNECTOR_CPP_VERSION "1.1.7")
|
||||
set(MARIADB_CONNECTOR_CPP_BUCKET "4464908")
|
||||
set(MARIADB_CONNECTOR_CPP_MD5 "08644a7ff084b5933325cadb904796e5")
|
||||
# These values need to be updated whenever a new minor release replaces an old one
|
||||
# Go to https://mariadb.com/downloads/connectors/ to find the up-to-date URL parts
|
||||
set(MARIADB_CONNECTOR_C_VERSION "3.2.7")
|
||||
set(MARIADB_CONNECTOR_C_BUCKET "2319651")
|
||||
set(MARIADB_CONNECTOR_C_MD5 "f8636d733f1d093af9d4f22f3239f885")
|
||||
set(MARIADB_CONNECTOR_CPP_VERSION "1.0.2")
|
||||
set(MARIADB_CONNECTOR_CPP_BUCKET "2531525")
|
||||
set(MARIADB_CONNECTOR_CPP_MD5 "3034bbd6ca00a0125345f9fd1a178401")
|
||||
|
||||
set(MARIADB_CONNECTOR_C_MSI "mariadb-connector-c-${MARIADB_CONNECTOR_C_VERSION}-win64.msi")
|
||||
set(MARIADB_CONNECTOR_CPP_MSI "mariadb-connector-cpp-${MARIADB_CONNECTOR_CPP_VERSION}-win64.msi")
|
||||
@@ -80,39 +79,23 @@ else() # Build from source
|
||||
-DWITH_EXTERNAL_ZLIB=ON
|
||||
-DOPENSSL_ROOT_DIR=${OPENSSL_ROOT_DIR}
|
||||
-DCMAKE_C_FLAGS=-w # disable zlib warnings
|
||||
-DCMAKE_CXX_FLAGS=-D_GLIBCXX_USE_CXX11_ABI=0\ -Wno-inconsistent-missing-override\ -include\ cstdint)
|
||||
-DCMAKE_CXX_FLAGS=-D_GLIBCXX_USE_CXX11_ABI=0)
|
||||
else()
|
||||
set(MARIADB_EXTRA_CMAKE_ARGS
|
||||
-DCMAKE_C_FLAGS=-w # disable zlib warnings
|
||||
-DCMAKE_CXX_FLAGS=-D_GLIBCXX_USE_CXX11_ABI=0\ -include\ cstdint)
|
||||
-DCMAKE_CXX_FLAGS=-D_GLIBCXX_USE_CXX11_ABI=0)
|
||||
endif()
|
||||
|
||||
set(MARIADBCPP_BUILD_DIR "${PROJECT_BINARY_DIR}/thirdparty/mariadb-connector-cpp/src/mariadb_connector_cpp-build")
|
||||
set(MARIADBCPP_INSTALL_DIR ${PROJECT_BINARY_DIR}/prefix)
|
||||
set(MARIADBCPP_LIBRARY_DIR ${PROJECT_BINARY_DIR}/mariadbcpp)
|
||||
set(MARIADBCPP_PLUGIN_DIR ${MARIADBCPP_LIBRARY_DIR}/plugin)
|
||||
set(MARIADBCPP_SOURCE_DIR ${PROJECT_SOURCE_DIR}/thirdparty/mariadb-connector-cpp)
|
||||
set(MARIADB_INCLUDE_DIR "${MARIADBCPP_SOURCE_DIR}/include")
|
||||
|
||||
if(WIN32)
|
||||
set(MARIADBCPP_LIBRARY_DIR ${PROJECT_BINARY_DIR}/mariadbcpp)
|
||||
set(MARIADBCPP_PLUGIN_DIR ${MARIADBCPP_LIBRARY_DIR}/plugin)
|
||||
set(MARIADB_INSTALL_COMMAND)
|
||||
else()
|
||||
set(MARIADBCPP_LIBRARY_DIR ${MARIADBCPP_BUILD_DIR})
|
||||
set(MARIADBCPP_PLUGIN_DIR ${MARIADBCPP_BUILD_DIR}/libmariadb)
|
||||
set(MARIADB_INSTALL_COMMAND INSTALL_COMMAND ${CMAKE_COMMAND} -E true)
|
||||
endif()
|
||||
|
||||
if(CMAKE_VERSION VERSION_GREATER_EQUAL "4.0")
|
||||
set(MARIADB_POLICY_VERSION_ARG -DCMAKE_POLICY_VERSION_MINIMUM=3.5)
|
||||
endif()
|
||||
|
||||
ExternalProject_Add(mariadb_connector_cpp
|
||||
PREFIX "${PROJECT_BINARY_DIR}/thirdparty/mariadb-connector-cpp"
|
||||
SOURCE_DIR ${MARIADBCPP_SOURCE_DIR}
|
||||
BINARY_DIR ${MARIADBCPP_BUILD_DIR}
|
||||
INSTALL_DIR ${MARIADBCPP_INSTALL_DIR}
|
||||
CMAKE_ARGS -Wno-dev
|
||||
${MARIADB_POLICY_VERSION_ARG}
|
||||
-DWITH_UNIT_TESTS=OFF
|
||||
-DMARIADB_LINK_DYNAMIC=OFF
|
||||
-DCMAKE_BUILD_RPATH_USE_ORIGIN=${CMAKE_BUILD_RPATH_USE_ORIGIN}
|
||||
@@ -120,7 +103,6 @@ else() # Build from source
|
||||
-DINSTALL_LIBDIR=${MARIADBCPP_LIBRARY_DIR}
|
||||
-DINSTALL_PLUGINDIR=${MARIADBCPP_PLUGIN_DIR}
|
||||
${MARIADB_EXTRA_CMAKE_ARGS}
|
||||
${MARIADB_INSTALL_COMMAND}
|
||||
BUILD_ALWAYS true
|
||||
)
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include "MessageType/Chat.h"
|
||||
#include "BitStreamUtils.h"
|
||||
#include "Game.h"
|
||||
#include "dConfig.h"
|
||||
#include "Logger.h"
|
||||
#include "eObjectBits.h"
|
||||
|
||||
@@ -73,8 +72,8 @@ void ChatIgnoreList::AddIgnore(Packet* packet) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int32_t MAX_IGNORES = Game::config->GetValue("max_ignores", 32);
|
||||
if (receiver.ignoredPlayers.size() >= MAX_IGNORES) {
|
||||
constexpr int32_t MAX_IGNORES = 32;
|
||||
if (receiver.ignoredPlayers.size() > MAX_IGNORES) {
|
||||
LOG_DEBUG("Player %llu has too many ignores", playerId);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -435,11 +435,6 @@ void ChatPacketHandler::HandleChatMessage(Packet* packet) {
|
||||
inStream.IgnoreBytes(4);
|
||||
inStream.Read(channel);
|
||||
inStream.Read(size);
|
||||
if (size > MAX_MESSAGE_LENGTH) {
|
||||
LOG("Received a probably spoofed chat message, ignoring msg");
|
||||
return;
|
||||
}
|
||||
|
||||
inStream.IgnoreBytes(77);
|
||||
|
||||
LUWString message(size);
|
||||
@@ -484,11 +479,6 @@ void ChatPacketHandler::HandlePrivateChatMessage(Packet* packet) {
|
||||
if (channel != eChatChannel::PRIVATE_CHAT) LOG("WARNING: Received Private chat with the wrong channel!");
|
||||
|
||||
inStream.Read(size);
|
||||
if (size > MAX_MESSAGE_LENGTH) {
|
||||
LOG("Received a probably spoofed chat message, ignoring msg");
|
||||
return;
|
||||
}
|
||||
|
||||
inStream.IgnoreBytes(77);
|
||||
|
||||
inStream.Read(LUReceiverName);
|
||||
|
||||
@@ -19,23 +19,24 @@
|
||||
#include "eGameMasterLevel.h"
|
||||
#include "dChatFilter.h"
|
||||
#include "TeamContainer.h"
|
||||
#include "HTTPContext.h"
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
void HandleHTTPPlayersRequest(HTTPReply& reply, std::string body) {
|
||||
void HandleHTTPPlayersRequest(HTTPReply& reply, const HTTPContext& context) {
|
||||
const json data = Game::playerContainer;
|
||||
reply.status = data.empty() ? eHTTPStatusCode::NO_CONTENT : eHTTPStatusCode::OK;
|
||||
reply.message = data.empty() ? "{\"error\":\"No Players Online\"}" : data.dump();
|
||||
}
|
||||
|
||||
void HandleHTTPTeamsRequest(HTTPReply& reply, std::string body) {
|
||||
void HandleHTTPTeamsRequest(HTTPReply& reply, const HTTPContext& context) {
|
||||
const json data = TeamContainer::GetTeamContainer();
|
||||
reply.status = data.empty() ? eHTTPStatusCode::NO_CONTENT : eHTTPStatusCode::OK;
|
||||
reply.message = data.empty() ? "{\"error\":\"No Teams Online\"}" : data.dump();
|
||||
}
|
||||
|
||||
void HandleHTTPAnnounceRequest(HTTPReply& reply, std::string body) {
|
||||
auto data = GeneralUtils::TryParse<json>(body);
|
||||
void HandleHTTPAnnounceRequest(HTTPReply& reply, const HTTPContext& context) {
|
||||
auto data = GeneralUtils::TryParse<json>(context.body);
|
||||
if (!data) {
|
||||
reply.status = eHTTPStatusCode::BAD_REQUEST;
|
||||
reply.message = "{\"error\":\"Invalid JSON\"}";
|
||||
@@ -96,18 +97,21 @@ namespace ChatWeb {
|
||||
Game::web.RegisterHTTPRoute({
|
||||
.path = v1_route + "players",
|
||||
.method = eHTTPMethod::GET,
|
||||
.middleware = {},
|
||||
.handle = HandleHTTPPlayersRequest
|
||||
});
|
||||
|
||||
Game::web.RegisterHTTPRoute({
|
||||
.path = v1_route + "teams",
|
||||
.method = eHTTPMethod::GET,
|
||||
.middleware = {},
|
||||
.handle = HandleHTTPTeamsRequest
|
||||
});
|
||||
|
||||
Game::web.RegisterHTTPRoute({
|
||||
.path = v1_route + "announce",
|
||||
.method = eHTTPMethod::POST,
|
||||
.middleware = {},
|
||||
.handle = HandleHTTPAnnounceRequest
|
||||
});
|
||||
|
||||
|
||||
@@ -176,7 +176,6 @@ LWOOBJID PlayerContainer::GetId(const std::u16string& playerName) {
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
// TODO Make this a pointer again or do something to make it so you cant edit the LWOOBJID_EMPTY entry? it should be ignored in all cases anyways though...
|
||||
PlayerData& PlayerContainer::GetPlayerDataMutable(const LWOOBJID& playerID) {
|
||||
return m_Players.contains(playerID) ? m_Players[playerID] : m_Players[LWOOBJID_EMPTY];
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include <stdexcept>
|
||||
|
||||
#include "Amf3.h"
|
||||
#include "StringifiedEnum.h"
|
||||
|
||||
/**
|
||||
* AMF3 Reference document https://rtmp.veriskope.com/pdf/amf3-file-format-spec.pdf
|
||||
@@ -54,7 +53,7 @@ std::unique_ptr<AMFBaseValue> AMFDeserialize::Read(RakNet::BitStream& inStream)
|
||||
case eAmf::VectorObject:
|
||||
[[fallthrough]];
|
||||
case eAmf::Dictionary:
|
||||
throw std::invalid_argument(StringifiedEnum::ToString(marker).data());
|
||||
throw marker;
|
||||
default:
|
||||
throw std::invalid_argument("Invalid AMF3 marker" + std::to_string(static_cast<int32_t>(marker)));
|
||||
}
|
||||
@@ -89,11 +88,6 @@ const std::string AMFDeserialize::ReadString(RakNet::BitStream& inStream) {
|
||||
// Right shift by 1 bit to get index if reference or size of next string if value
|
||||
length = length >> 1;
|
||||
if (isReference) {
|
||||
constexpr int32_t maxStringSize = 1024 * 1024;
|
||||
if (length > maxStringSize) {
|
||||
LOG("1MB string attempted to be allocated in AMF deserialize, possible spoof, aborting deserialize.");
|
||||
throw std::invalid_argument("1MB string attempted to be allocated in AMF deserialize, possible spoof, aborting deserialize.");
|
||||
}
|
||||
std::string value(length, 0);
|
||||
inStream.Read(&value[0], length);
|
||||
// Empty strings are never sent by reference
|
||||
@@ -123,12 +117,6 @@ std::unique_ptr<AMFArrayValue> AMFDeserialize::ReadAmfArray(RakNet::BitStream& i
|
||||
if (key.size() == 0) break;
|
||||
arrayValue->Insert(key, Read(inStream));
|
||||
}
|
||||
|
||||
constexpr int32_t maxArraySize = 10'000;
|
||||
if (sizeOfDenseArray > maxArraySize) {
|
||||
LOG("Someone sent 10,000 dense array entries, probably a bad packet.");
|
||||
throw std::invalid_argument("Someone sent 10,000 dense array entries, probably a bad packet.");
|
||||
}
|
||||
// Finally read dense portion
|
||||
for (uint32_t i = 0; i < sizeOfDenseArray; i++) {
|
||||
arrayValue->Insert(i, Read(inStream));
|
||||
|
||||
@@ -52,7 +52,8 @@ uint32_t BrickByBrickFix::TruncateBrokenBrickByBrickXml() {
|
||||
|
||||
if (actualUncompressedSize != -1) {
|
||||
uint32_t previousSize = completeUncompressedModel.size();
|
||||
completeUncompressedModel.append(reinterpret_cast<char*>(uncompressedChunk.get()), actualUncompressedSize);
|
||||
completeUncompressedModel.append(reinterpret_cast<char*>(uncompressedChunk.get()));
|
||||
completeUncompressedModel.resize(previousSize + actualUncompressedSize);
|
||||
} else {
|
||||
LOG("Failed to inflate chunk %i for model %llu. Error: %i", chunkCount, model.id, err);
|
||||
break;
|
||||
|
||||
@@ -308,9 +308,8 @@ std::vector<std::string> GeneralUtils::GetSqlFileNamesFromFolder(const std::stri
|
||||
for (const auto& t : std::filesystem::directory_iterator(folder)) {
|
||||
if (t.is_directory() || t.is_symlink()) continue;
|
||||
auto filename = t.path().filename().string();
|
||||
// Ensure the file has a name in the format of xxxxxxxx_anything_goes_here.sql
|
||||
const auto migrationNumber = TryParse<uint32_t>(GeneralUtils::SplitString(filename, '_').at(0));
|
||||
if (migrationNumber.has_value()) filenames.emplace(migrationNumber.value(), std::move(filename));
|
||||
const auto index = std::stoi(GeneralUtils::SplitString(filename, '_').at(0));
|
||||
filenames.emplace(index, std::move(filename));
|
||||
}
|
||||
|
||||
// Now sort the map by the oldest migration.
|
||||
|
||||
@@ -205,12 +205,6 @@ namespace GeneralUtils {
|
||||
return isParsed ? static_cast<T>(result) : std::optional<T>{};
|
||||
}
|
||||
|
||||
// A version of TryParse that will return `errorVal` if `str` failed to parse.
|
||||
template <Numeric T>
|
||||
[[nodiscard]] T TryParse(std::string_view str, const T errorVal) {
|
||||
return TryParse<T>(str).value_or(errorVal);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
requires(!Numeric<T>)
|
||||
[[nodiscard]] std::optional<T> TryParse(std::string_view str);
|
||||
@@ -243,12 +237,6 @@ namespace GeneralUtils {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// A version of TryParse that will return `errorVal` if `str` failed to parse.
|
||||
template <std::floating_point T>
|
||||
[[nodiscard]] T TryParse(std::string_view str, const T errorVal) {
|
||||
return TryParse<T>(str).value_or(errorVal);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
/**
|
||||
@@ -270,11 +258,6 @@ namespace GeneralUtils {
|
||||
return z ? std::make_optional<T>(x.value(), y.value(), z.value()) : std::nullopt;
|
||||
}
|
||||
|
||||
// Alternative overload of TryParse with a default value
|
||||
[[nodiscard]] inline NiPoint3 TryParse(const std::string_view strX, const std::string_view strY, const std::string_view strZ, const NiPoint3 errorVal) {
|
||||
return TryParse<NiPoint3>(strX, strY, strZ).value_or(errorVal);
|
||||
}
|
||||
|
||||
/**
|
||||
* The TryParse overload for handling NiPoint3 by passing a span of three strings
|
||||
* @param str The string vector representing the X, Y, and Z coordinates
|
||||
@@ -285,11 +268,6 @@ namespace GeneralUtils {
|
||||
return (str.size() == 3) ? TryParse<T>(str[0], str[1], str[2]) : std::nullopt;
|
||||
}
|
||||
|
||||
// Alternative overload of TryParse with a default value
|
||||
[[nodiscard]] inline NiPoint3 TryParse(const std::span<const std::string> str, const NiPoint3 errorVal) {
|
||||
return TryParse<NiPoint3>(str).value_or(errorVal);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
std::u16string to_u16string(const T value) {
|
||||
return GeneralUtils::ASCIIToUTF16(std::to_string(value));
|
||||
|
||||
@@ -53,21 +53,16 @@ Lxfml::Result Lxfml::NormalizePositionOnlyFirstPart(const std::string_view data)
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
auto x = GeneralUtils::TryParse<float>(split[9]).value();
|
||||
auto y = GeneralUtils::TryParse<float>(split[10]).value();
|
||||
auto z = GeneralUtils::TryParse<float>(split[11]).value();
|
||||
if (x < lowest.x) lowest.x = x;
|
||||
if (y < lowest.y) lowest.y = y;
|
||||
if (z < lowest.z) lowest.z = z;
|
||||
auto x = GeneralUtils::TryParse<float>(split[9]).value();
|
||||
auto y = GeneralUtils::TryParse<float>(split[10]).value();
|
||||
auto z = GeneralUtils::TryParse<float>(split[11]).value();
|
||||
if (x < lowest.x) lowest.x = x;
|
||||
if (y < lowest.y) lowest.y = y;
|
||||
if (z < lowest.z) lowest.z = z;
|
||||
|
||||
if (highest.x < x) highest.x = x;
|
||||
if (highest.y < y) highest.y = y;
|
||||
if (highest.z < z) highest.z = z;
|
||||
} catch (std::exception& e) {
|
||||
LOG("Failed to parse a split value of either (%s), (%s), or (%s).", split[9].c_str(), split[10].c_str(), split[11].c_str());
|
||||
return toReturn; // Early return since we failed to parse this lxfml.
|
||||
}
|
||||
if (highest.x < x) highest.x = x;
|
||||
if (highest.y < y) highest.y = y;
|
||||
if (highest.z < z) highest.z = z;
|
||||
}
|
||||
|
||||
auto delta = (highest - lowest) / 2.0f;
|
||||
|
||||
@@ -45,12 +45,6 @@ Sd0::Sd0(std::istream& buffer) {
|
||||
uint32_t bufferSize = buffer.tellg();
|
||||
buffer.seekg(0, std::ios::beg);
|
||||
WriteSize(firstChunk, bufferSize);
|
||||
// its expected that if we got here, we got an old sd0 buffer where we ignored the sd0 part
|
||||
// that means this can be at most the compressed chunk limit.
|
||||
if (bufferSize > MAX_UNCOMPRESSED_CHUNK_SIZE) {
|
||||
LOG("Possible bad chunk size of %i specified, rejecting.", bufferSize);
|
||||
return;
|
||||
}
|
||||
firstChunk.resize(firstChunk.size() + bufferSize);
|
||||
auto* dataStart = reinterpret_cast<char*>(firstChunk.data() + GetDataOffset(true));
|
||||
if (!buffer.read(dataStart, bufferSize)) {
|
||||
@@ -77,12 +71,6 @@ Sd0::Sd0(std::istream& buffer) {
|
||||
|
||||
WriteSize(chunk, chunkSize);
|
||||
|
||||
// Assuming a good buffer that is large enough to take up 2 zlib buffers
|
||||
// any buffer should be compressed enough to take up less size than its uncompressed counterpart
|
||||
if (chunkSize > MAX_UNCOMPRESSED_CHUNK_SIZE) {
|
||||
LOG("Possible bad chunk size of %i specified, rejecting.", chunkSize);
|
||||
break;
|
||||
}
|
||||
chunk.resize(chunkSize + dataOffset);
|
||||
auto* dataStart = reinterpret_cast<char*>(chunk.data() + dataOffset);
|
||||
if (!buffer.read(dataStart, chunkSize)) {
|
||||
@@ -107,11 +95,6 @@ void Sd0::FromData(const uint8_t* data, size_t bufferSize) {
|
||||
startOffset, numToCopy,
|
||||
compressedChunk.data(), compressedChunk.size());
|
||||
|
||||
if (compressedSize == -1) {
|
||||
LOG("Failed to compress chunk, aborting");
|
||||
break;
|
||||
}
|
||||
|
||||
auto& chunk = m_Chunks.emplace_back();
|
||||
bool firstBuffer = m_Chunks.size() == 1;
|
||||
auto dataOffset = GetDataOffset(firstBuffer);
|
||||
@@ -136,12 +119,6 @@ std::string Sd0::GetAsStringUncompressed() const {
|
||||
auto dataOffset = GetDataOffset(first);
|
||||
first = false;
|
||||
const auto chunkSize = chunk.size();
|
||||
if (chunkSize <= static_cast<size_t>(dataOffset)) {
|
||||
LOG("Bad chunkSize for data, aborting");
|
||||
toReturn = "";
|
||||
totalSize = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
auto oldSize = toReturn.size();
|
||||
toReturn.resize(oldSize + MAX_UNCOMPRESSED_CHUNK_SIZE);
|
||||
@@ -151,13 +128,6 @@ std::string Sd0::GetAsStringUncompressed() const {
|
||||
reinterpret_cast<uint8_t*>(toReturn.data()) + oldSize, MAX_UNCOMPRESSED_CHUNK_SIZE,
|
||||
error);
|
||||
|
||||
if (uncompressedSize == -1) {
|
||||
LOG("Failed to decompress chunk, aborting");
|
||||
toReturn = "";
|
||||
totalSize = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
totalSize += uncompressedSize;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
#include "zlib.h"
|
||||
|
||||
namespace ZCompression {
|
||||
uint32_t GetMaxCompressedLength(uint32_t nLenSrc) {
|
||||
uint32_t n16kBlocks = (nLenSrc + 16383) / 16384; // round up any fraction of a block
|
||||
int32_t GetMaxCompressedLength(int32_t nLenSrc) {
|
||||
int32_t n16kBlocks = (nLenSrc + 16383) / 16384; // round up any fraction of a block
|
||||
return (nLenSrc + 6 + (n16kBlocks * 5));
|
||||
}
|
||||
|
||||
int32_t Compress(const uint8_t* abSrc, uint32_t nLenSrc, uint8_t* abDst, uint32_t nLenDst) {
|
||||
int32_t Compress(const uint8_t* abSrc, int32_t nLenSrc, uint8_t* abDst, int32_t nLenDst) {
|
||||
z_stream zInfo = { 0 };
|
||||
zInfo.total_in = zInfo.avail_in = nLenSrc;
|
||||
zInfo.total_out = zInfo.avail_out = nLenDst;
|
||||
@@ -27,7 +27,7 @@ namespace ZCompression {
|
||||
return(nRet);
|
||||
}
|
||||
|
||||
int32_t Decompress(const uint8_t* abSrc, uint32_t nLenSrc, uint8_t* abDst, uint32_t nLenDst, int32_t& nErr) {
|
||||
int32_t Decompress(const uint8_t* abSrc, int32_t nLenSrc, uint8_t* abDst, int32_t nLenDst, int32_t& nErr) {
|
||||
// Get the size of the decompressed data
|
||||
z_stream zInfo = { 0 };
|
||||
zInfo.total_in = zInfo.avail_in = nLenSrc;
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
#include <cstdint>
|
||||
|
||||
namespace ZCompression {
|
||||
uint32_t GetMaxCompressedLength(uint32_t nLenSrc);
|
||||
int32_t GetMaxCompressedLength(int32_t nLenSrc);
|
||||
|
||||
int32_t Compress(const uint8_t* abSrc, uint32_t nLenSrc, uint8_t* abDst, uint32_t nLenDst);
|
||||
int32_t Compress(const uint8_t* abSrc, int32_t nLenSrc, uint8_t* abDst, int32_t nLenDst);
|
||||
|
||||
int32_t Decompress(const uint8_t* abSrc, uint32_t nLenSrc, uint8_t* abDst, uint32_t nLenDst, int32_t& nErr);
|
||||
int32_t Decompress(const uint8_t* abSrc, int32_t nLenSrc, uint8_t* abDst, int32_t nLenDst, int32_t& nErr);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
#include "zlib.h"
|
||||
|
||||
constexpr uint32_t CRC32_INIT = 0xFFFFFFFF;
|
||||
constexpr auto NULL_TERMINATOR = std::string_view{ "\0\0\0", 4 };
|
||||
constexpr auto NULL_TERMINATOR = std::string_view{"\0\0\0", 4};
|
||||
|
||||
AssetManager::AssetManager(const std::filesystem::path& path) {
|
||||
if (!std::filesystem::is_directory(path)) {
|
||||
@@ -25,7 +25,7 @@ AssetManager::AssetManager(const std::filesystem::path& path) {
|
||||
if (!std::filesystem::exists(m_Path / ".." / "versions")) {
|
||||
throw std::runtime_error("No \"versions\" directory found in the parent directories of \"res\" - packed asset bundle cannot be loaded.");
|
||||
}
|
||||
|
||||
|
||||
m_AssetBundleType = eAssetBundleType::Packed;
|
||||
|
||||
m_RootPath = (m_Path / "..");
|
||||
@@ -34,7 +34,7 @@ AssetManager::AssetManager(const std::filesystem::path& path) {
|
||||
if (!std::filesystem::exists(m_Path / ".." / ".." / "versions")) {
|
||||
throw std::runtime_error("No \"versions\" directory found in the parent directories of \"res\" - packed asset bundle cannot be loaded.");
|
||||
}
|
||||
|
||||
|
||||
m_AssetBundleType = eAssetBundleType::Packed;
|
||||
|
||||
m_RootPath = (m_Path / ".." / "..");
|
||||
@@ -54,15 +54,15 @@ AssetManager::AssetManager(const std::filesystem::path& path) {
|
||||
}
|
||||
|
||||
switch (m_AssetBundleType) {
|
||||
case eAssetBundleType::Packed: {
|
||||
this->LoadPackIndex();
|
||||
break;
|
||||
}
|
||||
case eAssetBundleType::None:
|
||||
[[fallthrough]];
|
||||
case eAssetBundleType::Unpacked: {
|
||||
break;
|
||||
}
|
||||
case eAssetBundleType::Packed: {
|
||||
this->LoadPackIndex();
|
||||
break;
|
||||
}
|
||||
case eAssetBundleType::None:
|
||||
[[fallthrough]];
|
||||
case eAssetBundleType::Unpacked: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ bool AssetManager::HasFile(std::string fixedName) const {
|
||||
std::replace(fixedName.begin(), fixedName.end(), '\\', '/');
|
||||
if (std::filesystem::exists(m_ResPath / fixedName)) return true;
|
||||
|
||||
if (this->m_AssetBundleType == eAssetBundleType::Unpacked || !m_PackIndex) return false;
|
||||
if (this->m_AssetBundleType == eAssetBundleType::Unpacked) return false;
|
||||
|
||||
std::replace(fixedName.begin(), fixedName.end(), '/', '\\');
|
||||
if (fixedName.rfind("client\\res\\", 0) != 0) fixedName = "client\\res\\" + fixedName;
|
||||
@@ -145,12 +145,8 @@ bool AssetManager::GetFile(std::string fixedName, char** data, uint32_t* len) co
|
||||
}
|
||||
|
||||
const auto& pack = this->m_PackIndex->GetPacks().at(packIndex);
|
||||
bool success = false;
|
||||
try {
|
||||
success = pack.ReadFileFromPack(crc, data, len);
|
||||
} catch (std::exception& e) {
|
||||
LOG("Failed to read file %s from pack file", fixedName.c_str());
|
||||
}
|
||||
const bool success = pack.ReadFileFromPack(crc, data, len);
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,6 @@ bool Pack::HasFile(const uint32_t crc) const {
|
||||
}
|
||||
|
||||
bool Pack::ReadFileFromPack(const uint32_t crc, char** data, uint32_t* len) const {
|
||||
const auto pathStr = m_FilePath.string();
|
||||
// Time for some wacky C file reading for speed reasons
|
||||
|
||||
PackRecord pkRecord{};
|
||||
@@ -66,21 +65,16 @@ bool Pack::ReadFileFromPack(const uint32_t crc, char** data, uint32_t* len) cons
|
||||
bool isCompressed = (pkRecord.m_IsCompressed & 0xff) > 0;
|
||||
auto inPackSize = isCompressed ? pkRecord.m_CompressedSize : pkRecord.m_UncompressedSize;
|
||||
|
||||
FILE* file = nullptr;
|
||||
FILE* file;
|
||||
#ifdef _WIN32
|
||||
fopen_s(&file, pathStr.c_str(), "rb");
|
||||
fopen_s(&file, m_FilePath.string().c_str(), "rb");
|
||||
#elif __APPLE__
|
||||
// macOS has 64bit file IO by default
|
||||
file = fopen(pathStr.c_str(), "rb");
|
||||
file = fopen(m_FilePath.string().c_str(), "rb");
|
||||
#else
|
||||
file = fopen64(pathStr.c_str(), "rb");
|
||||
file = fopen64(m_FilePath.string().c_str(), "rb");
|
||||
#endif
|
||||
|
||||
if (!file) {
|
||||
LOG("No file found for path %s", pathStr.c_str());
|
||||
throw std::runtime_error("Could not find file " + pathStr);
|
||||
}
|
||||
|
||||
fseek(file, pos, SEEK_SET);
|
||||
|
||||
if (!isCompressed) {
|
||||
@@ -108,18 +102,14 @@ bool Pack::ReadFileFromPack(const uint32_t crc, char** data, uint32_t* len) cons
|
||||
int32_t readInData = fread(&size, sizeof(uint32_t), 1, file);
|
||||
pos += 4; // Move pointer position 4 to the right
|
||||
|
||||
std::unique_ptr<char[]> chunk(new char[size]);
|
||||
int32_t readInData2 = fread(chunk.get(), sizeof(int8_t), size, file);
|
||||
char* chunk = static_cast<char*>(malloc(size));
|
||||
int32_t readInData2 = fread(chunk, sizeof(int8_t), size, file);
|
||||
pos += size; // Move pointer position the amount of bytes read to the right
|
||||
|
||||
int32_t err;
|
||||
const auto countToRead = ZCompression::Decompress(reinterpret_cast<uint8_t*>(chunk.get()), size, reinterpret_cast<uint8_t*>(decompressedData + currentReadPos), Sd0::MAX_UNCOMPRESSED_CHUNK_SIZE, err);
|
||||
if (countToRead == -1) {
|
||||
LOG("Error decompressing zlib data from file %s", pathStr.c_str());
|
||||
throw std::runtime_error("Error decompressing zlib data from file " + pathStr);
|
||||
}
|
||||
currentReadPos += countToRead;
|
||||
currentReadPos += ZCompression::Decompress(reinterpret_cast<uint8_t*>(chunk), size, reinterpret_cast<uint8_t*>(decompressedData + currentReadPos), Sd0::MAX_UNCOMPRESSED_CHUNK_SIZE, err);
|
||||
|
||||
free(chunk);
|
||||
}
|
||||
|
||||
*data = decompressedData;
|
||||
|
||||
@@ -84,7 +84,3 @@ void dConfig::ProcessLine(const std::string& line) {
|
||||
|
||||
this->m_ConfigValues.insert(std::make_pair(key, value));
|
||||
}
|
||||
|
||||
std::string dConfig::GetValue(const std::string& key, const char* emptyValue) {
|
||||
return GetValue(key, std::string(emptyValue));
|
||||
};
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
#include "GeneralUtils.h"
|
||||
|
||||
class dConfig {
|
||||
public:
|
||||
dConfig(const std::string& filepath);
|
||||
@@ -24,14 +22,6 @@ public:
|
||||
*/
|
||||
const std::string& GetValue(std::string key);
|
||||
|
||||
// Gets a value from the config and returns the parsed value, or the default value should parsing have failed.
|
||||
template<typename T>
|
||||
T GetValue(const std::string& key, const T emptyValue = T()) {
|
||||
return GeneralUtils::TryParse<T>(GetValue(key)).value_or(emptyValue);
|
||||
}
|
||||
|
||||
std::string GetValue(const std::string& key, const char* emptyValue);
|
||||
|
||||
/**
|
||||
* Loads the config from a file
|
||||
*/
|
||||
@@ -53,9 +43,3 @@ private:
|
||||
std::vector<std::function<void()>> m_ConfigHandlers;
|
||||
std::string m_ConfigFilePath;
|
||||
};
|
||||
|
||||
template<>
|
||||
inline std::string dConfig::GetValue(const std::string& key, const std::string emptyValue) {
|
||||
const auto& value = GetValue(key);
|
||||
return value.empty() ? emptyValue : value;
|
||||
};
|
||||
|
||||
@@ -27,6 +27,8 @@ namespace MessageType {
|
||||
AFFIRM_TRANSFER_REQUEST,
|
||||
AFFIRM_TRANSFER_RESPONSE,
|
||||
|
||||
NEW_SESSION_ALERT
|
||||
NEW_SESSION_ALERT,
|
||||
|
||||
REQUEST_SERVER_LIST
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@ enum class ServiceType : uint16_t {
|
||||
COMMON = 0,
|
||||
AUTH,
|
||||
CHAT,
|
||||
WORLD = 4,
|
||||
DASHBOARD,
|
||||
WORLD,
|
||||
CLIENT,
|
||||
MASTER,
|
||||
UNKNOWN
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
|
||||
// These are the same define, but they mean two different things in different contexts
|
||||
// so a different define to distinguish what calculation is happening will help clarity.
|
||||
#define FRAMES_TO_MS(x) (1000 / (x))
|
||||
#define MS_TO_FRAMES(x) (1000 / (x))
|
||||
#define FRAMES_TO_MS(x) 1000 / x
|
||||
#define MS_TO_FRAMES(x) 1000 / x
|
||||
|
||||
//=========== FRAME TIMINGS ===========
|
||||
constexpr uint32_t highFramerate = 60;
|
||||
@@ -58,7 +58,6 @@ constexpr LWOCLONEID LWOCLONEID_INVALID = -1; //!< Invalid LWOCLONEID
|
||||
constexpr LWOINSTANCEID LWOINSTANCEID_INVALID = -1; //!< Invalid LWOINSTANCEID
|
||||
constexpr LWOMAPID LWOMAPID_INVALID = -1; //!< Invalid LWOMAPID
|
||||
constexpr uint64_t LWOZONEID_INVALID = 0; //!< Invalid LWOZONEID
|
||||
constexpr uint32_t MAX_MESSAGE_LENGTH = 0x500000; //!< Prevent exceptionally large msgs from being processed. Should always be used to check user provided inputs.
|
||||
|
||||
constexpr float PI = 3.14159f;
|
||||
|
||||
|
||||
@@ -20,8 +20,7 @@ enum class eCharacterVersion : uint32_t {
|
||||
NJ_JAYMISSIONS,
|
||||
NEXUS_FORCE_EXPLORER, // Fixes pet ids in player inventories
|
||||
PET_IDS, // Fixes pet ids in player inventories
|
||||
INVENTORY_PERSISTENT_IDS, // Fixes racing meta missions
|
||||
UP_TO_DATE, // will become RACING_META_MISSIONS
|
||||
UP_TO_DATE, // will become INVENTORY_PERSISTENT_IDS
|
||||
};
|
||||
|
||||
#endif //!__ECHARACTERVERSION__H__
|
||||
|
||||
58
dDashboardServer/CMakeLists.txt
Normal file
58
dDashboardServer/CMakeLists.txt
Normal file
@@ -0,0 +1,58 @@
|
||||
set(DDASHBOARDSERVER_SOURCES
|
||||
"DashboardServer.cpp"
|
||||
)
|
||||
|
||||
add_subdirectory(routes)
|
||||
add_subdirectory(auth)
|
||||
|
||||
add_executable(DashboardServer ${DDASHBOARDSERVER_SOURCES})
|
||||
|
||||
target_include_directories(DashboardServer PRIVATE
|
||||
"${PROJECT_SOURCE_DIR}/dCommon"
|
||||
"${PROJECT_SOURCE_DIR}/dCommon/dClient"
|
||||
"${PROJECT_SOURCE_DIR}/dCommon/dEnums"
|
||||
"${PROJECT_SOURCE_DIR}/dDatabase"
|
||||
"${PROJECT_SOURCE_DIR}/dDatabase/CDClientDatabase"
|
||||
"${PROJECT_SOURCE_DIR}/dDatabase/CDClientDatabase/CDClientTables"
|
||||
"${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase"
|
||||
"${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase/ITables"
|
||||
"${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase/MySQL"
|
||||
"${PROJECT_SOURCE_DIR}/dNet"
|
||||
"${PROJECT_SOURCE_DIR}/dWeb"
|
||||
"${PROJECT_SOURCE_DIR}/dServer"
|
||||
"${PROJECT_SOURCE_DIR}/thirdparty"
|
||||
"${PROJECT_SOURCE_DIR}/thirdparty/nlohmann"
|
||||
"${PROJECT_SOURCE_DIR}/dDashboardServer"
|
||||
"${PROJECT_SOURCE_DIR}/dDashboardServer/auth"
|
||||
"${PROJECT_SOURCE_DIR}/dDashboardServer/routes"
|
||||
)
|
||||
|
||||
target_link_libraries(DashboardServer ${COMMON_LIBRARIES} dWeb dServer bcrypt OpenSSL::Crypto DashboardRoutes DashboardAuth)
|
||||
|
||||
|
||||
# Copy static files and templates to build directory (always copy)
|
||||
add_custom_command(TARGET DashboardServer POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E remove_directory
|
||||
${CMAKE_BINARY_DIR}/dDashboardServer/static
|
||||
COMMENT "Removing old static files"
|
||||
)
|
||||
|
||||
add_custom_command(TARGET DashboardServer POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/static
|
||||
${CMAKE_BINARY_DIR}/dDashboardServer/static
|
||||
COMMENT "Copying DashboardServer static files"
|
||||
)
|
||||
|
||||
add_custom_command(TARGET DashboardServer POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E remove_directory
|
||||
${CMAKE_BINARY_DIR}/dDashboardServer/templates
|
||||
COMMENT "Removing old templates"
|
||||
)
|
||||
|
||||
add_custom_command(TARGET DashboardServer POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/templates
|
||||
${CMAKE_BINARY_DIR}/dDashboardServer/templates
|
||||
COMMENT "Copying DashboardServer templates"
|
||||
)
|
||||
203
dDashboardServer/DashboardServer.cpp
Normal file
203
dDashboardServer/DashboardServer.cpp
Normal file
@@ -0,0 +1,203 @@
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
#include <csignal>
|
||||
#include <memory>
|
||||
|
||||
#include "CDClientDatabase.h"
|
||||
#include "CDClientManager.h"
|
||||
#include "Database.h"
|
||||
#include "dConfig.h"
|
||||
#include "Logger.h"
|
||||
#include "dServer.h"
|
||||
#include "AssetManager.h"
|
||||
#include "BinaryPathFinder.h"
|
||||
#include "ServiceType.h"
|
||||
#include "MessageType/Master.h"
|
||||
#include "Game.h"
|
||||
#include "BitStreamUtils.h"
|
||||
#include "Diagnostics.h"
|
||||
#include "Web.h"
|
||||
#include "Server.h"
|
||||
|
||||
#include "ServerState.h"
|
||||
#include "APIRoutes.h"
|
||||
#include "StaticRoutes.h"
|
||||
#include "DashboardRoutes.h"
|
||||
#include "WSRoutes.h"
|
||||
#include "AuthRoutes.h"
|
||||
#include "AuthMiddleware.h"
|
||||
|
||||
namespace Game {
|
||||
Logger* logger = nullptr;
|
||||
dServer* server = nullptr;
|
||||
dConfig* config = nullptr;
|
||||
Game::signal_t lastSignal = 0;
|
||||
std::mt19937 randomEngine;
|
||||
}
|
||||
|
||||
// Define global server state
|
||||
namespace ServerState {
|
||||
ServerStatus g_AuthStatus{};
|
||||
ServerStatus g_ChatStatus{};
|
||||
std::vector<WorldInstanceInfo> g_WorldInstances{};
|
||||
std::mutex g_StatusMutex{};
|
||||
}
|
||||
|
||||
namespace {
|
||||
dServer* g_Server = nullptr;
|
||||
bool g_RequestedServerList = false;
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
Diagnostics::SetProduceMemoryDump(true);
|
||||
std::signal(SIGINT, Game::OnSignal);
|
||||
std::signal(SIGTERM, Game::OnSignal);
|
||||
|
||||
uint32_t maxClients = 999;
|
||||
uint32_t ourPort = 2006;
|
||||
std::string ourIP = "127.0.0.1";
|
||||
|
||||
// Read config
|
||||
Game::config = new dConfig("dashboardconfig.ini");
|
||||
|
||||
// Setup logger
|
||||
Server::SetupLogger("DashboardServer");
|
||||
if (!Game::logger) return EXIT_FAILURE;
|
||||
Game::config->LogSettings();
|
||||
|
||||
LOG("Starting Dashboard Server");
|
||||
|
||||
// Load settings
|
||||
if (Game::config->GetValue("max_clients") != "")
|
||||
maxClients = std::stoi(Game::config->GetValue("max_clients"));
|
||||
|
||||
if (Game::config->GetValue("port") != "")
|
||||
ourPort = std::atoi(Game::config->GetValue("port").c_str());
|
||||
|
||||
if (Game::config->GetValue("listen_ip") != "")
|
||||
ourIP = Game::config->GetValue("listen_ip");
|
||||
|
||||
// Connect to CDClient database
|
||||
try {
|
||||
const std::string cdclientPath = BinaryPathFinder::GetBinaryDir() / "resServer/CDServer.sqlite";
|
||||
CDClientDatabase::Connect(cdclientPath);
|
||||
} catch (std::exception& ex) {
|
||||
LOG("Failed to connect to CDClient database: %s", ex.what());
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
// Connect to the database
|
||||
try {
|
||||
Database::Connect();
|
||||
} catch (std::exception& ex) {
|
||||
LOG("Failed to connect to the database: %s", ex.what());
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
// Get master info from database
|
||||
std::string masterIP = "localhost";
|
||||
uint32_t masterPort = 1000;
|
||||
std::string masterPassword;
|
||||
auto masterInfo = Database::Get()->GetMasterInfo();
|
||||
if (masterInfo) {
|
||||
masterIP = masterInfo->ip;
|
||||
masterPort = masterInfo->port;
|
||||
masterPassword = masterInfo->password;
|
||||
}
|
||||
|
||||
// Setup network server for communicating with Master
|
||||
g_Server = new dServer(
|
||||
masterIP,
|
||||
ourPort,
|
||||
0,
|
||||
maxClients,
|
||||
false,
|
||||
false,
|
||||
Game::logger,
|
||||
masterIP,
|
||||
masterPort,
|
||||
ServiceType::DASHBOARD, // Connect as dashboard to master
|
||||
Game::config,
|
||||
&Game::lastSignal,
|
||||
masterPassword
|
||||
);
|
||||
|
||||
// Initialize web server
|
||||
if (!Game::web.Startup(ourIP, ourPort)) {
|
||||
LOG("Failed to start web server on %s:%d", ourIP.c_str(), ourPort);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
// Register global middleware
|
||||
Game::web.AddGlobalMiddleware(std::make_shared<AuthMiddleware>());
|
||||
|
||||
// Register routes in order: API, Static, Auth, WebSocket, Dashboard (dashboard MUST be last)
|
||||
RegisterAPIRoutes();
|
||||
RegisterStaticRoutes();
|
||||
RegisterAuthRoutes();
|
||||
RegisterWSRoutes();
|
||||
RegisterDashboardRoutes(); // Must be last - catches all unmatched routes
|
||||
|
||||
LOG("Dashboard Server started successfully on %s:%d", ourIP.c_str(), ourPort);
|
||||
LOG("Connected to Master Server at %s:%d", masterIP.c_str(), masterPort);
|
||||
|
||||
// Main loop
|
||||
auto lastTime = std::chrono::high_resolution_clock::now();
|
||||
auto lastBroadcast = lastTime;
|
||||
auto currentTime = lastTime;
|
||||
constexpr float deltaTime = 1.0f / 60.0f; // 60 FPS
|
||||
constexpr float broadcastInterval = 2000.0f; // Broadcast every 2 seconds
|
||||
|
||||
while (!Game::ShouldShutdown()) {
|
||||
currentTime = std::chrono::high_resolution_clock::now();
|
||||
const auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(currentTime - lastTime).count();
|
||||
const auto elapsedSinceBroadcast = std::chrono::duration_cast<std::chrono::milliseconds>(currentTime - lastBroadcast).count();
|
||||
|
||||
if (elapsed >= 1000.0f / 60.0f) {
|
||||
// // Handle master server packets
|
||||
// Packet* packet = g_Server->ReceiveFromMaster();
|
||||
// if (packet) {
|
||||
// RakNet::BitStream bitStream(packet->data, packet->length, false);
|
||||
// PacketHandler::HandlePacket(bitStream, packet->systemAddress);
|
||||
// g_Server->DeallocateMasterPacket(packet);
|
||||
// }
|
||||
|
||||
// // Handle RakNet protocol packets from connected servers
|
||||
// packet = g_Server->Receive();
|
||||
// while (packet) {
|
||||
// RakNet::BitStream bitStream(packet->data, packet->length, false);
|
||||
// PacketHandler::HandlePacket(bitStream, packet->systemAddress);
|
||||
// g_Server->DeallocatePacket(packet);
|
||||
// packet = g_Server->Receive();
|
||||
// }
|
||||
|
||||
// Handle web requests
|
||||
Game::web.ReceiveRequests();
|
||||
|
||||
// Broadcast dashboard updates periodically
|
||||
if (elapsedSinceBroadcast >= broadcastInterval) {
|
||||
BroadcastDashboardUpdate();
|
||||
lastBroadcast = currentTime;
|
||||
}
|
||||
|
||||
lastTime = currentTime;
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
Database::Destroy("DashboardServer");
|
||||
delete g_Server;
|
||||
g_Server = nullptr;
|
||||
delete Game::logger;
|
||||
Game::logger = nullptr;
|
||||
delete Game::config;
|
||||
Game::config = nullptr;
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
|
||||
132
dDashboardServer/auth/AuthMiddleware.cpp
Normal file
132
dDashboardServer/auth/AuthMiddleware.cpp
Normal file
@@ -0,0 +1,132 @@
|
||||
#include "AuthMiddleware.h"
|
||||
#include "DashboardAuthService.h"
|
||||
#include "Game.h"
|
||||
#include "Logger.h"
|
||||
#include <string>
|
||||
#include <cctype>
|
||||
|
||||
// Helper to extract cookie value from header
|
||||
static std::string ExtractCookieValue(const std::string& cookieHeader, const std::string& cookieName) {
|
||||
std::string searchStr = cookieName + "=";
|
||||
size_t pos = cookieHeader.find(searchStr);
|
||||
|
||||
if (pos == std::string::npos) {
|
||||
return "";
|
||||
}
|
||||
|
||||
size_t valueStart = pos + searchStr.length();
|
||||
size_t valueEnd = cookieHeader.find(";", valueStart);
|
||||
|
||||
if (valueEnd == std::string::npos) {
|
||||
valueEnd = cookieHeader.length();
|
||||
}
|
||||
|
||||
std::string value = cookieHeader.substr(valueStart, valueEnd - valueStart);
|
||||
|
||||
// URL decode the value
|
||||
std::string decoded;
|
||||
for (size_t i = 0; i < value.length(); ++i) {
|
||||
if (value[i] == '%' && i + 2 < value.length()) {
|
||||
std::string hex = value.substr(i + 1, 2);
|
||||
char* endptr;
|
||||
int charCode = static_cast<int>(std::strtol(hex.c_str(), &endptr, 16));
|
||||
if (endptr - hex.c_str() == 2) {
|
||||
decoded += static_cast<char>(charCode);
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
decoded += value[i];
|
||||
}
|
||||
|
||||
return decoded;
|
||||
}
|
||||
|
||||
std::string AuthMiddleware::ExtractTokenFromQueryString(const std::string& queryString) {
|
||||
if (queryString.empty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Parse query string to find token parameter
|
||||
// Expected format: "?token=eyJhbGc..."
|
||||
std::string tokenPrefix = "token=";
|
||||
size_t tokenPos = queryString.find(tokenPrefix);
|
||||
|
||||
if (tokenPos == std::string::npos) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Extract token value (from "token=" to next "&" or end of string)
|
||||
size_t valueStart = tokenPos + tokenPrefix.length();
|
||||
size_t valueEnd = queryString.find("&", valueStart);
|
||||
|
||||
if (valueEnd == std::string::npos) {
|
||||
valueEnd = queryString.length();
|
||||
}
|
||||
|
||||
return queryString.substr(valueStart, valueEnd - valueStart);
|
||||
}
|
||||
|
||||
std::string AuthMiddleware::ExtractTokenFromCookies(const std::string& cookieHeader) {
|
||||
if (cookieHeader.empty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Extract dashboardToken cookie value
|
||||
return ExtractCookieValue(cookieHeader, "dashboardToken");
|
||||
}
|
||||
|
||||
std::string AuthMiddleware::ExtractTokenFromAuthHeader(const std::string& authHeader) {
|
||||
if (authHeader.empty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Check for "Bearer <token>" format
|
||||
if (authHeader.substr(0, 7) == "Bearer ") {
|
||||
return authHeader.substr(7);
|
||||
}
|
||||
|
||||
// Check for "Token <token>" format
|
||||
if (authHeader.substr(0, 6) == "Token ") {
|
||||
return authHeader.substr(6);
|
||||
}
|
||||
|
||||
// If no prefix, assume raw token
|
||||
return authHeader;
|
||||
}
|
||||
|
||||
bool AuthMiddleware::Process(HTTPContext& context, HTTPReply& reply) {
|
||||
// Try to extract token from various sources (in priority order)
|
||||
std::string token = ExtractTokenFromQueryString(context.queryString);
|
||||
|
||||
if (token.empty()) {
|
||||
const std::string& cookieHeader = context.GetHeader("Cookie");
|
||||
token = ExtractTokenFromCookies(cookieHeader);
|
||||
}
|
||||
|
||||
if (token.empty()) {
|
||||
const std::string& authHeader = context.GetHeader("Authorization");
|
||||
token = ExtractTokenFromAuthHeader(authHeader);
|
||||
}
|
||||
|
||||
// If we found a token, try to verify it
|
||||
if (!token.empty()) {
|
||||
std::string username;
|
||||
uint8_t gmLevel{};
|
||||
|
||||
if (DashboardAuthService::VerifyToken(token, username, gmLevel)) {
|
||||
context.isAuthenticated = true;
|
||||
context.authenticatedUser = username;
|
||||
context.gmLevel = gmLevel;
|
||||
LOG_DEBUG("User %s authenticated via API token (GM level %d)", username.c_str(), gmLevel);
|
||||
return true;
|
||||
} else {
|
||||
LOG_DEBUG("Invalid authentication token provided");
|
||||
return true; // Continue - let routes decide if auth is required
|
||||
}
|
||||
}
|
||||
|
||||
// No token found - continue without authentication
|
||||
// Routes can use RequireAuthMiddleware to enforce authentication
|
||||
return true;
|
||||
}
|
||||
34
dDashboardServer/auth/AuthMiddleware.h
Normal file
34
dDashboardServer/auth/AuthMiddleware.h
Normal file
@@ -0,0 +1,34 @@
|
||||
#ifndef __AUTHMIDDLEWARE_H__
|
||||
#define __AUTHMIDDLEWARE_H__
|
||||
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include "IHTTPMiddleware.h"
|
||||
|
||||
/**
|
||||
* AuthMiddleware: Extracts and verifies authentication tokens
|
||||
*
|
||||
* Token extraction sources (in priority order):
|
||||
* 1. Query parameter: ?token=eyJhbGc...
|
||||
* 2. Cookie: dashboardToken=...
|
||||
* 3. Authorization header: Bearer <token> or Token <token>
|
||||
*
|
||||
* Sets HTTPContext.isAuthenticated, HTTPContext.authenticatedUser,
|
||||
* and HTTPContext.gmLevel if token is valid.
|
||||
*/
|
||||
class AuthMiddleware final : public IHTTPMiddleware {
|
||||
public:
|
||||
AuthMiddleware() = default;
|
||||
~AuthMiddleware() override = default;
|
||||
|
||||
bool Process(HTTPContext& context, HTTPReply& reply) override;
|
||||
std::string GetName() const override { return "AuthMiddleware"; }
|
||||
|
||||
private:
|
||||
// Extract token from various sources
|
||||
static std::string ExtractTokenFromQueryString(const std::string& queryString);
|
||||
static std::string ExtractTokenFromCookies(const std::string& cookieHeader);
|
||||
static std::string ExtractTokenFromAuthHeader(const std::string& authHeader);
|
||||
};
|
||||
|
||||
#endif // !__AUTHMIDDLEWARE_H__
|
||||
28
dDashboardServer/auth/CMakeLists.txt
Normal file
28
dDashboardServer/auth/CMakeLists.txt
Normal file
@@ -0,0 +1,28 @@
|
||||
set(DASHBOARDAUTH_SOURCES
|
||||
"JWTUtils.cpp"
|
||||
"DashboardAuthService.cpp"
|
||||
"AuthMiddleware.cpp"
|
||||
"RequireAuthMiddleware.cpp"
|
||||
)
|
||||
|
||||
add_library(DashboardAuth STATIC ${DASHBOARDAUTH_SOURCES})
|
||||
|
||||
target_include_directories(DashboardAuth PRIVATE
|
||||
"${PROJECT_SOURCE_DIR}/dCommon"
|
||||
"${PROJECT_SOURCE_DIR}/dCommon/dClient"
|
||||
"${PROJECT_SOURCE_DIR}/dCommon/dEnums"
|
||||
"${PROJECT_SOURCE_DIR}/dDatabase"
|
||||
"${PROJECT_SOURCE_DIR}/dDatabase/CDClientDatabase"
|
||||
"${PROJECT_SOURCE_DIR}/dDatabase/CDClientDatabase/CDClientTables"
|
||||
"${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase"
|
||||
"${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase/ITables"
|
||||
"${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase/MySQL"
|
||||
"${PROJECT_SOURCE_DIR}/dNet"
|
||||
"${PROJECT_SOURCE_DIR}/dWeb"
|
||||
"${PROJECT_SOURCE_DIR}/dServer"
|
||||
"${PROJECT_SOURCE_DIR}/thirdparty"
|
||||
"${PROJECT_SOURCE_DIR}/thirdparty/nlohmann"
|
||||
"${PROJECT_SOURCE_DIR}/dDashboardServer/auth"
|
||||
)
|
||||
|
||||
target_link_libraries(DashboardAuth PRIVATE ${COMMON_LIBRARIES} dWeb dServer bcrypt OpenSSL::Crypto)
|
||||
144
dDashboardServer/auth/DashboardAuthService.cpp
Normal file
144
dDashboardServer/auth/DashboardAuthService.cpp
Normal file
@@ -0,0 +1,144 @@
|
||||
#include "DashboardAuthService.h"
|
||||
#include "JWTUtils.h"
|
||||
#include "Database.h"
|
||||
#include "Logger.h"
|
||||
#include "Game.h"
|
||||
#include "dConfig.h"
|
||||
#include "GeneralUtils.h"
|
||||
#include <bcrypt/bcrypt.h>
|
||||
#include <ctime>
|
||||
|
||||
namespace {
|
||||
constexpr int64_t LOCKOUT_DURATION = 15 * 60; // 15 minutes in seconds
|
||||
|
||||
}
|
||||
|
||||
DashboardAuthService::LoginResult DashboardAuthService::Login(
|
||||
const std::string& username,
|
||||
const std::string& password,
|
||||
bool rememberMe) {
|
||||
|
||||
LoginResult result;
|
||||
|
||||
if (username.empty() || password.empty()) {
|
||||
result.message = "Username and password are required";
|
||||
return result;
|
||||
}
|
||||
|
||||
if (password.length() > 40) {
|
||||
result.message = "Password exceeds maximum length (40 characters)";
|
||||
return result;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get account info
|
||||
auto accountInfo = Database::Get()->GetAccountInfo(username);
|
||||
if (!accountInfo) {
|
||||
result.message = "Invalid username or password";
|
||||
LOG_DEBUG("Login attempt for non-existent user: %s", username.c_str());
|
||||
return result;
|
||||
}
|
||||
|
||||
uint32_t accountId = accountInfo->id;
|
||||
|
||||
// Check if account is locked
|
||||
bool isLockedOut = Database::Get()->IsLockedOut(accountId);
|
||||
|
||||
if (isLockedOut) {
|
||||
// Record failed attempt even without checking password
|
||||
Database::Get()->RecordFailedAttempt(accountId);
|
||||
uint8_t failedAttempts = Database::Get()->GetFailedAttempts(accountId);
|
||||
|
||||
result.message = "Account is locked due to too many failed attempts";
|
||||
result.accountLocked = true;
|
||||
LOG("Login attempt on locked account: %s (failed attempts: %d)", username.c_str(), failedAttempts);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Check password
|
||||
if (::bcrypt_checkpw(password.c_str(), accountInfo->bcryptPassword.c_str()) != 0) {
|
||||
// Record failed attempt
|
||||
Database::Get()->RecordFailedAttempt(accountId);
|
||||
uint8_t newFailedAttempts = Database::Get()->GetFailedAttempts(accountId);
|
||||
|
||||
// Lock account after 3 failed attempts
|
||||
if (newFailedAttempts >= 3) {
|
||||
int64_t lockoutUntil = std::time(nullptr) + LOCKOUT_DURATION;
|
||||
Database::Get()->SetLockout(accountId, lockoutUntil);
|
||||
result.message = "Account locked due to too many failed attempts";
|
||||
result.accountLocked = true;
|
||||
LOG("Account locked after failed attempts: %s", username.c_str());
|
||||
} else {
|
||||
result.message = "Invalid username or password";
|
||||
LOG_DEBUG("Failed login attempt for user: %s (attempt %d/3)",
|
||||
username.c_str(), newFailedAttempts);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Check GM level
|
||||
if (!HasDashboardAccess(static_cast<uint8_t>(accountInfo->maxGmLevel))) {
|
||||
result.message = "Access denied: insufficient permissions";
|
||||
LOG("Access denied for non-admin user: %s", username.c_str());
|
||||
return result;
|
||||
}
|
||||
|
||||
// Successful login
|
||||
Database::Get()->ClearFailedAttempts(accountId);
|
||||
result.success = true;
|
||||
result.gmLevel = static_cast<uint8_t>(accountInfo->maxGmLevel);
|
||||
result.token = JWTUtils::GenerateToken(username, result.gmLevel, rememberMe);
|
||||
result.message = "Login successful";
|
||||
|
||||
LOG("Successful login: %s (GM Level: %d)", username.c_str(), result.gmLevel);
|
||||
return result;
|
||||
|
||||
} catch (const std::exception& ex) {
|
||||
result.message = "An error occurred during login";
|
||||
LOG("Error during login process: %s", ex.what());
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
bool DashboardAuthService::VerifyToken(const std::string& token, std::string& username, uint8_t& gmLevel) {
|
||||
JWTUtils::JWTPayload payload;
|
||||
if (!JWTUtils::ValidateToken(token, payload)) {
|
||||
LOG_DEBUG("Token validation failed: invalid or expired JWT");
|
||||
return false;
|
||||
}
|
||||
|
||||
username = payload.username;
|
||||
gmLevel = payload.gmLevel;
|
||||
|
||||
// Optionally verify user still exists and has access
|
||||
try {
|
||||
auto accountInfo = Database::Get()->GetAccountInfo(username);
|
||||
if (!accountInfo || !HasDashboardAccess(static_cast<uint8_t>(accountInfo->maxGmLevel))) {
|
||||
LOG_DEBUG("Token verification failed: user no longer has access");
|
||||
return false;
|
||||
}
|
||||
} catch (const std::exception& ex) {
|
||||
LOG_DEBUG("Error verifying user during token validation: %s", ex.what());
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_DEBUG("Token verified successfully for user: %s (GM Level: %d)", username.c_str(), gmLevel);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DashboardAuthService::HasDashboardAccess(uint8_t gmLevel) {
|
||||
// Get minimum GM level from config (default 0 = any user)
|
||||
uint8_t minGmLevel = 0;
|
||||
|
||||
if (Game::config) {
|
||||
const std::string& minGmLevelStr = Game::config->GetValue("min_dashboard_gm_level");
|
||||
if (!minGmLevelStr.empty()) {
|
||||
const auto parsed = GeneralUtils::TryParse<uint8_t>(minGmLevelStr);
|
||||
if (parsed) {
|
||||
minGmLevel = parsed.value();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return gmLevel >= minGmLevel;
|
||||
}
|
||||
47
dDashboardServer/auth/DashboardAuthService.h
Normal file
47
dDashboardServer/auth/DashboardAuthService.h
Normal file
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <cstdint>
|
||||
|
||||
/**
|
||||
* Dashboard authentication service
|
||||
* Handles user login, password verification, and account lockout
|
||||
*/
|
||||
class DashboardAuthService {
|
||||
public:
|
||||
/**
|
||||
* Login result structure
|
||||
*/
|
||||
struct LoginResult {
|
||||
bool success{false};
|
||||
std::string message{};
|
||||
std::string token{}; // JWT token if successful
|
||||
uint8_t gmLevel{0}; // GM level if successful
|
||||
bool accountLocked{false}; // Account is locked out
|
||||
};
|
||||
|
||||
/**
|
||||
* Attempt to log in with username and password
|
||||
* @param username The username
|
||||
* @param password The plaintext password (max 40 characters)
|
||||
* @param rememberMe If true, extends token expiration to 30 days
|
||||
* @return LoginResult with success status and JWT token if successful
|
||||
*/
|
||||
static LoginResult Login(const std::string& username, const std::string& password, bool rememberMe = false);
|
||||
|
||||
/**
|
||||
* Verify that a token is valid and get the username
|
||||
* @param token The JWT token
|
||||
* @param username Output parameter for the username
|
||||
* @param gmLevel Output parameter for the GM level
|
||||
* @return true if token is valid
|
||||
*/
|
||||
static bool VerifyToken(const std::string& token, std::string& username, uint8_t& gmLevel);
|
||||
|
||||
/**
|
||||
* Check if user has required GM level for dashboard access
|
||||
* @param gmLevel The user's GM level
|
||||
* @return true if user can access dashboard (GM level > 0)
|
||||
*/
|
||||
static bool HasDashboardAccess(uint8_t gmLevel);
|
||||
};
|
||||
186
dDashboardServer/auth/JWTUtils.cpp
Normal file
186
dDashboardServer/auth/JWTUtils.cpp
Normal file
@@ -0,0 +1,186 @@
|
||||
#include "JWTUtils.h"
|
||||
#include "GeneralUtils.h"
|
||||
#include "Logger.h"
|
||||
#include "json.hpp"
|
||||
#include <ctime>
|
||||
#include <cstring>
|
||||
#include <openssl/hmac.h>
|
||||
#include <openssl/sha.h>
|
||||
|
||||
namespace {
|
||||
std::string g_Secret = "default-secret-change-me";
|
||||
|
||||
// Simple base64 encoding
|
||||
std::string Base64Encode(const std::string& input) {
|
||||
static const char* base64_chars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
std::string ret;
|
||||
int i = 0;
|
||||
unsigned char char_array_3[3];
|
||||
unsigned char char_array_4[4];
|
||||
|
||||
for (size_t n = 0; n < input.length(); n++) {
|
||||
char_array_3[i++] = input[n];
|
||||
if (i == 3) {
|
||||
char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
|
||||
char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
|
||||
char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
|
||||
char_array_4[3] = char_array_3[2] & 0x3f;
|
||||
for (i = 0; i < 4; i++) ret += base64_chars[char_array_4[i]];
|
||||
i = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (i) {
|
||||
for (int j = i; j < 3; j++) char_array_3[j] = '\0';
|
||||
char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
|
||||
char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
|
||||
char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
|
||||
for (int j = 0; j <= i; j++) ret += base64_chars[char_array_4[j]];
|
||||
while (i++ < 3) ret += '=';
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Simple base64 decoding
|
||||
std::string Base64Decode(const std::string& encoded_string) {
|
||||
static const std::string base64_chars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
int in_len = encoded_string.size();
|
||||
int i = 0, j = 0, in_ = 0;
|
||||
unsigned char char_array_4[4], char_array_3[3];
|
||||
std::string ret;
|
||||
|
||||
while (in_len-- && (encoded_string[in_] != '=') &&
|
||||
(isalnum(encoded_string[in_]) || encoded_string[in_] == '+' || encoded_string[in_] == '/')) {
|
||||
char_array_4[i++] = encoded_string[in_]; in_++;
|
||||
if (i == 4) {
|
||||
for (i = 0; i < 4; i++) char_array_4[i] = base64_chars.find(char_array_4[i]);
|
||||
char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
|
||||
char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
|
||||
char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];
|
||||
for (i = 0; i < 3; i++) ret += char_array_3[i];
|
||||
i = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (i) {
|
||||
for (j = i; j < 4; j++) char_array_4[j] = 0;
|
||||
for (j = 0; j < 4; j++) char_array_4[j] = base64_chars.find(char_array_4[j]);
|
||||
char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
|
||||
char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
|
||||
for (j = 0; j < i - 1; j++) ret += char_array_3[j];
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// HMAC-SHA256
|
||||
std::string HmacSha256(const std::string& key, const std::string& message) {
|
||||
unsigned char* digest = HMAC(EVP_sha256(),
|
||||
reinterpret_cast<const unsigned char*>(key.c_str()), key.length(),
|
||||
reinterpret_cast<const unsigned char*>(message.c_str()), message.length(),
|
||||
nullptr, nullptr);
|
||||
|
||||
std::string result(reinterpret_cast<char*>(digest), SHA256_DIGEST_LENGTH);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Create signature for JWT
|
||||
std::string CreateSignature(const std::string& header, const std::string& payload, const std::string& secret) {
|
||||
std::string message = header + "." + payload;
|
||||
std::string signature = HmacSha256(secret, message);
|
||||
return Base64Encode(signature);
|
||||
}
|
||||
|
||||
// Verify JWT signature
|
||||
bool VerifySignature(const std::string& header, const std::string& payload, const std::string& signature, const std::string& secret) {
|
||||
std::string expected = CreateSignature(header, payload, secret);
|
||||
return signature == expected;
|
||||
}
|
||||
}
|
||||
|
||||
namespace JWTUtils {
|
||||
void SetSecretKey(const std::string& secret) {
|
||||
if (secret.empty()) {
|
||||
LOG("Warning: JWT secret key is empty, using default");
|
||||
return;
|
||||
}
|
||||
g_Secret = secret;
|
||||
}
|
||||
|
||||
std::string GenerateToken(const std::string& username, uint8_t gmLevel, bool rememberMe) {
|
||||
// Header
|
||||
std::string header = R"({"alg":"HS256","typ":"JWT"})";
|
||||
std::string encodedHeader = Base64Encode(header);
|
||||
|
||||
// Payload
|
||||
int64_t now = std::time(nullptr);
|
||||
int64_t expiresAt = now + (rememberMe ? 30 * 24 * 60 * 60 : 24 * 60 * 60); // 30 days or 24 hours
|
||||
|
||||
std::string payload = R"({"username":")" + username + R"(","gmLevel":)" + std::to_string(gmLevel) +
|
||||
R"(,"rememberMe":)" + (rememberMe ? "true" : "false") +
|
||||
R"(,"iat":)" + std::to_string(now) +
|
||||
R"(,"exp":)" + std::to_string(expiresAt) + "}";
|
||||
std::string encodedPayload = Base64Encode(payload);
|
||||
|
||||
// Signature
|
||||
std::string signature = CreateSignature(encodedHeader, encodedPayload, g_Secret);
|
||||
|
||||
return encodedHeader + "." + encodedPayload + "." + signature;
|
||||
}
|
||||
|
||||
bool ValidateToken(const std::string& token, JWTPayload& payload) {
|
||||
// Split token into parts
|
||||
size_t firstDot = token.find('.');
|
||||
size_t secondDot = token.find('.', firstDot + 1);
|
||||
|
||||
if (firstDot == std::string::npos || secondDot == std::string::npos) {
|
||||
LOG_DEBUG("Invalid JWT format");
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string header = token.substr(0, firstDot);
|
||||
std::string encodedPayload = token.substr(firstDot + 1, secondDot - firstDot - 1);
|
||||
std::string signature = token.substr(secondDot + 1);
|
||||
|
||||
// Verify signature
|
||||
if (!VerifySignature(header, encodedPayload, signature, g_Secret)) {
|
||||
LOG_DEBUG("Invalid JWT signature");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Decode and parse payload
|
||||
std::string decodedPayload = Base64Decode(encodedPayload);
|
||||
try {
|
||||
auto json = nlohmann::json::parse(decodedPayload);
|
||||
|
||||
payload.username = json.value("username", "");
|
||||
payload.gmLevel = json.value("gmLevel", 0);
|
||||
payload.rememberMe = json.value("rememberMe", false);
|
||||
payload.issuedAt = json.value("iat", 0);
|
||||
payload.expiresAt = json.value("exp", 0);
|
||||
|
||||
if (payload.username.empty()) {
|
||||
LOG_DEBUG("JWT missing username");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check expiration
|
||||
if (IsTokenExpired(payload.expiresAt)) {
|
||||
LOG_DEBUG("JWT token expired");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (const std::exception& ex) {
|
||||
LOG_DEBUG("Error parsing JWT payload: %s", ex.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool IsTokenExpired(int64_t expiresAt) {
|
||||
return std::time(nullptr) > expiresAt;
|
||||
}
|
||||
}
|
||||
52
dDashboardServer/auth/JWTUtils.h
Normal file
52
dDashboardServer/auth/JWTUtils.h
Normal file
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <ctime>
|
||||
#include "json_fwd.hpp"
|
||||
|
||||
/**
|
||||
* JWT Token utilities for dashboard authentication
|
||||
* Provides secure token generation, validation, and parsing
|
||||
*/
|
||||
namespace JWTUtils {
|
||||
/**
|
||||
* JWT payload structure
|
||||
*/
|
||||
struct JWTPayload {
|
||||
std::string username{};
|
||||
uint8_t gmLevel{0};
|
||||
bool rememberMe{false};
|
||||
int64_t issuedAt{0};
|
||||
int64_t expiresAt{0};
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a new JWT token
|
||||
* @param username The username to encode in the token
|
||||
* @param gmLevel The GM level of the user
|
||||
* @param rememberMe If true, extends token expiration to 30 days; otherwise 24 hours
|
||||
* @return Signed JWT token string
|
||||
*/
|
||||
std::string GenerateToken(const std::string& username, uint8_t gmLevel, bool rememberMe = false);
|
||||
|
||||
/**
|
||||
* Validate and decode a JWT token
|
||||
* @param token The JWT token to validate
|
||||
* @param payload Output parameter for the decoded payload
|
||||
* @return true if token is valid and not expired, false otherwise
|
||||
*/
|
||||
bool ValidateToken(const std::string& token, JWTPayload& payload);
|
||||
|
||||
/**
|
||||
* Check if a token is expired
|
||||
* @param expiresAt Expiration timestamp
|
||||
* @return true if token is expired
|
||||
*/
|
||||
bool IsTokenExpired(int64_t expiresAt);
|
||||
|
||||
/**
|
||||
* Set the JWT secret key (must be called once at startup)
|
||||
* @param secret The secret key for signing tokens
|
||||
*/
|
||||
void SetSecretKey(const std::string& secret);
|
||||
}
|
||||
35
dDashboardServer/auth/RequireAuthMiddleware.cpp
Normal file
35
dDashboardServer/auth/RequireAuthMiddleware.cpp
Normal file
@@ -0,0 +1,35 @@
|
||||
#include "RequireAuthMiddleware.h"
|
||||
#include "HTTPContext.h"
|
||||
#include "Web.h"
|
||||
#include "Game.h"
|
||||
#include "Logger.h"
|
||||
|
||||
RequireAuthMiddleware::RequireAuthMiddleware(uint8_t minGmLevel) : minGmLevel(minGmLevel) {}
|
||||
|
||||
bool RequireAuthMiddleware::Process(HTTPContext& context, HTTPReply& reply) {
|
||||
// Check if user is authenticated
|
||||
if (!context.isAuthenticated) {
|
||||
LOG_DEBUG("Unauthorized access attempt to %s from %s", context.path.c_str(), context.clientIP.c_str());
|
||||
reply.status = eHTTPStatusCode::FOUND;
|
||||
reply.message = "";
|
||||
reply.location = "/login";
|
||||
reply.contentType = eContentType::TEXT_HTML;
|
||||
return false; // Stop middleware chain and send reply
|
||||
}
|
||||
|
||||
// Check if user has required GM level
|
||||
if (context.gmLevel < minGmLevel) {
|
||||
LOG_DEBUG("Forbidden access attempt by user %s (GM level %d < %d required) to %s from %s",
|
||||
context.authenticatedUser.c_str(), context.gmLevel, minGmLevel,
|
||||
context.path.c_str(), context.clientIP.c_str());
|
||||
reply.status = eHTTPStatusCode::FORBIDDEN;
|
||||
reply.message = "{\"error\":\"Forbidden - Insufficient permissions\"}";
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
return false; // Stop middleware chain and send reply
|
||||
}
|
||||
|
||||
// Authentication passed
|
||||
LOG_DEBUG("User %s authenticated with GM level %d accessing %s",
|
||||
context.authenticatedUser.c_str(), context.gmLevel, context.path.c_str());
|
||||
return true; // Continue to next middleware or route handler
|
||||
}
|
||||
30
dDashboardServer/auth/RequireAuthMiddleware.h
Normal file
30
dDashboardServer/auth/RequireAuthMiddleware.h
Normal file
@@ -0,0 +1,30 @@
|
||||
#ifndef __REQUIREAUTHMIDDLEWARE_H__
|
||||
#define __REQUIREAUTHMIDDLEWARE_H__
|
||||
|
||||
#include <memory>
|
||||
#include <cstdint>
|
||||
#include "IHTTPMiddleware.h"
|
||||
|
||||
/**
|
||||
* RequireAuthMiddleware: Enforces authentication on protected routes
|
||||
*
|
||||
* Returns 401 Unauthorized if user is not authenticated
|
||||
* Returns 403 Forbidden if user's GM level is below minimum required
|
||||
*/
|
||||
class RequireAuthMiddleware final : public IHTTPMiddleware {
|
||||
public:
|
||||
/**
|
||||
* @param minGmLevel Minimum GM level required to access this route
|
||||
* 0 = any authenticated user, higher numbers = GM-only
|
||||
*/
|
||||
explicit RequireAuthMiddleware(uint8_t minGmLevel = 0);
|
||||
~RequireAuthMiddleware() override = default;
|
||||
|
||||
bool Process(HTTPContext& context, HTTPReply& reply) override;
|
||||
std::string GetName() const override { return "RequireAuthMiddleware"; }
|
||||
|
||||
private:
|
||||
uint8_t minGmLevel;
|
||||
};
|
||||
|
||||
#endif // !__REQUIREAUTHMIDDLEWARE_H__
|
||||
443
dDashboardServer/routes/APIRoutes.cpp
Normal file
443
dDashboardServer/routes/APIRoutes.cpp
Normal file
@@ -0,0 +1,443 @@
|
||||
#include "APIRoutes.h"
|
||||
#include "ServerState.h"
|
||||
#include "Web.h"
|
||||
#include "eHTTPMethod.h"
|
||||
#include "json.hpp"
|
||||
#include "Game.h"
|
||||
#include "Database.h"
|
||||
#include "Logger.h"
|
||||
#include "HTTPContext.h"
|
||||
#include "RequireAuthMiddleware.h"
|
||||
#include <memory>
|
||||
|
||||
void RegisterAPIRoutes() {
|
||||
// GET /api/status - Get overall server status
|
||||
Game::web.RegisterHTTPRoute({
|
||||
.path = "/api/status",
|
||||
.method = eHTTPMethod::GET,
|
||||
.middleware = { std::make_shared<RequireAuthMiddleware>(0) },
|
||||
.handle = [](HTTPReply& reply, const HTTPContext& context) {
|
||||
nlohmann::json response = ServerState::GetServerStateJson();
|
||||
|
||||
reply.status = eHTTPStatusCode::OK;
|
||||
reply.message = response.dump();
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/players - Get list of online players
|
||||
Game::web.RegisterHTTPRoute({
|
||||
.path = "/api/players",
|
||||
.method = eHTTPMethod::GET,
|
||||
.middleware = { std::make_shared<RequireAuthMiddleware>(0) },
|
||||
.handle = [](HTTPReply& reply, const HTTPContext& context) {
|
||||
nlohmann::json response = {
|
||||
{"players", nlohmann::json::array()},
|
||||
{"count", 0}
|
||||
};
|
||||
|
||||
reply.status = eHTTPStatusCode::OK;
|
||||
reply.message = response.dump();
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/accounts/count - Get total account count
|
||||
Game::web.RegisterHTTPRoute({
|
||||
.path = "/api/accounts/count",
|
||||
.method = eHTTPMethod::GET,
|
||||
.middleware = { std::make_shared<RequireAuthMiddleware>(0) },
|
||||
.handle = [](HTTPReply& reply, const HTTPContext& context) {
|
||||
try {
|
||||
const uint32_t count = Database::Get()->GetAccountCount();
|
||||
nlohmann::json response = {{"count", count}};
|
||||
reply.status = eHTTPStatusCode::OK;
|
||||
reply.message = response.dump();
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
} catch (std::exception& ex) {
|
||||
LOG("Error in /api/accounts/count: %s", ex.what());
|
||||
reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR;
|
||||
reply.message = "{\"error\":\"Database error\"}";
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/characters/count - Get total character count
|
||||
Game::web.RegisterHTTPRoute({
|
||||
.path = "/api/characters/count",
|
||||
.method = eHTTPMethod::GET,
|
||||
.middleware = { std::make_shared<RequireAuthMiddleware>(0) },
|
||||
.handle = [](HTTPReply& reply, const HTTPContext& context) {
|
||||
try {
|
||||
const uint32_t count = Database::Get()->GetCharacterCount();
|
||||
nlohmann::json response = {{"count", count}};
|
||||
reply.status = eHTTPStatusCode::OK;
|
||||
reply.message = response.dump();
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
} catch (std::exception& ex) {
|
||||
LOG("Error in /api/characters/count: %s", ex.what());
|
||||
reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR;
|
||||
reply.message = "{\"error\":\"Database error\"}";
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/tables/accounts - Get accounts table data (DataTables.js format)
|
||||
Game::web.RegisterHTTPRoute({
|
||||
.path = "/api/tables/accounts",
|
||||
.method = eHTTPMethod::POST,
|
||||
.middleware = { std::make_shared<RequireAuthMiddleware>(0) },
|
||||
.handle = [](HTTPReply& reply, const HTTPContext& context) {
|
||||
try {
|
||||
// Only admins (GM > 0) can access table data
|
||||
if (context.gmLevel == 0) {
|
||||
reply.status = eHTTPStatusCode::FORBIDDEN;
|
||||
reply.message = "{\"error\":\"Forbidden - Admin access required\"}";
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
return;
|
||||
}
|
||||
|
||||
nlohmann::json requestData = nlohmann::json::parse(context.body);
|
||||
|
||||
// Extract DataTables parameters
|
||||
uint32_t draw = requestData.value("draw", 1);
|
||||
uint32_t start = requestData.value("start", 0);
|
||||
uint32_t length = requestData.value("length", 10);
|
||||
|
||||
// Extract search - it can be a string or an object with a "value" property
|
||||
std::string search = "";
|
||||
if (requestData.contains("search")) {
|
||||
if (requestData["search"].is_string()) {
|
||||
search = requestData["search"].get<std::string>();
|
||||
} else if (requestData["search"].is_object() && requestData["search"].contains("value")) {
|
||||
search = requestData["search"]["value"].get<std::string>();
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t orderColumn = 0;
|
||||
bool orderAsc = true;
|
||||
|
||||
// Extract order parameters
|
||||
if (requestData.contains("order") && requestData["order"].is_array() && requestData["order"].size() > 0) {
|
||||
orderColumn = requestData["order"][0].value("column", 0);
|
||||
orderAsc = requestData["order"][0].value("dir", "asc") == "asc";
|
||||
}
|
||||
|
||||
// Get the accounts table data
|
||||
nlohmann::json response = Database::Get()->GetAccountsTable(start, length, search, orderColumn, orderAsc);
|
||||
|
||||
reply.status = eHTTPStatusCode::OK;
|
||||
reply.message = response.dump();
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
} catch (const nlohmann::json::exception& jsonEx) {
|
||||
LOG("JSON error in /api/tables/accounts: %s", jsonEx.what());
|
||||
reply.status = eHTTPStatusCode::BAD_REQUEST;
|
||||
reply.message = "{\"error\":\"Invalid JSON\"}";
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
} catch (std::exception& ex) {
|
||||
LOG("Error in /api/tables/accounts: %s", ex.what());
|
||||
reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR;
|
||||
reply.message = "{\"error\":\"Database error\"}";
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/tables/characters - Get characters table data (DataTables.js format)
|
||||
Game::web.RegisterHTTPRoute({
|
||||
.path = "/api/tables/characters",
|
||||
.method = eHTTPMethod::POST,
|
||||
.middleware = { std::make_shared<RequireAuthMiddleware>(0) },
|
||||
.handle = [](HTTPReply& reply, const HTTPContext& context) {
|
||||
try {
|
||||
// Only admins (GM > 0) can access table data
|
||||
if (context.gmLevel == 0) {
|
||||
reply.status = eHTTPStatusCode::FORBIDDEN;
|
||||
reply.message = "{\"error\":\"Forbidden - Admin access required\"}";
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
return;
|
||||
}
|
||||
|
||||
nlohmann::json requestData = nlohmann::json::parse(context.body);
|
||||
|
||||
uint32_t draw = requestData.value("draw", 1);
|
||||
uint32_t start = requestData.value("start", 0);
|
||||
uint32_t length = requestData.value("length", 10);
|
||||
|
||||
std::string search = "";
|
||||
if (requestData.contains("search")) {
|
||||
if (requestData["search"].is_string()) {
|
||||
search = requestData["search"].get<std::string>();
|
||||
} else if (requestData["search"].is_object() && requestData["search"].contains("value")) {
|
||||
search = requestData["search"]["value"].get<std::string>();
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t orderColumn = 0;
|
||||
bool orderAsc = true;
|
||||
|
||||
if (requestData.contains("order") && requestData["order"].is_array() && requestData["order"].size() > 0) {
|
||||
orderColumn = requestData["order"][0].value("column", 0);
|
||||
orderAsc = requestData["order"][0].value("dir", "asc") == "asc";
|
||||
}
|
||||
|
||||
std::string tableData = Database::Get()->GetCharactersTable(start, length, search, orderColumn, orderAsc);
|
||||
|
||||
nlohmann::json response = nlohmann::json::parse(tableData);
|
||||
response["draw"] = draw;
|
||||
|
||||
reply.status = eHTTPStatusCode::OK;
|
||||
reply.message = response.dump();
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
} catch (const nlohmann::json::exception& jsonEx) {
|
||||
LOG("JSON error in /api/tables/characters: %s", jsonEx.what());
|
||||
reply.status = eHTTPStatusCode::BAD_REQUEST;
|
||||
reply.message = "{\"error\":\"Invalid JSON\"}";
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
} catch (std::exception& ex) {
|
||||
LOG("Error in /api/tables/characters: %s", ex.what());
|
||||
reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR;
|
||||
reply.message = "{\"error\":\"Database error\"}";
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/tables/play_keys - Get play keys table data (DataTables.js format)
|
||||
Game::web.RegisterHTTPRoute({
|
||||
.path = "/api/tables/play_keys",
|
||||
.method = eHTTPMethod::POST,
|
||||
.middleware = { std::make_shared<RequireAuthMiddleware>(0) },
|
||||
.handle = [](HTTPReply& reply, const HTTPContext& context) {
|
||||
try { // Only admins (GM > 0) can access table data
|
||||
if (context.gmLevel == 0) {
|
||||
reply.status = eHTTPStatusCode::FORBIDDEN;
|
||||
reply.message = "{\"error\":\"Forbidden - Admin access required\"}";
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
return;
|
||||
}
|
||||
nlohmann::json requestData = nlohmann::json::parse(context.body);
|
||||
|
||||
uint32_t draw = requestData.value("draw", 1);
|
||||
uint32_t start = requestData.value("start", 0);
|
||||
uint32_t length = requestData.value("length", 10);
|
||||
|
||||
std::string search = "";
|
||||
if (requestData.contains("search")) {
|
||||
if (requestData["search"].is_string()) {
|
||||
search = requestData["search"].get<std::string>();
|
||||
} else if (requestData["search"].is_object() && requestData["search"].contains("value")) {
|
||||
search = requestData["search"]["value"].get<std::string>();
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t orderColumn = 0;
|
||||
bool orderAsc = true;
|
||||
|
||||
if (requestData.contains("order") && requestData["order"].is_array() && requestData["order"].size() > 0) {
|
||||
orderColumn = requestData["order"][0].value("column", 0);
|
||||
orderAsc = requestData["order"][0].value("dir", "asc") == "asc";
|
||||
}
|
||||
|
||||
std::string tableData = Database::Get()->GetPlayKeysTable(start, length, search, orderColumn, orderAsc);
|
||||
|
||||
nlohmann::json response = nlohmann::json::parse(tableData);
|
||||
response["draw"] = draw;
|
||||
|
||||
reply.status = eHTTPStatusCode::OK;
|
||||
reply.message = response.dump();
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
} catch (const nlohmann::json::exception& jsonEx) {
|
||||
LOG("JSON error in /api/tables/play_keys: %s", jsonEx.what());
|
||||
reply.status = eHTTPStatusCode::BAD_REQUEST;
|
||||
reply.message = "{\"error\":\"Invalid JSON\"}";
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
} catch (std::exception& ex) {
|
||||
LOG("Error in /api/tables/play_keys: %s", ex.what());
|
||||
reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR;
|
||||
reply.message = "{\"error\":\"Database error\"}";
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/tables/properties - Get properties table data (DataTables.js format)
|
||||
Game::web.RegisterHTTPRoute({
|
||||
.path = "/api/tables/properties",
|
||||
.method = eHTTPMethod::POST,
|
||||
.middleware = { std::make_shared<RequireAuthMiddleware>(0) },
|
||||
.handle = [](HTTPReply& reply, const HTTPContext& context) {
|
||||
try {
|
||||
// Only admins (GM > 0) can access table data
|
||||
if (context.gmLevel == 0) {
|
||||
reply.status = eHTTPStatusCode::FORBIDDEN;
|
||||
reply.message = "{\"error\":\"Forbidden - Admin access required\"}";
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
return;
|
||||
}
|
||||
|
||||
nlohmann::json requestData = nlohmann::json::parse(context.body);
|
||||
|
||||
uint32_t draw = requestData.value("draw", 1);
|
||||
uint32_t start = requestData.value("start", 0);
|
||||
uint32_t length = requestData.value("length", 10);
|
||||
|
||||
std::string search = "";
|
||||
if (requestData.contains("search")) {
|
||||
if (requestData["search"].is_string()) {
|
||||
search = requestData["search"].get<std::string>();
|
||||
} else if (requestData["search"].is_object() && requestData["search"].contains("value")) {
|
||||
search = requestData["search"]["value"].get<std::string>();
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t orderColumn = 0;
|
||||
bool orderAsc = true;
|
||||
|
||||
if (requestData.contains("order") && requestData["order"].is_array() && requestData["order"].size() > 0) {
|
||||
orderColumn = requestData["order"][0].value("column", 0);
|
||||
orderAsc = requestData["order"][0].value("dir", "asc") == "asc";
|
||||
}
|
||||
|
||||
std::string tableData = Database::Get()->GetPropertiesTable(start, length, search, orderColumn, orderAsc);
|
||||
|
||||
nlohmann::json response = nlohmann::json::parse(tableData);
|
||||
response["draw"] = draw;
|
||||
|
||||
reply.status = eHTTPStatusCode::OK;
|
||||
reply.message = response.dump();
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
} catch (const nlohmann::json::exception& jsonEx) {
|
||||
LOG("JSON error in /api/tables/properties: %s", jsonEx.what());
|
||||
reply.status = eHTTPStatusCode::BAD_REQUEST;
|
||||
reply.message = "{\"error\":\"Invalid JSON\"}";
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
} catch (std::exception& ex) {
|
||||
LOG("Error in /api/tables/properties: %s", ex.what());
|
||||
reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR;
|
||||
reply.message = "{\"error\":\"Database error\"}";
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/tables/bug_reports - Get bug reports table data (DataTables.js format)
|
||||
Game::web.RegisterHTTPRoute({
|
||||
.path = "/api/tables/bug_reports",
|
||||
.method = eHTTPMethod::POST,
|
||||
.middleware = { std::make_shared<RequireAuthMiddleware>(0) },
|
||||
.handle = [](HTTPReply& reply, const HTTPContext& context) {
|
||||
try { // Only admins (GM > 0) can access table data
|
||||
if (context.gmLevel == 0) {
|
||||
reply.status = eHTTPStatusCode::FORBIDDEN;
|
||||
reply.message = "{\"error\":\"Forbidden - Admin access required\"}";
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
return;
|
||||
}
|
||||
nlohmann::json requestData = nlohmann::json::parse(context.body);
|
||||
|
||||
uint32_t draw = requestData.value("draw", 1);
|
||||
uint32_t start = requestData.value("start", 0);
|
||||
uint32_t length = requestData.value("length", 10);
|
||||
|
||||
std::string search = "";
|
||||
if (requestData.contains("search")) {
|
||||
if (requestData["search"].is_string()) {
|
||||
search = requestData["search"].get<std::string>();
|
||||
} else if (requestData["search"].is_object() && requestData["search"].contains("value")) {
|
||||
search = requestData["search"]["value"].get<std::string>();
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t orderColumn = 0;
|
||||
bool orderAsc = true;
|
||||
|
||||
if (requestData.contains("order") && requestData["order"].is_array() && requestData["order"].size() > 0) {
|
||||
orderColumn = requestData["order"][0].value("column", 0);
|
||||
orderAsc = requestData["order"][0].value("dir", "asc") == "asc";
|
||||
}
|
||||
|
||||
std::string tableData = Database::Get()->GetBugReportsTable(start, length, search, orderColumn, orderAsc);
|
||||
|
||||
nlohmann::json response = nlohmann::json::parse(tableData);
|
||||
response["draw"] = draw;
|
||||
|
||||
reply.status = eHTTPStatusCode::OK;
|
||||
reply.message = response.dump();
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
} catch (const nlohmann::json::exception& jsonEx) {
|
||||
LOG("JSON error in /api/tables/bug_reports: %s", jsonEx.what());
|
||||
reply.status = eHTTPStatusCode::BAD_REQUEST;
|
||||
reply.message = "{\"error\":\"Invalid JSON\"}";
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
} catch (std::exception& ex) {
|
||||
LOG("Error in /api/tables/bug_reports: %s", ex.what());
|
||||
reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR;
|
||||
reply.message = "{\"error\":\"Database error\"}";
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/accounts/:id - Get single account by ID
|
||||
Game::web.RegisterHTTPRoute({
|
||||
.path = "/api/accounts/:id",
|
||||
.method = eHTTPMethod::GET,
|
||||
.middleware = { std::make_shared<RequireAuthMiddleware>(0) },
|
||||
.handle = [](HTTPReply& reply, const HTTPContext& context) {
|
||||
try {
|
||||
// Extract account ID from URL path
|
||||
const std::string path = context.path;
|
||||
size_t lastSlash = path.rfind('/');
|
||||
if (lastSlash == std::string::npos) {
|
||||
reply.status = eHTTPStatusCode::BAD_REQUEST;
|
||||
reply.message = "{\"error\":\"Invalid account ID\"}";
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
return;
|
||||
}
|
||||
|
||||
std::string idStr = path.substr(lastSlash + 1);
|
||||
uint32_t accountId = 0;
|
||||
try {
|
||||
accountId = std::stoul(idStr);
|
||||
} catch (...) {
|
||||
reply.status = eHTTPStatusCode::BAD_REQUEST;
|
||||
reply.message = "{\"error\":\"Invalid account ID\"}";
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
return;
|
||||
}
|
||||
|
||||
// Permission check: GM 0 can only view own account, GM > 0 can view any account
|
||||
if (context.gmLevel == 0) {
|
||||
// Regular user - get their own account ID
|
||||
auto currentUserInfo = Database::Get()->GetAccountInfo(context.authenticatedUser);
|
||||
if (!currentUserInfo.has_value() || currentUserInfo->id != accountId) {
|
||||
reply.status = eHTTPStatusCode::FORBIDDEN;
|
||||
reply.message = "{\"error\":\"Forbidden - You do not have permission to view this account\"}";
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Get account data
|
||||
nlohmann::json response = Database::Get()->GetAccountById(accountId);
|
||||
reply.status = eHTTPStatusCode::OK;
|
||||
reply.message = response.dump();
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
} catch (const nlohmann::json::exception& jsonEx) {
|
||||
LOG("JSON error in /api/accounts/:id: %s", jsonEx.what());
|
||||
reply.status = eHTTPStatusCode::BAD_REQUEST;
|
||||
reply.message = "{\"error\":\"Invalid JSON\"}";
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
} catch (std::exception& ex) {
|
||||
LOG("Error in /api/accounts/:id: %s", ex.what());
|
||||
reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR;
|
||||
reply.message = "{\"error\":\"Database error\"}";
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
3
dDashboardServer/routes/APIRoutes.h
Normal file
3
dDashboardServer/routes/APIRoutes.h
Normal file
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
void RegisterAPIRoutes();
|
||||
102
dDashboardServer/routes/AuthRoutes.cpp
Normal file
102
dDashboardServer/routes/AuthRoutes.cpp
Normal file
@@ -0,0 +1,102 @@
|
||||
#include "AuthRoutes.h"
|
||||
#include "DashboardAuthService.h"
|
||||
#include "json.hpp"
|
||||
#include "Logger.h"
|
||||
#include "GeneralUtils.h"
|
||||
#include "Web.h"
|
||||
#include "eHTTPMethod.h"
|
||||
#include "HTTPContext.h"
|
||||
|
||||
void RegisterAuthRoutes() {
|
||||
// POST /api/auth/login
|
||||
// Request body: { "username": "string", "password": "string", "rememberMe": boolean }
|
||||
// Response: { "success": boolean, "message": "string", "token": "string", "gmLevel": number }
|
||||
Game::web.RegisterHTTPRoute({
|
||||
.path = "/api/auth/login",
|
||||
.method = eHTTPMethod::POST,
|
||||
.middleware = {},
|
||||
.handle = [](HTTPReply& reply, const HTTPContext& context) {
|
||||
try {
|
||||
auto json = nlohmann::json::parse(context.body);
|
||||
std::string username = json.value("username", "");
|
||||
std::string password = json.value("password", "");
|
||||
bool rememberMe = json.value("rememberMe", false);
|
||||
|
||||
// Validate input
|
||||
if (username.empty() || password.empty()) {
|
||||
reply.message = R"({"success":false,"message":"Username and password are required"})";
|
||||
reply.status = eHTTPStatusCode::BAD_REQUEST;
|
||||
return;
|
||||
}
|
||||
|
||||
if (password.length() > 40) {
|
||||
reply.message = R"({"success":false,"message":"Password exceeds maximum length"})";
|
||||
reply.status = eHTTPStatusCode::BAD_REQUEST;
|
||||
return;
|
||||
}
|
||||
|
||||
// Attempt login
|
||||
auto result = DashboardAuthService::Login(username, password, rememberMe);
|
||||
|
||||
nlohmann::json response;
|
||||
response["success"] = result.success;
|
||||
response["message"] = result.message;
|
||||
if (result.success) {
|
||||
response["token"] = result.token;
|
||||
response["gmLevel"] = result.gmLevel;
|
||||
}
|
||||
|
||||
reply.message = response.dump();
|
||||
reply.status = result.success ? eHTTPStatusCode::OK : eHTTPStatusCode::UNAUTHORIZED;
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
} catch (const std::exception& ex) {
|
||||
LOG("Error processing login request: %s", ex.what());
|
||||
reply.message = R"({"success":false,"message":"Internal server error"})";
|
||||
reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR;
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/auth/verify
|
||||
// Request body: { "token": "string" }
|
||||
// Response: { "valid": boolean, "username": "string", "gmLevel": number }
|
||||
Game::web.RegisterHTTPRoute({
|
||||
.path = "/api/auth/verify",
|
||||
.method = eHTTPMethod::POST,
|
||||
.middleware = {},
|
||||
.handle = [](HTTPReply& reply, const HTTPContext& context) {
|
||||
try {
|
||||
auto json = nlohmann::json::parse(context.body);
|
||||
std::string token = json.value("token", "");
|
||||
|
||||
if (token.empty()) {
|
||||
reply.message = R"({"valid":false})";
|
||||
reply.status = eHTTPStatusCode::BAD_REQUEST;
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
return;
|
||||
}
|
||||
|
||||
std::string username;
|
||||
uint8_t gmLevel{};
|
||||
bool valid = DashboardAuthService::VerifyToken(token, username, gmLevel);
|
||||
|
||||
nlohmann::json response;
|
||||
response["valid"] = valid;
|
||||
if (valid) {
|
||||
response["username"] = username;
|
||||
response["gmLevel"] = gmLevel;
|
||||
}
|
||||
|
||||
reply.message = response.dump();
|
||||
reply.status = eHTTPStatusCode::OK;
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
} catch (const std::exception& ex) {
|
||||
LOG("Error processing verify request: %s", ex.what());
|
||||
reply.message = R"({"valid":false})";
|
||||
reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR;
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
10
dDashboardServer/routes/AuthRoutes.h
Normal file
10
dDashboardServer/routes/AuthRoutes.h
Normal file
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "Web.h"
|
||||
|
||||
/**
|
||||
* Register authentication routes
|
||||
* /api/auth/login - POST login endpoint
|
||||
* /api/auth/verify - POST verify token endpoint
|
||||
*/
|
||||
void RegisterAuthRoutes();
|
||||
30
dDashboardServer/routes/CMakeLists.txt
Normal file
30
dDashboardServer/routes/CMakeLists.txt
Normal file
@@ -0,0 +1,30 @@
|
||||
set(DASHBOARDROUTES_SOURCES
|
||||
"APIRoutes.cpp"
|
||||
"StaticRoutes.cpp"
|
||||
"DashboardRoutes.cpp"
|
||||
"WSRoutes.cpp"
|
||||
"AuthRoutes.cpp"
|
||||
)
|
||||
|
||||
add_library(DashboardRoutes STATIC ${DASHBOARDROUTES_SOURCES})
|
||||
|
||||
target_include_directories(DashboardRoutes PRIVATE
|
||||
"${PROJECT_SOURCE_DIR}/dCommon"
|
||||
"${PROJECT_SOURCE_DIR}/dCommon/dClient"
|
||||
"${PROJECT_SOURCE_DIR}/dCommon/dEnums"
|
||||
"${PROJECT_SOURCE_DIR}/dDatabase"
|
||||
"${PROJECT_SOURCE_DIR}/dDatabase/CDClientDatabase"
|
||||
"${PROJECT_SOURCE_DIR}/dDatabase/CDClientDatabase/CDClientTables"
|
||||
"${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase"
|
||||
"${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase/ITables"
|
||||
"${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase/MySQL"
|
||||
"${PROJECT_SOURCE_DIR}/dNet"
|
||||
"${PROJECT_SOURCE_DIR}/dWeb"
|
||||
"${PROJECT_SOURCE_DIR}/dServer"
|
||||
"${PROJECT_SOURCE_DIR}/thirdparty"
|
||||
"${PROJECT_SOURCE_DIR}/thirdparty/nlohmann"
|
||||
"${PROJECT_SOURCE_DIR}/dDashboardServer/auth"
|
||||
"${PROJECT_SOURCE_DIR}/dDashboardServer/routes"
|
||||
)
|
||||
|
||||
target_link_libraries(DashboardRoutes PRIVATE ${COMMON_LIBRARIES} dWeb dServer)
|
||||
291
dDashboardServer/routes/DashboardRoutes.cpp
Normal file
291
dDashboardServer/routes/DashboardRoutes.cpp
Normal file
@@ -0,0 +1,291 @@
|
||||
#include "DashboardRoutes.h"
|
||||
#include "ServerState.h"
|
||||
#include "Web.h"
|
||||
#include "HTTPContext.h"
|
||||
#include "eHTTPMethod.h"
|
||||
#include "json.hpp"
|
||||
#include "Game.h"
|
||||
#include "Database.h"
|
||||
#include "Logger.h"
|
||||
#include "inja.hpp"
|
||||
#include "AuthMiddleware.h"
|
||||
#include "RequireAuthMiddleware.h"
|
||||
|
||||
void RegisterDashboardRoutes() {
|
||||
// GET / - Main dashboard page (requires authentication)
|
||||
Game::web.RegisterHTTPRoute({
|
||||
.path = "/",
|
||||
.method = eHTTPMethod::GET,
|
||||
.middleware = { std::make_shared<RequireAuthMiddleware>(0) },
|
||||
.handle = [](HTTPReply& reply, const HTTPContext& context) {
|
||||
try {
|
||||
// Initialize inja environment
|
||||
inja::Environment env{"dDashboardServer/templates/"};
|
||||
env.set_trim_blocks(true);
|
||||
env.set_lstrip_blocks(true);
|
||||
|
||||
// Prepare data for template
|
||||
nlohmann::json data = context.GetUserDataJson();
|
||||
|
||||
// Server status - merge with server state
|
||||
nlohmann::json serverState = ServerState::GetServerStateJson();
|
||||
data.merge_patch(serverState);
|
||||
|
||||
// Statistics
|
||||
data["stats"]["onlinePlayers"] = 0; // TODO: Get from server communication
|
||||
data["stats"]["totalAccounts"] = Database::Get()->GetAccountCount();
|
||||
data["stats"]["totalCharacters"] = Database::Get()->GetCharacterCount();
|
||||
|
||||
// Render template
|
||||
const std::string html = env.render_file("index.jinja2", data);
|
||||
|
||||
reply.status = eHTTPStatusCode::OK;
|
||||
reply.message = html;
|
||||
reply.contentType = eContentType::TEXT_HTML;
|
||||
} catch (const std::exception& ex) {
|
||||
LOG("Error rendering template: %s", ex.what());
|
||||
reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR;
|
||||
reply.message = "{\"error\":\"Failed to render template\"}";
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// GET /login - Login page (no authentication required)
|
||||
Game::web.RegisterHTTPRoute({
|
||||
.path = "/login",
|
||||
.method = eHTTPMethod::GET,
|
||||
.middleware = {},
|
||||
.handle = [](HTTPReply& reply, const HTTPContext& context) {
|
||||
try {
|
||||
// Initialize inja environment
|
||||
inja::Environment env{"dDashboardServer/templates/"};
|
||||
env.set_trim_blocks(true);
|
||||
env.set_lstrip_blocks(true);
|
||||
|
||||
// Render template with empty user data (not authenticated)
|
||||
nlohmann::json data = context.GetUserDataJson();
|
||||
const std::string html = env.render_file("login.jinja2", data);
|
||||
|
||||
reply.status = eHTTPStatusCode::OK;
|
||||
reply.message = html;
|
||||
reply.contentType = eContentType::TEXT_HTML;
|
||||
} catch (const std::exception& ex) {
|
||||
LOG("Error rendering login template: %s", ex.what());
|
||||
reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR;
|
||||
reply.message = "{\"error\":\"Failed to render login page\"}";
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// GET /accounts/:id - View single account
|
||||
Game::web.RegisterHTTPRoute({
|
||||
.path = "/accounts/:id",
|
||||
.method = eHTTPMethod::GET,
|
||||
.middleware = { std::make_shared<RequireAuthMiddleware>(0) },
|
||||
.handle = [](HTTPReply& reply, const HTTPContext& context) {
|
||||
try {
|
||||
// Extract account ID from URL path
|
||||
const std::string path = context.path;
|
||||
size_t lastSlash = path.rfind('/');
|
||||
if (lastSlash == std::string::npos) {
|
||||
reply.status = eHTTPStatusCode::NOT_FOUND;
|
||||
reply.message = "<h1>404 - Account not found</h1>";
|
||||
reply.contentType = eContentType::TEXT_HTML;
|
||||
return;
|
||||
}
|
||||
|
||||
std::string idStr = path.substr(lastSlash + 1);
|
||||
uint32_t accountId = 0;
|
||||
try {
|
||||
accountId = std::stoul(idStr);
|
||||
} catch (...) {
|
||||
reply.status = eHTTPStatusCode::NOT_FOUND;
|
||||
reply.message = "<h1>404 - Invalid account ID</h1>";
|
||||
reply.contentType = eContentType::TEXT_HTML;
|
||||
return;
|
||||
}
|
||||
|
||||
// Permission check: GM 0 can only view own account, GM > 0 can view any account
|
||||
if (context.gmLevel == 0) {
|
||||
LOG("Regular user '%s' (GM level 0) is trying to access account ID %u", context.authenticatedUser.c_str(), accountId);
|
||||
// Regular user - get their own account ID
|
||||
auto currentUserInfo = Database::Get()->GetAccountInfo(context.authenticatedUser);
|
||||
if (!currentUserInfo.has_value() || currentUserInfo->id != accountId) {
|
||||
LOG("Permission denied: user '%s' cannot access account ID %u", context.authenticatedUser.c_str(), accountId);
|
||||
reply.status = eHTTPStatusCode::FORBIDDEN;
|
||||
reply.message = "<h1>403 - Forbidden</h1><p>You do not have permission to view this account.</p>";
|
||||
reply.contentType = eContentType::TEXT_HTML;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Get account data from API
|
||||
nlohmann::json account = Database::Get()->GetAccountById(accountId);
|
||||
|
||||
// Check if account was found
|
||||
if (account.contains("error")) {
|
||||
reply.status = eHTTPStatusCode::NOT_FOUND;
|
||||
reply.message = "<h1>404 - Account not found</h1>";
|
||||
reply.contentType = eContentType::TEXT_HTML;
|
||||
return;
|
||||
}
|
||||
// Initialize inja environment
|
||||
inja::Environment env{"dDashboardServer/templates/"};
|
||||
env.set_trim_blocks(true);
|
||||
env.set_lstrip_blocks(true);
|
||||
|
||||
// Prepare data for template
|
||||
nlohmann::json data = context.GetUserDataJson();
|
||||
data["account"] = account;
|
||||
|
||||
// Render template
|
||||
const std::string html = env.render_file("account-view.jinja2", data);
|
||||
|
||||
reply.status = eHTTPStatusCode::OK;
|
||||
reply.message = html;
|
||||
reply.contentType = eContentType::TEXT_HTML;
|
||||
} catch (const std::exception& ex) {
|
||||
LOG("Error rendering account view template: %s", ex.what());
|
||||
reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR;
|
||||
reply.message = "<h1>500 - Server Error</h1>";
|
||||
reply.contentType = eContentType::TEXT_HTML;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// GET /accounts - Accounts management page
|
||||
Game::web.RegisterHTTPRoute({
|
||||
.path = "/accounts",
|
||||
.method = eHTTPMethod::GET,
|
||||
.middleware = { std::make_shared<RequireAuthMiddleware>(1) },
|
||||
.handle = [](HTTPReply& reply, const HTTPContext& context) {
|
||||
try {
|
||||
// Initialize inja environment
|
||||
inja::Environment env{"dDashboardServer/templates/"};
|
||||
env.set_trim_blocks(true);
|
||||
env.set_lstrip_blocks(true);
|
||||
|
||||
// Prepare data for template
|
||||
nlohmann::json data = context.GetUserDataJson();
|
||||
|
||||
// Render template
|
||||
const std::string html = env.render_file("accounts.jinja2", data);
|
||||
|
||||
reply.status = eHTTPStatusCode::OK;
|
||||
reply.message = html;
|
||||
reply.contentType = eContentType::TEXT_HTML;
|
||||
} catch (const std::exception& ex) {
|
||||
LOG("Error rendering accounts template: %s", ex.what());
|
||||
reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR;
|
||||
reply.message = "{\"error\":\"Failed to render accounts page\"}";
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// GET /characters - Characters management page
|
||||
Game::web.RegisterHTTPRoute({
|
||||
.path = "/characters",
|
||||
.method = eHTTPMethod::GET,
|
||||
.middleware = { std::make_shared<RequireAuthMiddleware>(1) },
|
||||
.handle = [](HTTPReply& reply, const HTTPContext& context) {
|
||||
try {
|
||||
inja::Environment env{"dDashboardServer/templates/"};
|
||||
env.set_trim_blocks(true);
|
||||
env.set_lstrip_blocks(true);
|
||||
|
||||
nlohmann::json data = context.GetUserDataJson();
|
||||
const std::string html = env.render_file("characters.jinja2", data);
|
||||
|
||||
reply.status = eHTTPStatusCode::OK;
|
||||
reply.message = html;
|
||||
reply.contentType = eContentType::TEXT_HTML;
|
||||
} catch (const std::exception& ex) {
|
||||
LOG("Error rendering characters template: %s", ex.what());
|
||||
reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR;
|
||||
reply.message = "{\"error\":\"Failed to render characters page\"}";
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// GET /play_keys - Play keys management page
|
||||
Game::web.RegisterHTTPRoute({
|
||||
.path = "/play_keys",
|
||||
.method = eHTTPMethod::GET,
|
||||
.middleware = { std::make_shared<RequireAuthMiddleware>(1) },
|
||||
.handle = [](HTTPReply& reply, const HTTPContext& context) {
|
||||
try {
|
||||
inja::Environment env{"dDashboardServer/templates/"};
|
||||
env.set_trim_blocks(true);
|
||||
env.set_lstrip_blocks(true);
|
||||
|
||||
nlohmann::json data = context.GetUserDataJson();
|
||||
const std::string html = env.render_file("play_keys.jinja2", data);
|
||||
|
||||
reply.status = eHTTPStatusCode::OK;
|
||||
reply.message = html;
|
||||
reply.contentType = eContentType::TEXT_HTML;
|
||||
} catch (const std::exception& ex) {
|
||||
LOG("Error rendering play_keys template: %s", ex.what());
|
||||
reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR;
|
||||
reply.message = "{\"error\":\"Failed to render play_keys page\"}";
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// GET /properties - Properties management page
|
||||
Game::web.RegisterHTTPRoute({
|
||||
.path = "/properties",
|
||||
.method = eHTTPMethod::GET,
|
||||
.middleware = { std::make_shared<RequireAuthMiddleware>(1) },
|
||||
.handle = [](HTTPReply& reply, const HTTPContext& context) {
|
||||
try {
|
||||
inja::Environment env{"dDashboardServer/templates/"};
|
||||
env.set_trim_blocks(true);
|
||||
env.set_lstrip_blocks(true);
|
||||
|
||||
nlohmann::json data = context.GetUserDataJson();
|
||||
const std::string html = env.render_file("properties.jinja2", data);
|
||||
|
||||
reply.status = eHTTPStatusCode::OK;
|
||||
reply.message = html;
|
||||
reply.contentType = eContentType::TEXT_HTML;
|
||||
} catch (const std::exception& ex) {
|
||||
LOG("Error rendering properties template: %s", ex.what());
|
||||
reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR;
|
||||
reply.message = "{\"error\":\"Failed to render properties page\"}";
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// GET /bug_reports - Bug reports management page
|
||||
Game::web.RegisterHTTPRoute({
|
||||
.path = "/bug_reports",
|
||||
.method = eHTTPMethod::GET,
|
||||
.middleware = { std::make_shared<RequireAuthMiddleware>(1) },
|
||||
.handle = [](HTTPReply& reply, const HTTPContext& context) {
|
||||
try {
|
||||
inja::Environment env{"dDashboardServer/templates/"};
|
||||
env.set_trim_blocks(true);
|
||||
env.set_lstrip_blocks(true);
|
||||
|
||||
nlohmann::json data = context.GetUserDataJson();
|
||||
const std::string html = env.render_file("bug_reports.jinja2", data);
|
||||
|
||||
reply.status = eHTTPStatusCode::OK;
|
||||
reply.message = html;
|
||||
reply.contentType = eContentType::TEXT_HTML;
|
||||
} catch (const std::exception& ex) {
|
||||
LOG("Error rendering bug_reports template: %s", ex.what());
|
||||
reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR;
|
||||
reply.message = "{\"error\":\"Failed to render bug_reports page\"}";
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
7
dDashboardServer/routes/DashboardRoutes.h
Normal file
7
dDashboardServer/routes/DashboardRoutes.h
Normal file
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "json.hpp"
|
||||
|
||||
class HTTPContext;
|
||||
|
||||
void RegisterDashboardRoutes();
|
||||
52
dDashboardServer/routes/ServerState.h
Normal file
52
dDashboardServer/routes/ServerState.h
Normal file
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <cstdint>
|
||||
#include "json.hpp"
|
||||
|
||||
struct ServerStatus {
|
||||
bool online{false};
|
||||
uint32_t players{0};
|
||||
std::string version{};
|
||||
std::chrono::steady_clock::time_point lastSeen{};
|
||||
};
|
||||
|
||||
struct WorldInstanceInfo {
|
||||
uint32_t mapID{0};
|
||||
uint32_t instanceID{0};
|
||||
uint32_t cloneID{0};
|
||||
uint32_t players{0};
|
||||
std::string ip{};
|
||||
uint32_t port{0};
|
||||
bool isPrivate{false};
|
||||
};
|
||||
|
||||
namespace ServerState {
|
||||
extern ServerStatus g_AuthStatus;
|
||||
extern ServerStatus g_ChatStatus;
|
||||
extern std::vector<WorldInstanceInfo> g_WorldInstances;
|
||||
|
||||
// Helper function to get all server state as JSON
|
||||
inline nlohmann::json GetServerStateJson() {
|
||||
nlohmann::json data;
|
||||
data["auth"]["online"] = g_AuthStatus.online;
|
||||
data["auth"]["players"] = g_AuthStatus.players;
|
||||
data["chat"]["online"] = g_ChatStatus.online;
|
||||
data["chat"]["players"] = g_ChatStatus.players;
|
||||
|
||||
data["worlds"] = nlohmann::json::array();
|
||||
for (const auto& world : g_WorldInstances) {
|
||||
data["worlds"].push_back({
|
||||
{"mapID", world.mapID},
|
||||
{"instanceID", world.instanceID},
|
||||
{"cloneID", world.cloneID},
|
||||
{"players", world.players},
|
||||
{"isPrivate", world.isPrivate}
|
||||
});
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
68
dDashboardServer/routes/StaticRoutes.cpp
Normal file
68
dDashboardServer/routes/StaticRoutes.cpp
Normal file
@@ -0,0 +1,68 @@
|
||||
#include "StaticRoutes.h"
|
||||
#include "Web.h"
|
||||
#include "HTTPContext.h"
|
||||
#include "eHTTPMethod.h"
|
||||
#include "Game.h"
|
||||
#include "Logger.h"
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
namespace {
|
||||
std::string ReadFileToString(const std::string& filePath) {
|
||||
std::ifstream file(filePath);
|
||||
if (!file.is_open()) {
|
||||
LOG("Failed to open file: %s", filePath.c_str());
|
||||
return "";
|
||||
}
|
||||
std::stringstream buffer{};
|
||||
buffer << file.rdbuf();
|
||||
return buffer.str();
|
||||
}
|
||||
|
||||
eContentType GetContentType(const std::string& filePath) {
|
||||
if (filePath.ends_with(".css")) {
|
||||
return eContentType::TEXT_CSS;
|
||||
} else if (filePath.ends_with(".js")) {
|
||||
return eContentType::TEXT_JAVASCRIPT;
|
||||
} else if (filePath.ends_with(".html")) {
|
||||
return eContentType::TEXT_HTML;
|
||||
} else if (filePath.ends_with(".png")) {
|
||||
return eContentType::IMAGE_PNG;
|
||||
} else if (filePath.ends_with(".jpg") || filePath.ends_with(".jpeg")) {
|
||||
return eContentType::IMAGE_JPEG;
|
||||
} else if (filePath.ends_with(".json")) {
|
||||
return eContentType::APPLICATION_JSON;
|
||||
}
|
||||
return eContentType::TEXT_PLAIN;
|
||||
}
|
||||
|
||||
void ServeStaticFile(const std::string& urlPath, const std::string& filePath) {
|
||||
Game::web.RegisterHTTPRoute({
|
||||
.path = urlPath,
|
||||
.method = eHTTPMethod::GET,
|
||||
.middleware = {},
|
||||
.handle = [filePath](HTTPReply& reply, const HTTPContext& context) {
|
||||
const std::string content = ReadFileToString(filePath);
|
||||
if (content.empty()) {
|
||||
reply.status = eHTTPStatusCode::NOT_FOUND;
|
||||
reply.message = "{\"error\":\"File not found\"}";
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
} else {
|
||||
reply.status = eHTTPStatusCode::OK;
|
||||
reply.message = content;
|
||||
reply.contentType = GetContentType(filePath);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void RegisterStaticRoutes() {
|
||||
// Serve CSS files
|
||||
ServeStaticFile("/css/dashboard.css", "dDashboardServer/static/css/dashboard.css");
|
||||
ServeStaticFile("/css/login.css", "dDashboardServer/static/css/login.css");
|
||||
|
||||
// Serve JavaScript files
|
||||
ServeStaticFile("/js/dashboard.js", "dDashboardServer/static/js/dashboard.js");
|
||||
ServeStaticFile("/js/login.js", "dDashboardServer/static/js/login.js");
|
||||
}
|
||||
3
dDashboardServer/routes/StaticRoutes.h
Normal file
3
dDashboardServer/routes/StaticRoutes.h
Normal file
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
void RegisterStaticRoutes();
|
||||
35
dDashboardServer/routes/WSRoutes.cpp
Normal file
35
dDashboardServer/routes/WSRoutes.cpp
Normal file
@@ -0,0 +1,35 @@
|
||||
#include "WSRoutes.h"
|
||||
#include "ServerState.h"
|
||||
#include "Web.h"
|
||||
#include "json.hpp"
|
||||
#include "Game.h"
|
||||
#include "Database.h"
|
||||
#include "Logger.h"
|
||||
|
||||
void RegisterWSRoutes() {
|
||||
// Register WebSocket subscriptions for real-time updates
|
||||
Game::web.RegisterWSSubscription("dashboard_update");
|
||||
Game::web.RegisterWSSubscription("server_status");
|
||||
Game::web.RegisterWSSubscription("player_joined");
|
||||
Game::web.RegisterWSSubscription("player_left");
|
||||
|
||||
// dashboard_update: Broadcasts complete dashboard data every 2 seconds
|
||||
// Other subscriptions can be triggered by events from the master server
|
||||
}
|
||||
|
||||
void BroadcastDashboardUpdate() {
|
||||
// Get server state data (auth, chat, worlds) - mutex is acquired internally
|
||||
nlohmann::json data = ServerState::GetServerStateJson();
|
||||
|
||||
// Add statistics
|
||||
try {
|
||||
data["stats"]["onlinePlayers"] = 0; // TODO: Get from server communication
|
||||
data["stats"]["totalAccounts"] = Database::Get()->GetAccountCount();
|
||||
data["stats"]["totalCharacters"] = Database::Get()->GetCharacterCount();
|
||||
} catch (const std::exception& ex) {
|
||||
LOG_DEBUG("Error getting stats: %s", ex.what());
|
||||
}
|
||||
|
||||
// Broadcast to all connected WebSocket clients subscribed to "dashboard_update"
|
||||
Game::web.SendWSMessage("dashboard_update", data);
|
||||
}
|
||||
4
dDashboardServer/routes/WSRoutes.h
Normal file
4
dDashboardServer/routes/WSRoutes.h
Normal file
@@ -0,0 +1,4 @@
|
||||
#pragma once
|
||||
|
||||
void RegisterWSRoutes();
|
||||
void BroadcastDashboardUpdate();
|
||||
495
dDashboardServer/static/css/dashboard.css
Normal file
495
dDashboardServer/static/css/dashboard.css
Normal file
@@ -0,0 +1,495 @@
|
||||
/* Minimal custom styling - mostly Bootstrap5 utilities */
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body.d-flex.bg-dark.text-white {
|
||||
background-color: #0d0d0d;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Sidebar adjustments */
|
||||
.navbar.flex-column {
|
||||
box-shadow: 0.125rem 0 0.25rem rgba(0, 0, 0, 0.075);
|
||||
}
|
||||
|
||||
.navbar.flex-column .navbar-nav {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.navbar.flex-column .nav-link {
|
||||
padding: 0.75rem 1.25rem;
|
||||
border-left: 3px solid transparent;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.navbar.flex-column .nav-link:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
border-left-color: #667eea;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.navbar.flex-column .nav-link.active {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
border-left-color: #667eea;
|
||||
}
|
||||
|
||||
main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 991.98px) {
|
||||
body {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
main {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
.navbar.flex-column {
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
position: relative !important;
|
||||
top: auto !important;
|
||||
start: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar {
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
}
|
||||
|
||||
.username {
|
||||
font-weight: 600;
|
||||
color: #667eea;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
padding: 10px 20px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.logout-btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stat {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.stat:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.status {
|
||||
display: inline-block;
|
||||
padding: 5px 15px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.9em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status.online {
|
||||
background: #4caf50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status.offline {
|
||||
background: #f44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.world-list {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.world-item {
|
||||
padding: 15px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.world-item h3 {
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.world-detail {
|
||||
color: #666;
|
||||
font-size: 0.9em;
|
||||
margin: 3px 0;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* Dark theme for data tables and containers */
|
||||
|
||||
/* Container margin for sidebar layout */
|
||||
.account-view-container,
|
||||
.accounts-container,
|
||||
.characters-container,
|
||||
.play-keys-container,
|
||||
.properties-container,
|
||||
.bug-reports-container {
|
||||
margin-left: 280px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* Table card styling */
|
||||
.table-card {
|
||||
background-color: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.table-header {
|
||||
background-color: #222;
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid #333;
|
||||
border-radius: 0.5rem 0.5rem 0 0;
|
||||
}
|
||||
|
||||
.table-header h2 {
|
||||
margin: 0;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.table-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
/* Bootstrap card overrides for dark theme */
|
||||
.card {
|
||||
background-color: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background-color: #222;
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid #333;
|
||||
border-radius: 0.5rem 0.5rem 0 0;
|
||||
}
|
||||
|
||||
.card-header h2 {
|
||||
margin: 0;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
/* Table styling */
|
||||
.table-dark {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.table-dark thead {
|
||||
background-color: #2a2a2a;
|
||||
}
|
||||
|
||||
.table-dark thead th {
|
||||
border-bottom: 2px solid #444;
|
||||
color: #aaa;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.875rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.table-dark tbody td {
|
||||
padding: 0.875rem 1rem;
|
||||
border-bottom: 1px solid #333;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.table-dark tbody tr:hover {
|
||||
background-color: #252525;
|
||||
}
|
||||
|
||||
/* DataTables customization */
|
||||
.dataTables_wrapper {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.dataTables_wrapper .dataTables_filter input {
|
||||
background-color: #2a2a2a;
|
||||
color: #fff;
|
||||
border: 1px solid #444;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.4rem 0.6rem;
|
||||
}
|
||||
|
||||
.dataTables_wrapper .dataTables_filter input::placeholder {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.dataTables_wrapper .dataTables_info {
|
||||
color: #aaa;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.dataTables_wrapper .dataTables_paginate .paginate_button {
|
||||
background: #2a2a2a;
|
||||
color: #fff;
|
||||
border: 1px solid #444;
|
||||
margin: 0 2px;
|
||||
padding: 0.4rem 0.8rem;
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.dataTables_wrapper .dataTables_paginate .paginate_button:hover {
|
||||
background: #3a3a3a;
|
||||
border: 1px solid #555;
|
||||
}
|
||||
|
||||
.dataTables_wrapper .dataTables_paginate .paginate_button.current {
|
||||
background: #0d6efd;
|
||||
border: 1px solid #0d6efd;
|
||||
}
|
||||
|
||||
.dataTables_wrapper .dataTables_paginate .paginate_button.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.dataTables_wrapper .dataTables_length select {
|
||||
background-color: #2a2a2a;
|
||||
color: #fff;
|
||||
border: 1px solid #444;
|
||||
padding: 0.4rem 0.6rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
/* Detail grid layout */
|
||||
.detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
background-color: #0a0a0a;
|
||||
padding: 1rem;
|
||||
border-radius: 0.25rem;
|
||||
border-left: 3px solid #0d6efd;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
color: #999;
|
||||
font-size: 0.875rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Badge styling */
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 0.35rem 0.6rem;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.badge-active {
|
||||
background-color: #28a745;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.badge-inactive {
|
||||
background-color: #6c757d;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.badge-banned {
|
||||
background-color: #dc3545;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.badge-locked {
|
||||
background-color: #ffc107;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.badge-gm {
|
||||
background-color: #17a2b8;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.badge-approved {
|
||||
background-color: #28a745;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.badge-pending {
|
||||
background-color: #ffc107;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/* Button styling */
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.25rem;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #0d6efd;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #0b5ed7;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: #6c757d;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: #5c636a;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: #dc3545;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background-color: #c82333;
|
||||
}
|
||||
|
||||
.btn-info {
|
||||
background-color: #17a2b8;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-info:hover {
|
||||
background-color: #138496;
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background-color: #ffc107;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.btn-warning:hover {
|
||||
background-color: #e0a800;
|
||||
}
|
||||
|
||||
/* Action buttons */
|
||||
.account-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Utility classes */
|
||||
.text-muted {
|
||||
color: #999 !important;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
color: #0d6efd;
|
||||
text-decoration: none;
|
||||
margin-bottom: 1rem;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.back-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.report-preview {
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.search-section {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
background-color: #2a2a2a;
|
||||
border: 1px solid #444;
|
||||
color: #fff;
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: #888;
|
||||
}
|
||||
30
dDashboardServer/static/css/login.css
Normal file
30
dDashboardServer/static/css/login.css
Normal file
@@ -0,0 +1,30 @@
|
||||
/* Custom styling for login page on top of Bootstrap5 */
|
||||
|
||||
body {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
}
|
||||
|
||||
.card {
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2) !important;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: linear-gradient(135deg, #5568d3 0%, #6a3f93 100%);
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
240
dDashboardServer/static/js/dashboard.js
Normal file
240
dDashboardServer/static/js/dashboard.js
Normal file
@@ -0,0 +1,240 @@
|
||||
let ws = null;
|
||||
let reconnectAttempts = 0;
|
||||
const maxReconnectAttempts = 5;
|
||||
const reconnectDelay = 3000;
|
||||
|
||||
// Helper function to get cookie value
|
||||
function getCookie(name) {
|
||||
const nameEQ = name + '=';
|
||||
const cookies = document.cookie.split(';');
|
||||
for (let cookie of cookies) {
|
||||
cookie = cookie.trim();
|
||||
if (cookie.indexOf(nameEQ) === 0) {
|
||||
return decodeURIComponent(cookie.substring(nameEQ.length));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Helper function to delete cookie
|
||||
function deleteCookie(name) {
|
||||
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; SameSite=Strict`;
|
||||
}
|
||||
|
||||
// Check authentication on page load
|
||||
function checkAuthentication() {
|
||||
// Check localStorage first (most secure)
|
||||
let token = localStorage.getItem('dashboardToken');
|
||||
|
||||
// Fallback to cookie if localStorage empty
|
||||
if (!token) {
|
||||
token = getCookie('dashboardToken');
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
// Redirect to login if no token
|
||||
window.location.href = '/login';
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify token is valid (asynchronous)
|
||||
fetch('/api/auth/verify', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token: token })
|
||||
})
|
||||
.then(res => {
|
||||
if (!res.ok) {
|
||||
console.error('Verify endpoint returned:', res.status);
|
||||
throw new Error(`HTTP ${res.status}`);
|
||||
}
|
||||
return res.json();
|
||||
})
|
||||
.then(data => {
|
||||
console.log('Token verification response:', data);
|
||||
if (!data.valid) {
|
||||
// Token is invalid/expired, delete cookies and redirect to login
|
||||
console.log('Token verification failed, redirecting to login');
|
||||
deleteCookie('dashboardToken');
|
||||
deleteCookie('gmLevel');
|
||||
localStorage.removeItem('dashboardToken');
|
||||
window.location.href = '/login';
|
||||
} else {
|
||||
// Update UI with username
|
||||
console.log('Token verified, user:', data.username);
|
||||
const usernameElement = document.querySelector('.username');
|
||||
if (usernameElement) {
|
||||
usernameElement.textContent = data.username || 'User';
|
||||
} else {
|
||||
console.warn('Username element not found in DOM');
|
||||
}
|
||||
// Now that verification is complete, connect to WebSocket
|
||||
setTimeout(() => {
|
||||
console.log('Starting WebSocket connection');
|
||||
connectWebSocket();
|
||||
}, 100);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Token verification error:', err);
|
||||
// Network error - log but don't redirect immediately
|
||||
// This prevents redirect loops on network issues
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get token from localStorage or cookie
|
||||
function getAuthToken() {
|
||||
let token = localStorage.getItem('dashboardToken');
|
||||
if (!token) {
|
||||
token = getCookie('dashboardToken');
|
||||
}
|
||||
console.log('getAuthToken called, token available:', !!token);
|
||||
return token;
|
||||
}
|
||||
|
||||
// Logout function
|
||||
function logout() {
|
||||
deleteCookie('dashboardToken');
|
||||
deleteCookie('gmLevel');
|
||||
localStorage.removeItem('dashboardToken');
|
||||
window.location.href = '/login';
|
||||
}
|
||||
|
||||
function connectWebSocket() {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const token = getAuthToken();
|
||||
if (!token) {
|
||||
console.error('No token available for WebSocket connection');
|
||||
window.location.href = '/login';
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`WebSocket connection attempt ${reconnectAttempts + 1}/${maxReconnectAttempts}`);
|
||||
|
||||
// Connect to WebSocket without token in URL (token is in cookies)
|
||||
const wsUrl = `${protocol}//${window.location.host}/ws`;
|
||||
console.log(`Connecting to WebSocket: ${wsUrl}`);
|
||||
|
||||
try {
|
||||
ws = new WebSocket(wsUrl);
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('WebSocket connected');
|
||||
reconnectAttempts = 0;
|
||||
|
||||
// Subscribe to dashboard updates
|
||||
ws.send(JSON.stringify({
|
||||
event: 'subscribe',
|
||||
subscription: 'dashboard_update'
|
||||
}));
|
||||
|
||||
document.getElementById('connection-status')?.remove();
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
// Handle subscription confirmation
|
||||
if (data.subscribed) {
|
||||
console.log('Subscribed to:', data.subscribed);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle dashboard updates
|
||||
if (data.event === 'dashboard_update') {
|
||||
updateDashboard(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing WebSocket message:', error);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log('WebSocket disconnected');
|
||||
ws = null;
|
||||
|
||||
// Show connection status
|
||||
showConnectionStatus('Disconnected - Attempting to reconnect...');
|
||||
|
||||
// Attempt to reconnect with exponential backoff
|
||||
if (reconnectAttempts < maxReconnectAttempts) {
|
||||
reconnectAttempts++;
|
||||
const backoffDelay = reconnectDelay * Math.pow(2, reconnectAttempts - 1);
|
||||
console.log(`Reconnecting in ${backoffDelay}ms (attempt ${reconnectAttempts}/${maxReconnectAttempts})`);
|
||||
setTimeout(connectWebSocket, backoffDelay);
|
||||
} else {
|
||||
console.error('Max reconnection attempts reached');
|
||||
showConnectionStatus('Connection lost - Reload page to reconnect');
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to create WebSocket:', error);
|
||||
showConnectionStatus('Failed to connect - Reload page to retry');
|
||||
}
|
||||
}
|
||||
|
||||
function showConnectionStatus(message) {
|
||||
let statusEl = document.getElementById('connection-status');
|
||||
if (!statusEl) {
|
||||
statusEl = document.createElement('div');
|
||||
statusEl.id = 'connection-status';
|
||||
statusEl.style.cssText = 'position: fixed; top: 10px; right: 10px; background: #f44336; color: white; padding: 10px 20px; border-radius: 4px; z-index: 1000;';
|
||||
document.body.appendChild(statusEl);
|
||||
}
|
||||
statusEl.textContent = message;
|
||||
}
|
||||
|
||||
function updateDashboard(data) {
|
||||
// Update server status
|
||||
if (data.auth) {
|
||||
document.getElementById('auth-status').textContent = data.auth.online ? 'Online' : 'Offline';
|
||||
document.getElementById('auth-status').className = 'status ' + (data.auth.online ? 'online' : 'offline');
|
||||
}
|
||||
|
||||
if (data.chat) {
|
||||
document.getElementById('chat-status').textContent = data.chat.online ? 'Online' : 'Offline';
|
||||
document.getElementById('chat-status').className = 'status ' + (data.chat.online ? 'online' : 'offline');
|
||||
}
|
||||
|
||||
// Update world instances
|
||||
if (data.worlds) {
|
||||
document.getElementById('world-count').textContent = data.worlds.length;
|
||||
|
||||
const worldList = document.getElementById('world-list');
|
||||
if (data.worlds.length === 0) {
|
||||
worldList.innerHTML = '<div class="loading">No active world instances</div>';
|
||||
} else {
|
||||
worldList.innerHTML = data.worlds.map(world => `
|
||||
<div class="world-item">
|
||||
<h3>Zone ${world.mapID} - Instance ${world.instanceID}</h3>
|
||||
<div class="world-detail">Clone ID: ${world.cloneID}</div>
|
||||
<div class="world-detail">Players: ${world.players}</div>
|
||||
<div class="world-detail">Type: ${world.isPrivate ? 'Private' : 'Public'}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
}
|
||||
|
||||
// Update statistics
|
||||
if (data.stats) {
|
||||
if (data.stats.onlinePlayers !== undefined) {
|
||||
document.getElementById('online-players').textContent = data.stats.onlinePlayers;
|
||||
}
|
||||
if (data.stats.totalAccounts !== undefined) {
|
||||
document.getElementById('total-accounts').textContent = data.stats.totalAccounts;
|
||||
}
|
||||
if (data.stats.totalCharacters !== undefined) {
|
||||
document.getElementById('total-characters').textContent = data.stats.totalCharacters;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Connect on page load
|
||||
connectWebSocket();
|
||||
99
dDashboardServer/static/js/login.js
Normal file
99
dDashboardServer/static/js/login.js
Normal file
@@ -0,0 +1,99 @@
|
||||
// Check if user is already logged in
|
||||
function checkExistingToken() {
|
||||
const token = localStorage.getItem('dashboardToken');
|
||||
if (token) {
|
||||
verifyTokenAndRedirect(token);
|
||||
}
|
||||
}
|
||||
|
||||
function verifyTokenAndRedirect(token) {
|
||||
fetch('/api/auth/verify', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token: token })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.valid) {
|
||||
window.location.href = '/';
|
||||
}
|
||||
})
|
||||
.catch(err => console.error('Token verification failed:', err));
|
||||
}
|
||||
|
||||
function showAlert(message, type) {
|
||||
const alert = document.getElementById('alert');
|
||||
alert.textContent = message;
|
||||
alert.className = 'alert';
|
||||
if (type === 'error') {
|
||||
alert.classList.add('alert-danger');
|
||||
} else if (type === 'success') {
|
||||
alert.classList.add('alert-success');
|
||||
}
|
||||
alert.style.display = 'block';
|
||||
}
|
||||
|
||||
// Wait for DOM to be ready
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const loginForm = document.getElementById('loginForm');
|
||||
if (!loginForm) {
|
||||
console.error('Login form not found');
|
||||
return;
|
||||
}
|
||||
|
||||
loginForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const username = document.getElementById('username').value;
|
||||
const password = document.getElementById('password').value;
|
||||
const rememberMe = document.getElementById('rememberMe').checked;
|
||||
|
||||
// Validate input
|
||||
if (!username || !password) {
|
||||
showAlert('Username and password are required', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (password.length > 40) {
|
||||
showAlert('Password exceeds maximum length (40 characters)', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
document.getElementById('loading').style.display = 'inline-block';
|
||||
document.getElementById('loginBtn').disabled = true;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password, rememberMe })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
// Store token in localStorage (also set as cookie for API calls)
|
||||
localStorage.setItem('dashboardToken', data.token);
|
||||
document.cookie = `dashboardToken=${data.token}; path=/; SameSite=Strict`;
|
||||
showAlert('Login successful! Redirecting...', 'success');
|
||||
|
||||
// Redirect after a short delay (no token in URL)
|
||||
setTimeout(() => {
|
||||
window.location.href = '/';
|
||||
}, 1000);
|
||||
} else {
|
||||
showAlert(data.message || 'Login failed', 'error');
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
document.getElementById('loginBtn').disabled = false;
|
||||
}
|
||||
} catch (error) {
|
||||
showAlert('Network error: ' + error.message, 'error');
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
document.getElementById('loginBtn').disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Check existing token on page load
|
||||
checkExistingToken();
|
||||
});
|
||||
137
dDashboardServer/templates/account-view.jinja2
Normal file
137
dDashboardServer/templates/account-view.jinja2
Normal file
@@ -0,0 +1,137 @@
|
||||
{% extends "base.jinja2" %}
|
||||
|
||||
{% block title %}Account - DarkflameServer{% endblock %}
|
||||
|
||||
{% block css %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="account-view-container">
|
||||
<div class="container-fluid">
|
||||
<a href="/accounts" class="back-link">← Back to Accounts</a>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>Account #{{ account.id }} - {{ account.name }}</h2>
|
||||
<p class="text-muted">View account details and manage settings</p>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="detail-grid">
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">Account ID</div>
|
||||
<div class="detail-value">{{ account.id }}</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">Username</div>
|
||||
<div class="detail-value">{{ account.name }}</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">Created</div>
|
||||
<div class="detail-value">{{ account.created_at }}</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">GM Level</div>
|
||||
<div class="detail-value">
|
||||
{% if account.gm_level > 0 %}
|
||||
<span class="badge badge-gm">GM {{ account.gm_level }}</span>
|
||||
{% else %}
|
||||
<span class="badge badge-inactive">User</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">Ban Status</div>
|
||||
<div class="detail-value">
|
||||
{% if account.banned %}
|
||||
<span class="badge badge-banned">BANNED</span>
|
||||
{% else %}
|
||||
<span class="badge badge-active">Active</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">Lock Status</div>
|
||||
<div class="detail-value">
|
||||
{% if account.locked %}
|
||||
<span class="badge badge-locked">LOCKED</span>
|
||||
{% else %}
|
||||
<span class="badge badge-active">Unlocked</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">Mute Expires</div>
|
||||
<div class="detail-value">
|
||||
{% if account.mute_expire > 0 %}
|
||||
<span>{{ account.mute_expire }}</span>
|
||||
{% else %}
|
||||
<span class="text-muted">Not muted</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<button class="btn btn-primary" onclick="EditAccount()">Edit Account</button>
|
||||
{% if not account.banned %}
|
||||
<button class="btn btn-danger" onclick="BanAccount()">Ban Account</button>
|
||||
{% else %}
|
||||
<button class="btn btn-secondary" onclick="UnbanAccount()">Unban Account</button>
|
||||
{% endif %}
|
||||
{% if not account.locked %}
|
||||
<button class="btn btn-danger" onclick="LockAccount()">Lock Account</button>
|
||||
{% else %}
|
||||
<button class="btn btn-secondary" onclick="UnlockAccount()">Unlock Account</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TODO: Add modals for edit, ban, lock operations -->
|
||||
<!-- TODO: Add character list for this account -->
|
||||
<!-- TODO: Add login history -->
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
function EditAccount() {
|
||||
alert("Edit functionality coming soon");
|
||||
// TODO: Open edit modal
|
||||
}
|
||||
|
||||
function BanAccount() {
|
||||
if (confirm("Are you sure you want to ban this account?")) {
|
||||
alert("Ban functionality coming soon");
|
||||
// TODO: Call ban API endpoint
|
||||
}
|
||||
}
|
||||
|
||||
function UnbanAccount() {
|
||||
if (confirm("Are you sure you want to unban this account?")) {
|
||||
alert("Unban functionality coming soon");
|
||||
// TODO: Call unban API endpoint
|
||||
}
|
||||
}
|
||||
|
||||
function LockAccount() {
|
||||
if (confirm("Are you sure you want to lock this account?")) {
|
||||
alert("Lock functionality coming soon");
|
||||
// TODO: Call lock API endpoint
|
||||
}
|
||||
}
|
||||
|
||||
function UnlockAccount() {
|
||||
if (confirm("Are you sure you want to unlock this account?")) {
|
||||
alert("Unlock functionality coming soon");
|
||||
// TODO: Call unlock API endpoint
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
133
dDashboardServer/templates/accounts.jinja2
Normal file
133
dDashboardServer/templates/accounts.jinja2
Normal file
@@ -0,0 +1,133 @@
|
||||
{% extends "base.jinja2" %}
|
||||
|
||||
{% block title %}Accounts - DarkflameServer{% endblock %}
|
||||
|
||||
{% block css %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="accounts-container">
|
||||
<div class="container-fluid">
|
||||
<div class="table-card">
|
||||
<div class="table-header">
|
||||
<h2 class="mb-0">Accounts</h2>
|
||||
<p class="text-muted">View and manage user accounts</p>
|
||||
</div>
|
||||
<div class="table-body">
|
||||
<div class="table-responsive">
|
||||
<table id="accountsTable" class="table table-dark table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Username</th>
|
||||
<th>Banned</th>
|
||||
<th>Locked</th>
|
||||
<th>GM Level</th>
|
||||
<th>Mute Expires</th>
|
||||
<th>Created</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Data populated by DataTables -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Initialize DataTable with server-side processing
|
||||
$('#accountsTable').DataTable({
|
||||
processing: true,
|
||||
serverSide: true,
|
||||
pageLength: 25,
|
||||
lengthMenu: [10, 25, 50, 100],
|
||||
ajax: {
|
||||
url: '/api/tables/accounts',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: function(d) {
|
||||
return JSON.stringify(d);
|
||||
},
|
||||
error: function(xhr, error, thrown) {
|
||||
console.error('Error loading accounts:', error);
|
||||
if (xhr.status === 401) {
|
||||
window.location.href = '/login';
|
||||
}
|
||||
}
|
||||
},
|
||||
columns: [
|
||||
{ data: 'id' },
|
||||
{ data: 'name' },
|
||||
{
|
||||
data: 'banned',
|
||||
render: function(data) {
|
||||
return data ? '<span class="badge badge-banned">Banned</span>' : '<span class="badge bg-success">Active</span>';
|
||||
}
|
||||
},
|
||||
{
|
||||
data: 'locked',
|
||||
render: function(data) {
|
||||
return data ? '<span class="badge badge-locked">Locked</span>' : '<span class="badge bg-secondary">Unlocked</span>';
|
||||
}
|
||||
},
|
||||
{
|
||||
data: 'gm_level',
|
||||
render: function(data) {
|
||||
if (data === 0) return '-';
|
||||
return '<span class="badge badge-gm">Level ' + data + '</span>';
|
||||
}
|
||||
},
|
||||
{
|
||||
data: 'mute_expire',
|
||||
render: function(data) {
|
||||
if (data === 0) return 'Not Muted';
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const isMuted = data > now;
|
||||
const date = new Date(data * 1000);
|
||||
const dateStr = date.toLocaleString();
|
||||
if (isMuted) {
|
||||
return '<span class="badge bg-danger">Muted until ' + dateStr + '</span>';
|
||||
} else {
|
||||
return '<span class="badge bg-success">Expired ' + dateStr + '</span>';
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
data: 'created_at',
|
||||
render: function(data) {
|
||||
return data ? new Date(data).toLocaleString() : '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
data: 'id',
|
||||
render: function(data) {
|
||||
return '<div class="account-actions">' +
|
||||
'<button class="btn btn-sm btn-info" onclick="viewAccount(' + data + ')" title="View">👁️</button>' +
|
||||
'<button class="btn btn-sm btn-warning" onclick="editAccount(' + data + ')" title="Edit">✏️</button>' +
|
||||
'</div>';
|
||||
},
|
||||
orderable: false,
|
||||
searchable: false
|
||||
}
|
||||
],
|
||||
order: [[0, 'asc']],
|
||||
stateSave: false
|
||||
});
|
||||
});
|
||||
|
||||
function viewAccount(id) {
|
||||
window.location.href = '/accounts/' + id;
|
||||
}
|
||||
|
||||
function editAccount(id) {
|
||||
alert('Edit account: ' + id);
|
||||
// TODO: Implement account edit modal
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
34
dDashboardServer/templates/base.jinja2
Normal file
34
dDashboardServer/templates/base.jinja2
Normal file
@@ -0,0 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}DarkflameServer{% endblock %}</title>
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
|
||||
<link href="https://cdn.datatables.net/v/bs5/jq-3.7.0/dt-2.3.7/b-3.2.6/fh-4.0.6/sc-2.4.3/datatables.min.css" rel="stylesheet" integrity="sha384-XMNDGLb5fN9IqhIrVXOAtGKcz4KCr+JSHXGZ1TDXQPDukbEAfmLPjHdCXhgK93fv" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="/css/dashboard.css">
|
||||
{% block css %}{% endblock %}
|
||||
</head>
|
||||
<body class="d-flex bg-dark text-white">
|
||||
{% if username and username != "" %}
|
||||
{% include "header.jinja2" %}
|
||||
{% endif %}
|
||||
|
||||
<div class="container-fluid py-3">
|
||||
{% block content_before %}{% endblock %}
|
||||
{% block content %}{% endblock %}
|
||||
{% block content_after %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<footer class="mt-5 pt-5 border-top border-secondary text-center pb-3">
|
||||
{% block footer %}
|
||||
<p class="text-muted small">DarkflameServer Dashboard © 2024</p>
|
||||
{% endblock %}
|
||||
</footer>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.3.0/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.datatables.net/v/bs5/jq-3.7.0/dt-2.3.7/b-3.2.6/fh-4.0.6/sc-2.4.3/datatables.min.js" integrity="sha384-BPUXtS4tH3onFfu5m+dPbFfpLOXQwSWGwrsNWxOAAwqqJx6tJHhFkGF6uitrmEui" crossorigin="anonymous"></script>
|
||||
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
97
dDashboardServer/templates/bug_reports.jinja2
Normal file
97
dDashboardServer/templates/bug_reports.jinja2
Normal file
@@ -0,0 +1,97 @@
|
||||
{% extends "base.jinja2" %}
|
||||
|
||||
{% block title %}Bug Reports - DarkflameServer{% endblock %}
|
||||
|
||||
{% block css %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="bug-reports-container">
|
||||
<div class="table-card">
|
||||
<div class="table-header">
|
||||
<h2 class="mb-0">Bug Reports</h2>
|
||||
<p class="text-muted">View and manage bug reports from players</p>
|
||||
</div>
|
||||
<div class="table-body">
|
||||
<table id="bugReportsTable" class="table table-dark table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Player</th>
|
||||
<th>Client Version</th>
|
||||
<th>Submitted</th>
|
||||
<th>Report Preview</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Data populated by DataTables -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Initialize DataTable with server-side processing
|
||||
$('#bugReportsTable').DataTable({
|
||||
processing: true,
|
||||
serverSide: true,
|
||||
pageLength: 25,
|
||||
lengthMenu: [10, 25, 50, 100],
|
||||
ajax: {
|
||||
url: '/api/tables/bug_reports',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: function(d) {
|
||||
return JSON.stringify(d);
|
||||
}
|
||||
},
|
||||
columns: [
|
||||
{ data: 'id' },
|
||||
{ data: 'other_player_id' },
|
||||
{ data: 'client_version' },
|
||||
{
|
||||
data: 'submitted',
|
||||
render: function(data) {
|
||||
return data ? new Date(data).toLocaleString() : '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
data: 'body',
|
||||
render: function(data) {
|
||||
return '<span class="report-preview" title="' + (data || '') + '">' + (data || '-') + '</span>';
|
||||
}
|
||||
},
|
||||
{
|
||||
data: 'id',
|
||||
render: function(data) {
|
||||
return '<div class="account-actions">' +
|
||||
'<button class="btn btn-sm btn-info" onclick="viewReport(' + data + ')" title="View">👁️</button>' +
|
||||
'<button class="btn btn-sm btn-danger" onclick="deleteReport(' + data + ')" title="Delete">🗑️</button>' +
|
||||
'</div>';
|
||||
},
|
||||
orderable: false,
|
||||
searchable: false
|
||||
}
|
||||
],
|
||||
order: [[0, 'desc']],
|
||||
stateSave: false
|
||||
});
|
||||
});
|
||||
|
||||
function viewReport(id) {
|
||||
alert('View report: ' + id);
|
||||
// TODO: Implement report view modal
|
||||
}
|
||||
|
||||
function deleteReport(id) {
|
||||
if (confirm('Are you sure you want to delete this report?')) {
|
||||
alert('Delete report: ' + id);
|
||||
// TODO: Implement report deletion
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
90
dDashboardServer/templates/characters.jinja2
Normal file
90
dDashboardServer/templates/characters.jinja2
Normal file
@@ -0,0 +1,90 @@
|
||||
{% extends "base.jinja2" %}
|
||||
|
||||
{% block title %}Characters - DarkflameServer{% endblock %}
|
||||
|
||||
{% block css %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="characters-container">
|
||||
<div class="table-card">
|
||||
<div class="table-header">
|
||||
<h2 class="mb-0">Characters</h2>
|
||||
<p class="text-muted">View and manage player characters</p>
|
||||
</div>
|
||||
<div class="table-body">
|
||||
<table id="charactersTable" class="table table-dark table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Account</th>
|
||||
<th>Last Login</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Data populated by DataTables -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Initialize DataTable with server-side processing
|
||||
$('#charactersTable').DataTable({
|
||||
processing: true,
|
||||
serverSide: true,
|
||||
pageLength: 25,
|
||||
lengthMenu: [10, 25, 50, 100],
|
||||
ajax: {
|
||||
url: '/api/tables/characters',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: function(d) {
|
||||
return JSON.stringify(d);
|
||||
}
|
||||
},
|
||||
columns: [
|
||||
{ data: 'id' },
|
||||
{ data: 'name' },
|
||||
{ data: 'account_name' },
|
||||
{
|
||||
data: 'last_login',
|
||||
render: function(data) {
|
||||
if (data === 0) return 'Never';
|
||||
const date = new Date(data * 1000);
|
||||
return date.toLocaleString();
|
||||
}
|
||||
},
|
||||
{
|
||||
data: 'id',
|
||||
render: function(data) {
|
||||
return '<div class="account-actions">' +
|
||||
'<button class="btn btn-sm btn-info" onclick="viewCharacter(' + data + ')" title="View">👁️</button>' +
|
||||
'<button class="btn btn-sm btn-warning" onclick="editCharacter(' + data + ')" title="Edit">✏️</button>' +
|
||||
'</div>';
|
||||
},
|
||||
orderable: false,
|
||||
searchable: false
|
||||
}
|
||||
],
|
||||
order: [[0, 'asc']],
|
||||
stateSave: false
|
||||
});
|
||||
});
|
||||
|
||||
function viewCharacter(id) {
|
||||
alert('View character: ' + id);
|
||||
// TODO: Implement character view modal
|
||||
}
|
||||
|
||||
function editCharacter(id) {
|
||||
alert('Edit character: ' + id);
|
||||
// TODO: Implement character edit modal
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
30
dDashboardServer/templates/header.jinja2
Normal file
30
dDashboardServer/templates/header.jinja2
Normal file
@@ -0,0 +1,30 @@
|
||||
{# Navigation #}
|
||||
<nav class="navbar navbar-dark bg-dark flex-column" style="width: 280px; height: 100vh; position: fixed; left: 0; top: 0; overflow-y: auto;">
|
||||
<div class="p-3">
|
||||
<a class="navbar-brand fw-bold" href="/">🎮 DarkflameServer</a>
|
||||
</div>
|
||||
|
||||
<ul class="navbar-nav flex-column w-100 flex-grow-1 p-3">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/">Home</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/accounts">Accounts</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/characters">Characters</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/play_keys">Play Keys</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/properties">Properties</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/bug_reports">Bug Reports</a>
|
||||
</li>
|
||||
<li class="nav-item mt-auto">
|
||||
<a class="nav-link" href="#" id="logoutBtn">Logout</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
35
dDashboardServer/templates/index.jinja2
Normal file
35
dDashboardServer/templates/index.jinja2
Normal file
@@ -0,0 +1,35 @@
|
||||
{% extends "base.jinja2" %}
|
||||
|
||||
{% block title %}Dashboard - DarkflameServer{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Main Content -->
|
||||
<main style="margin-left: 280px;">
|
||||
<div class="container-fluid p-3 p-md-4">
|
||||
|
||||
<div class="row g-3">
|
||||
{% include "server_status.jinja2" %}
|
||||
{% include "statistics.jinja2" %}
|
||||
</div>
|
||||
|
||||
{% include "world_instances.jinja2" %}
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="/js/dashboard.js"></script>
|
||||
<script>
|
||||
// Check authentication and initialize dashboard
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// checkAuthentication is now async and calls connectWebSocket when ready
|
||||
checkAuthentication();
|
||||
|
||||
// Setup logout button
|
||||
document.getElementById('logoutBtn').addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
logout();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
53
dDashboardServer/templates/login.jinja2
Normal file
53
dDashboardServer/templates/login.jinja2
Normal file
@@ -0,0 +1,53 @@
|
||||
{% extends "base.jinja2" %}
|
||||
|
||||
{% block title %}Dashboard Login - DarkflameServer{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<link rel="stylesheet" href="/css/login.css">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="min-vh-100 d-flex align-items-center justify-content-center">
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 col-lg-5">
|
||||
<div class="card shadow-lg border-0">
|
||||
<div class="card-body p-5">
|
||||
<h1 class="text-center mb-4">🎮 DarkflameServer</h1>
|
||||
|
||||
<div id="alert" class="alert" role="alert" style="display: none;"></div>
|
||||
|
||||
<form id="loginForm">
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Username</label>
|
||||
<input type="text" class="form-control" id="username" name="username" required autofocus>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<input type="password" class="form-control" id="password" name="password" required maxlength="40">
|
||||
</div>
|
||||
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" id="rememberMe" name="rememberMe">
|
||||
<label class="form-check-label" for="rememberMe">
|
||||
Remember me for 30 days
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary w-100" id="loginBtn">
|
||||
<span id="loading" class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true" style="display: none;"></span>
|
||||
<span>Login</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="/js/login.js"></script>
|
||||
{% endblock %}
|
||||
95
dDashboardServer/templates/play_keys.jinja2
Normal file
95
dDashboardServer/templates/play_keys.jinja2
Normal file
@@ -0,0 +1,95 @@
|
||||
{% extends "base.jinja2" %}
|
||||
|
||||
{% block title %}Play Keys - DarkflameServer{% endblock %}
|
||||
|
||||
{% block css %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="play-keys-container">
|
||||
<div class="table-card">
|
||||
<div class="table-header">
|
||||
<h2 class="mb-0">Play Keys</h2>
|
||||
<p class="text-muted">View and manage play keys</p>
|
||||
</div>
|
||||
<div class="table-body">
|
||||
<table id="playKeysTable" class="table table-dark table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Key String</th>
|
||||
<th>Uses Remaining</th>
|
||||
<th>Created</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Data populated by DataTables -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Initialize DataTable with server-side processing
|
||||
$('#playKeysTable').DataTable({
|
||||
processing: true,
|
||||
serverSide: true,
|
||||
pageLength: 25,
|
||||
lengthMenu: [10, 25, 50, 100],
|
||||
ajax: {
|
||||
url: '/api/tables/play_keys',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: function(d) {
|
||||
return JSON.stringify(d);
|
||||
}
|
||||
},
|
||||
columns: [
|
||||
{ data: 'id' },
|
||||
{ data: 'key_string' },
|
||||
{ data: 'key_uses' },
|
||||
{
|
||||
data: 'created_at',
|
||||
render: function(data) {
|
||||
return data ? new Date(data).toLocaleString() : '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
data: 'active',
|
||||
render: function(data) {
|
||||
return data ? '<span class="badge badge-active">Active</span>' : '<span class="badge badge-inactive">Inactive</span>';
|
||||
}
|
||||
},
|
||||
{
|
||||
data: 'id',
|
||||
render: function(data) {
|
||||
return '<div class="account-actions">' +
|
||||
'<button class="btn btn-sm btn-info" onclick="viewKey(' + data + ')" title="View">👁️</button>' +
|
||||
'<button class="btn btn-sm btn-warning" onclick="editKey(' + data + ')" title="Edit">✏️</button>' +
|
||||
'</div>';
|
||||
},
|
||||
orderable: false,
|
||||
searchable: false
|
||||
}
|
||||
],
|
||||
order: [[0, 'asc']],
|
||||
stateSave: false
|
||||
});
|
||||
});
|
||||
|
||||
function viewKey(id) {
|
||||
alert('View key: ' + id);
|
||||
// TODO: Implement key view modal
|
||||
}
|
||||
|
||||
function editKey(id) {
|
||||
alert('Edit key: ' + id);
|
||||
// TODO: Implement key edit modal
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
92
dDashboardServer/templates/properties.jinja2
Normal file
92
dDashboardServer/templates/properties.jinja2
Normal file
@@ -0,0 +1,92 @@
|
||||
{% extends "base.jinja2" %}
|
||||
|
||||
{% block title %}Properties - DarkflameServer{% endblock %}
|
||||
|
||||
{% block css %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="properties-container">
|
||||
<div class="table-card">
|
||||
<div class="table-header">
|
||||
<h2 class="mb-0">Properties</h2>
|
||||
<p class="text-muted">View and manage player properties</p>
|
||||
</div>
|
||||
<div class="table-body">
|
||||
<table id="propertiesTable" class="table table-dark table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Owner ID</th>
|
||||
<th>Moderation Status</th>
|
||||
<th>Reputation</th>
|
||||
<th>Zone</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Data populated by DataTables -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Initialize DataTable with server-side processing
|
||||
$('#propertiesTable').DataTable({
|
||||
processing: true,
|
||||
serverSide: true,
|
||||
pageLength: 25,
|
||||
lengthMenu: [10, 25, 50, 100],
|
||||
ajax: {
|
||||
url: '/api/tables/properties',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: function(d) {
|
||||
return JSON.stringify(d);
|
||||
}
|
||||
},
|
||||
columns: [
|
||||
{ data: 'id' },
|
||||
{ data: 'name' },
|
||||
{ data: 'owner_id' },
|
||||
{
|
||||
data: 'mod_approved',
|
||||
render: function(data) {
|
||||
return data ? '<span class="badge badge-approved">Approved</span>' : '<span class="badge badge-pending">Pending</span>';
|
||||
}
|
||||
},
|
||||
{ data: 'reputation' },
|
||||
{ data: 'zone_id' },
|
||||
{
|
||||
data: 'id',
|
||||
render: function(data) {
|
||||
return '<div class="account-actions">' +
|
||||
'<button class="btn btn-sm btn-info" onclick="viewProperty(' + data + ')" title="View">👁️</button>' +
|
||||
'<button class="btn btn-sm btn-warning" onclick="editProperty(' + data + ')" title="Edit">✏️</button>' +
|
||||
'</div>';
|
||||
},
|
||||
orderable: false,
|
||||
searchable: false
|
||||
}
|
||||
],
|
||||
order: [[0, 'asc']],
|
||||
stateSave: false
|
||||
});
|
||||
});
|
||||
|
||||
function viewProperty(id) {
|
||||
alert('View property: ' + id);
|
||||
// TODO: Implement property view modal
|
||||
}
|
||||
|
||||
function editProperty(id) {
|
||||
alert('Edit property: ' + id);
|
||||
// TODO: Implement property edit modal
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
29
dDashboardServer/templates/server_status.jinja2
Normal file
29
dDashboardServer/templates/server_status.jinja2
Normal file
@@ -0,0 +1,29 @@
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="card border-0 shadow-sm h-100">
|
||||
<div class="card-header bg-light">
|
||||
<h5 class="mb-0">Server Status</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<span>Auth Server</span>
|
||||
{% if auth.online %}
|
||||
<span class="badge bg-success" id="auth-status">Online</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger" id="auth-status">Offline</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<span>Chat Server</span>
|
||||
{% if chat.online %}
|
||||
<span class="badge bg-success" id="chat-status">Online</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger" id="chat-status">Offline</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span>Active Worlds</span>
|
||||
<span class="badge bg-primary" id="world-count">{{ length(worlds) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
21
dDashboardServer/templates/statistics.jinja2
Normal file
21
dDashboardServer/templates/statistics.jinja2
Normal file
@@ -0,0 +1,21 @@
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="card border-0 shadow-sm h-100">
|
||||
<div class="card-header bg-light">
|
||||
<h5 class="mb-0">Statistics</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<span>Online Players</span>
|
||||
<span class="badge bg-info" id="online-players">{{ stats.onlinePlayers }}</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<span>Total Accounts</span>
|
||||
<span class="badge bg-info" id="total-accounts">{{ stats.totalAccounts }}</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span>Total Characters</span>
|
||||
<span class="badge bg-info" id="total-characters">{{ stats.totalCharacters }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
37
dDashboardServer/templates/world_instances.jinja2
Normal file
37
dDashboardServer/templates/world_instances.jinja2
Normal file
@@ -0,0 +1,37 @@
|
||||
<div class="card border-0 shadow-sm mt-4">
|
||||
<div class="card-header bg-light">
|
||||
<h5 class="mb-0">Active World Instances</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="world-list">
|
||||
{% if length(worlds) == 0 %}
|
||||
<p class="text-muted text-center mb-0">No active world instances</p>
|
||||
{% else %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Zone</th>
|
||||
<th>Instance</th>
|
||||
<th>Clone</th>
|
||||
<th>Players</th>
|
||||
<th>Type</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for world in worlds %}
|
||||
<tr>
|
||||
<td>{{ world.mapID }}</td>
|
||||
<td>{{ world.instanceID }}</td>
|
||||
<td>{{ world.cloneID }}</td>
|
||||
<td><span class="badge bg-secondary">{{ world.players }}</span></td>
|
||||
<td>{% if world.isPrivate %}<span class="badge bg-warning">Private</span>{% else %}<span class="badge bg-primary">Public</span>{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -154,8 +154,8 @@ std::map<LOT, uint32_t> CDItemComponentTable::ParseCraftingCurrencies(const CDIt
|
||||
// Checking for 2 here, not sure what to do when there's more stuff than expected
|
||||
if (amountSplit.size() == 2) {
|
||||
currencies.insert({
|
||||
GeneralUtils::TryParse<LOT>(amountSplit[0], LOT_NULL),
|
||||
GeneralUtils::TryParse<uint32_t>(amountSplit[1], 0)
|
||||
std::stoull(amountSplit[0]),
|
||||
std::stoi(amountSplit[1])
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,14 +93,13 @@ std::vector<CDMissions> CDMissionsTable::Query(std::function<bool(CDMissions)> p
|
||||
}
|
||||
|
||||
const CDMissions* CDMissionsTable::GetPtrByMissionID(uint32_t missionID) const {
|
||||
const CDMissions* toReturn = &Default;
|
||||
for (const auto& entry : GetEntries()) {
|
||||
if (entry.id == missionID) {
|
||||
toReturn = &entry;
|
||||
return const_cast<CDMissions*>(&entry);
|
||||
}
|
||||
}
|
||||
|
||||
return toReturn;
|
||||
return &Default;
|
||||
}
|
||||
|
||||
const CDMissions& CDMissionsTable::GetByMissionID(uint32_t missionID, bool& found) const {
|
||||
|
||||
@@ -66,7 +66,6 @@ public:
|
||||
// Queries the table with a custom "where" clause
|
||||
std::vector<CDMissions> Query(std::function<bool(CDMissions)> predicate);
|
||||
|
||||
// Cannot be null.
|
||||
const CDMissions* GetPtrByMissionID(uint32_t missionID) const;
|
||||
|
||||
const CDMissions& GetByMissionID(uint32_t missionID, bool& found) const;
|
||||
|
||||
@@ -69,7 +69,7 @@ const CDObjects& CDObjectsTable::GetByID(const uint32_t lot) {
|
||||
entry.name = tableData.getStringField("name", "");
|
||||
UNUSED(entry.placeable = tableData.getIntField("placeable", -1));
|
||||
entry.type = tableData.getStringField("type", "");
|
||||
UNUSED(entry.description = tableData.getStringField(4, ""));
|
||||
UNUSED(ntry.description = tableData.getStringField(4, ""));
|
||||
UNUSED(entry.localize = tableData.getIntField("localize", -1));
|
||||
UNUSED(entry.npcTemplateID = tableData.getIntField("npcTemplateID", -1));
|
||||
UNUSED(entry.displayName = tableData.getStringField("displayName", ""));
|
||||
|
||||
@@ -56,7 +56,7 @@ CDRailActivatorComponent CDRailActivatorComponentTable::GetEntryByID(int32_t id)
|
||||
std::pair<uint32_t, std::u16string> CDRailActivatorComponentTable::EffectPairFromString(std::string& str) {
|
||||
const auto split = GeneralUtils::SplitString(str, ':');
|
||||
if (split.size() == 2) {
|
||||
return { GeneralUtils::TryParse(split.at(0), 0), GeneralUtils::ASCIIToUTF16(split.at(1)) };
|
||||
return { std::stoi(split.at(0)), GeneralUtils::ASCIIToUTF16(split.at(1)) };
|
||||
}
|
||||
|
||||
return {};
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include "json.hpp"
|
||||
|
||||
enum class eGameMasterLevel : uint8_t;
|
||||
|
||||
@@ -39,6 +40,30 @@ public:
|
||||
virtual void UpdateAccountGmLevel(const uint32_t accountId, const eGameMasterLevel gmLevel) = 0;
|
||||
|
||||
virtual uint32_t GetAccountCount() = 0;
|
||||
|
||||
// Login attempt tracking methods
|
||||
// Record a failed login attempt
|
||||
virtual void RecordFailedAttempt(const uint32_t accountId) = 0;
|
||||
|
||||
// Clear failed login attempts and update last login time
|
||||
virtual void ClearFailedAttempts(const uint32_t accountId) = 0;
|
||||
|
||||
// Set account lockout
|
||||
virtual void SetLockout(const uint32_t accountId, const int64_t lockoutUntil) = 0;
|
||||
|
||||
// Check if account is locked out
|
||||
virtual bool IsLockedOut(const uint32_t accountId) = 0;
|
||||
|
||||
// Get failed attempt count
|
||||
virtual uint8_t GetFailedAttempts(const uint32_t accountId) = 0;
|
||||
|
||||
// Get paginated list of accounts with optional search/filtering for DataTables
|
||||
// Returns a JSON object with the account data and metadata
|
||||
virtual nlohmann::json GetAccountsTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) = 0;
|
||||
|
||||
// Get a single account by ID
|
||||
// Returns a JSON object with the account details
|
||||
virtual nlohmann::json GetAccountById(uint32_t accountId) = 0;
|
||||
};
|
||||
|
||||
#endif //!__IACCOUNTS__H__
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#define __IBUGREPORTS__H__
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
class IBugReports {
|
||||
@@ -16,5 +17,9 @@ public:
|
||||
|
||||
// Add a new bug report to the database.
|
||||
virtual void InsertNewBugReport(const Info& info) = 0;
|
||||
|
||||
// Get paginated list of bug reports with optional search/filtering for DataTables
|
||||
// Returns a JSON-formatted string with the bug report data and metadata
|
||||
virtual std::string GetBugReportsTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) = 0;
|
||||
};
|
||||
#endif //!__IBUGREPORTS__H__
|
||||
|
||||
@@ -33,6 +33,9 @@ public:
|
||||
// Get the character ids for the given account.
|
||||
virtual std::vector<LWOOBJID> GetAccountCharacterIds(const LWOOBJID accountId) = 0;
|
||||
|
||||
// Get the total number of characters in the database.
|
||||
virtual uint32_t GetCharacterCount() = 0;
|
||||
|
||||
// Insert a new character into the database.
|
||||
virtual void InsertNewCharacter(const ICharInfo::Info info) = 0;
|
||||
|
||||
|
||||
@@ -15,6 +15,10 @@ public:
|
||||
|
||||
// Insert the character xml for the given character id.
|
||||
virtual void InsertCharacterXml(const LWOOBJID characterId, const std::string_view lxfml) = 0;
|
||||
|
||||
// Get paginated list of characters with optional search/filtering for DataTables
|
||||
// Returns a JSON-formatted string with the character data and metadata
|
||||
virtual std::string GetCharactersTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) = 0;
|
||||
};
|
||||
|
||||
#endif //!__ICHARXML__H__
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
class IPlayKeys {
|
||||
public:
|
||||
@@ -10,6 +12,10 @@ public:
|
||||
// Optional of bool may seem pointless, however the optional indicates if the playkey exists
|
||||
// and the bool indicates if the playkey is active.
|
||||
virtual std::optional<bool> IsPlaykeyActive(const int32_t playkeyId) = 0;
|
||||
|
||||
// Get paginated list of play keys with optional search/filtering for DataTables
|
||||
// Returns a JSON-formatted string with the play key data and metadata
|
||||
virtual std::string GetPlayKeysTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) = 0;
|
||||
};
|
||||
|
||||
#endif //!__IPLAYKEYS__H__
|
||||
|
||||
@@ -34,7 +34,6 @@ public:
|
||||
};
|
||||
|
||||
struct PropertyEntranceResult {
|
||||
// This is the number of entries that are in the query IF it were ran without a limit.
|
||||
int32_t totalEntriesMatchingQuery{};
|
||||
// The entries that match the query. This should only contain up to 12 entries.
|
||||
std::vector<IProperty::Info> entries;
|
||||
@@ -49,7 +48,7 @@ public:
|
||||
// Get the properties for the given property lookup params.
|
||||
// This is expected to return a result set of up to 12 properties
|
||||
// so as not to transfer too much data at once.
|
||||
virtual IProperty::PropertyEntranceResult GetProperties(const PropertyLookup& params) = 0;
|
||||
virtual std::optional<IProperty::PropertyEntranceResult> GetProperties(const PropertyLookup& params) = 0;
|
||||
|
||||
// Update the property moderation info for the given property id.
|
||||
virtual void UpdatePropertyModerationInfo(const IProperty::Info& info) = 0;
|
||||
@@ -65,5 +64,9 @@ public:
|
||||
|
||||
// Insert a new property into the database.
|
||||
virtual void InsertNewProperty(const IProperty::Info& info, const uint32_t templateId, const LWOZONEID& zoneId) = 0;
|
||||
|
||||
// Get paginated list of properties with optional search/filtering for DataTables
|
||||
// Returns a JSON-formatted string with the property data and metadata
|
||||
virtual std::string GetPropertiesTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) = 0;
|
||||
};
|
||||
#endif //!__IPROPERTY__H__
|
||||
|
||||
@@ -9,20 +9,6 @@
|
||||
typedef std::unique_ptr<sql::PreparedStatement>& UniquePreppedStmtRef;
|
||||
typedef std::unique_ptr<sql::ResultSet> UniqueResultSet;
|
||||
|
||||
// This struct is used to keep the PreparedStatement alive alongside the ResultSet, since the ResultSet will be invalidated if the PreparedStatement is destroyed.
|
||||
// Declaring the members in reverse order of usage to ensure the PreparedStatement is destroyed after the ResultSet. This is guaranteed by the C++ standard.
|
||||
struct PreparedStmtResultSet {
|
||||
std::unique_ptr<sql::PreparedStatement> m_stmt;
|
||||
std::unique_ptr<sql::ResultSet> m_resultSet;
|
||||
|
||||
PreparedStmtResultSet(sql::PreparedStatement* stmt = nullptr, sql::ResultSet* resultSet = nullptr)
|
||||
: m_stmt(stmt), m_resultSet(resultSet) {}
|
||||
|
||||
sql::ResultSet* operator->() const {
|
||||
return m_resultSet.get();
|
||||
}
|
||||
};
|
||||
|
||||
// Purposefully no definition for this to provide linker errors in the case someone tries to
|
||||
// bind a parameter to a type that isn't defined.
|
||||
template<typename ParamType>
|
||||
@@ -74,6 +60,7 @@ public:
|
||||
std::optional<IAccounts::Info> GetAccountInfo(const std::string_view username) override;
|
||||
void InsertNewCharacter(const ICharInfo::Info info) override;
|
||||
void InsertCharacterXml(const LWOOBJID accountId, const std::string_view lxfml) override;
|
||||
std::string GetCharactersTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) override;
|
||||
std::vector<LWOOBJID> GetAccountCharacterIds(LWOOBJID accountId) override;
|
||||
void DeleteCharacter(const LWOOBJID characterId) override;
|
||||
void SetCharacterName(const LWOOBJID characterId, const std::string_view name) override;
|
||||
@@ -93,6 +80,7 @@ public:
|
||||
void RemoveModel(const LWOOBJID& modelId) override;
|
||||
void UpdatePerformanceCost(const LWOZONEID& zoneId, const float performanceCost) override;
|
||||
void InsertNewBugReport(const IBugReports::Info& info) override;
|
||||
std::string GetBugReportsTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) override;
|
||||
void InsertCheatDetection(const IPlayerCheatDetections::Info& info) override;
|
||||
void InsertNewMail(const MailInfo& mail) override;
|
||||
void InsertNewUgcModel(
|
||||
@@ -117,6 +105,7 @@ public:
|
||||
void InsertDefaultPersistentId() override;
|
||||
std::optional<uint32_t> GetDonationTotal(const uint32_t activityId) override;
|
||||
std::optional<bool> IsPlaykeyActive(const int32_t playkeyId) override;
|
||||
std::string GetPlayKeysTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) override;
|
||||
std::vector<IUgc::Model> GetUgcModels(const LWOOBJID& propertyId) override;
|
||||
void AddIgnore(const LWOOBJID playerId, const LWOOBJID ignoredPlayerId) override;
|
||||
void RemoveIgnore(const LWOOBJID playerId, const LWOOBJID ignoredPlayerId) override;
|
||||
@@ -127,7 +116,7 @@ public:
|
||||
std::string GetBehavior(const LWOOBJID behaviorId) override;
|
||||
void RemoveBehavior(const LWOOBJID characterId) override;
|
||||
void UpdateAccountGmLevel(const uint32_t accountId, const eGameMasterLevel gmLevel) override;
|
||||
IProperty::PropertyEntranceResult GetProperties(const IProperty::PropertyLookup& params) override;
|
||||
std::optional<IProperty::PropertyEntranceResult> GetProperties(const IProperty::PropertyLookup& params) override;
|
||||
std::vector<ILeaderboard::Entry> GetDescendingLeaderboard(const uint32_t activityId) override;
|
||||
std::vector<ILeaderboard::Entry> GetAscendingLeaderboard(const uint32_t activityId) override;
|
||||
std::vector<ILeaderboard::Entry> GetNsLeaderboard(const uint32_t activityId) override;
|
||||
@@ -140,25 +129,31 @@ public:
|
||||
void InsertUgcBuild(const std::string& modules, const LWOOBJID bigId, const std::optional<LWOOBJID> characterId) override;
|
||||
void DeleteUgcBuild(const LWOOBJID bigId) override;
|
||||
uint32_t GetAccountCount() override;
|
||||
uint32_t GetCharacterCount() override;
|
||||
void RecordFailedAttempt(const uint32_t accountId) override;
|
||||
void ClearFailedAttempts(const uint32_t accountId) override;
|
||||
void SetLockout(const uint32_t accountId, const int64_t lockoutUntil) override;
|
||||
bool IsLockedOut(const uint32_t accountId) override;
|
||||
uint8_t GetFailedAttempts(const uint32_t accountId) override;
|
||||
nlohmann::json GetAccountsTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) override;
|
||||
nlohmann::json GetAccountById(uint32_t accountId) override;
|
||||
bool IsNameInUse(const std::string_view name) override;
|
||||
std::optional<IPropertyContents::Model> GetModel(const LWOOBJID modelID) override;
|
||||
std::optional<IUgc::Model> GetUgcModel(const LWOOBJID ugcId) override;
|
||||
std::optional<IProperty::Info> GetPropertyInfo(const LWOOBJID id) override;
|
||||
std::string GetPropertiesTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) override;
|
||||
sql::PreparedStatement* CreatePreppedStmt(const std::string& query);
|
||||
private:
|
||||
|
||||
// Generic query functions that can be used for any query.
|
||||
// Return type may be different depending on the query, so it is up to the caller to check the return type.
|
||||
// The first argument is the query string, and the rest are the parameters to bind to the query.
|
||||
// The return type is a PreparedStmtResultSet which keeps the PreparedStatement alive alongside the ResultSet.
|
||||
// The return type is a unique_ptr to the result set, which is deleted automatically when it goes out of scope
|
||||
template<typename... Args>
|
||||
inline PreparedStmtResultSet ExecuteSelect(const std::string& query, Args&&... args) {
|
||||
PreparedStmtResultSet toReturn;
|
||||
toReturn.m_stmt.reset(CreatePreppedStmt(query));
|
||||
SetParams(toReturn.m_stmt, std::forward<Args>(args)...);
|
||||
DLU_SQL_TRY_CATCH_RETHROW(toReturn.m_resultSet.reset(toReturn.m_stmt->executeQuery()));
|
||||
// Return the PreparedStmtResultSet, which now owns both the PreparedStatement and ResultSet via unique_ptr and will ensure they are properly cleaned up.
|
||||
return toReturn;
|
||||
inline std::unique_ptr<sql::ResultSet> ExecuteSelect(const std::string& query, Args&&... args) {
|
||||
std::unique_ptr<sql::PreparedStatement> preppedStmt(CreatePreppedStmt(query));
|
||||
SetParams(preppedStmt, std::forward<Args>(args)...);
|
||||
DLU_SQL_TRY_CATCH_RETHROW(return std::unique_ptr<sql::ResultSet>(preppedStmt->executeQuery()));
|
||||
}
|
||||
|
||||
template<typename... Args>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "MySQLDatabase.h"
|
||||
|
||||
#include "eGameMasterLevel.h"
|
||||
#include "json.hpp"
|
||||
|
||||
std::optional<IAccounts::Info> MySQLDatabase::GetAccountInfo(const std::string_view username) {
|
||||
auto result = ExecuteSelect("SELECT id, password, banned, locked, play_key_id, gm_level, mute_expire FROM accounts WHERE name = ? LIMIT 1;", username);
|
||||
@@ -45,3 +46,142 @@ uint32_t MySQLDatabase::GetAccountCount() {
|
||||
auto res = ExecuteSelect("SELECT COUNT(*) as count FROM accounts;");
|
||||
return res->next() ? res->getUInt("count") : 0;
|
||||
}
|
||||
|
||||
void MySQLDatabase::RecordFailedAttempt(const uint32_t accountId) {
|
||||
ExecuteUpdate("UPDATE accounts SET failed_attempts = failed_attempts + 1 WHERE id = ?;", accountId);
|
||||
}
|
||||
|
||||
void MySQLDatabase::ClearFailedAttempts(const uint32_t accountId) {
|
||||
ExecuteUpdate("UPDATE accounts SET failed_attempts = 0, lockout_time = NULL, last_login = NOW() WHERE id = ?;", accountId);
|
||||
}
|
||||
|
||||
void MySQLDatabase::SetLockout(const uint32_t accountId, const int64_t lockoutUntil) {
|
||||
ExecuteUpdate("UPDATE accounts SET lockout_time = FROM_UNIXTIME(?) WHERE id = ?;", lockoutUntil, accountId);
|
||||
}
|
||||
|
||||
bool MySQLDatabase::IsLockedOut(const uint32_t accountId) {
|
||||
auto result = ExecuteSelect("SELECT lockout_time FROM accounts WHERE id = ?;", accountId);
|
||||
if (!result->next()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If lockout_time is set and in the future, account is locked
|
||||
const char* lockoutTime = result->getString("lockout_time").c_str();
|
||||
if (lockoutTime == nullptr || strlen(lockoutTime) == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Simplified check - if lockout_time exists and is not null, it's locked
|
||||
return true;
|
||||
}
|
||||
|
||||
uint8_t MySQLDatabase::GetFailedAttempts(const uint32_t accountId) {
|
||||
auto result = ExecuteSelect("SELECT failed_attempts FROM accounts WHERE id = ?;", accountId);
|
||||
if (!result->next()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return result->getUInt("failed_attempts");
|
||||
}
|
||||
|
||||
nlohmann::json MySQLDatabase::GetAccountsTable(uint32_t start, uint32_t length, const std::string_view search, uint32_t orderColumn, bool orderAsc) {
|
||||
// Build base query
|
||||
std::string baseQuery = "SELECT id, name, banned, locked, gm_level, mute_expire, created_at FROM accounts";
|
||||
std::string whereClause;
|
||||
std::string orderClause;
|
||||
|
||||
// Add search filter if provided
|
||||
if (!search.empty()) {
|
||||
whereClause = " WHERE name LIKE CONCAT('%', ?, '%')";
|
||||
}
|
||||
|
||||
// Map column indices to database columns
|
||||
std::string orderColumnName = "id";
|
||||
switch (orderColumn) {
|
||||
case 0: orderColumnName = "id"; break;
|
||||
case 1: orderColumnName = "name"; break;
|
||||
case 2: orderColumnName = "banned"; break;
|
||||
case 3: orderColumnName = "locked"; break;
|
||||
case 4: orderColumnName = "gm_level"; break;
|
||||
case 5: orderColumnName = "mute_expire"; break;
|
||||
case 6: orderColumnName = "created_at"; break;
|
||||
default: orderColumnName = "id";
|
||||
}
|
||||
|
||||
orderClause = " ORDER BY " + orderColumnName + (orderAsc ? " ASC" : " DESC");
|
||||
|
||||
// Build the main query
|
||||
std::string mainQuery = baseQuery + whereClause + orderClause + " LIMIT ?, ?;";
|
||||
|
||||
// Get total count
|
||||
std::string totalCountQuery = "SELECT COUNT(*) as count FROM accounts;";
|
||||
auto totalCountResult = ExecuteSelect(totalCountQuery);
|
||||
uint32_t totalRecords = totalCountResult->next() ? totalCountResult->getUInt("count") : 0;
|
||||
|
||||
// Get filtered count
|
||||
uint32_t filteredRecords = totalRecords;
|
||||
if (!search.empty()) {
|
||||
std::string filteredCountQuery = "SELECT COUNT(*) as count FROM accounts WHERE name LIKE CONCAT('%', ?, '%');";
|
||||
auto filteredCountResult = ExecuteSelect(filteredCountQuery, search);
|
||||
filteredRecords = filteredCountResult->next() ? filteredCountResult->getUInt("count") : 0;
|
||||
}
|
||||
|
||||
// Execute main query
|
||||
std::unique_ptr<sql::ResultSet> result;
|
||||
if (!search.empty()) {
|
||||
result = ExecuteSelect(mainQuery, search, start, length);
|
||||
} else {
|
||||
result = ExecuteSelect(mainQuery, start, length);
|
||||
}
|
||||
|
||||
// Build response JSON
|
||||
nlohmann::json accountsArray = nlohmann::json::array();
|
||||
|
||||
while (result->next()) {
|
||||
nlohmann::json account = {
|
||||
{"id", result->getUInt("id")},
|
||||
{"name", result->getString("name")},
|
||||
{"banned", result->getBoolean("banned")},
|
||||
{"locked", result->getBoolean("locked")},
|
||||
{"gm_level", result->getInt("gm_level")},
|
||||
{"mute_expire", result->getUInt64("mute_expire")},
|
||||
{"created_at", result->getString("created_at")}
|
||||
};
|
||||
accountsArray.push_back(account);
|
||||
}
|
||||
|
||||
nlohmann::json response = {
|
||||
{"draw", 1},
|
||||
{"recordsTotal", totalRecords},
|
||||
{"recordsFiltered", filteredRecords},
|
||||
{"data", accountsArray}
|
||||
};
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
nlohmann::json MySQLDatabase::GetAccountById(uint32_t accountId) {
|
||||
try {
|
||||
const std::string query = "SELECT id, name, banned, locked, gm_level, mute_expire, created_at FROM accounts WHERE id = ?;";
|
||||
auto result = ExecuteSelect(query, accountId);
|
||||
|
||||
if (!result->next()) {
|
||||
return nlohmann::json{{"error", "Account not found"}};
|
||||
}
|
||||
|
||||
nlohmann::json account = {
|
||||
{"id", result->getUInt("id")},
|
||||
{"name", result->getString("name")},
|
||||
{"banned", result->getBoolean("banned")},
|
||||
{"locked", result->getBoolean("locked")},
|
||||
{"gm_level", result->getInt("gm_level")},
|
||||
{"mute_expire", result->getUInt64("mute_expire")},
|
||||
{"created_at", result->getString("created_at")}
|
||||
};
|
||||
|
||||
return account;
|
||||
} catch (const sql::SQLException& e) {
|
||||
LOG_DEBUG("SQL Error: %s", e.what());
|
||||
return nlohmann::json{{"error", "Database error"}};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,3 +4,77 @@ void MySQLDatabase::InsertNewBugReport(const IBugReports::Info& info) {
|
||||
ExecuteInsert("INSERT INTO `bug_reports`(body, client_version, other_player_id, selection, reporter_id) VALUES (?, ?, ?, ?, ?)",
|
||||
info.body, info.clientVersion, info.otherPlayer, info.selection, info.characterId);
|
||||
}
|
||||
|
||||
#include "json.hpp"
|
||||
|
||||
std::string MySQLDatabase::GetBugReportsTable(uint32_t start, uint32_t length, const std::string_view search, uint32_t orderColumn, bool orderAsc) {
|
||||
// Build base query
|
||||
std::string baseQuery = "SELECT id, body, client_version, other_player_id, selection, submitted FROM bug_reports";
|
||||
std::string whereClause;
|
||||
std::string orderClause;
|
||||
|
||||
// Add search filter if provided
|
||||
if (!search.empty()) {
|
||||
whereClause = " WHERE body LIKE CONCAT('%', ?, '%') OR other_player_id LIKE CONCAT('%', ?, '%')";
|
||||
}
|
||||
|
||||
// Map column indices to database columns
|
||||
std::string orderColumnName = "id";
|
||||
switch (orderColumn) {
|
||||
case 0: orderColumnName = "id"; break;
|
||||
case 1: orderColumnName = "other_player_id"; break;
|
||||
case 2: orderColumnName = "client_version"; break;
|
||||
case 3: orderColumnName = "submitted"; break;
|
||||
default: orderColumnName = "id";
|
||||
}
|
||||
|
||||
orderClause = " ORDER BY " + orderColumnName + (orderAsc ? " ASC" : " DESC");
|
||||
|
||||
// Build the main query
|
||||
std::string mainQuery = baseQuery + whereClause + orderClause + " LIMIT ?, ?;";
|
||||
|
||||
// Get total count
|
||||
std::string totalCountQuery = "SELECT COUNT(*) as count FROM bug_reports;";
|
||||
auto totalCountResult = ExecuteSelect(totalCountQuery);
|
||||
uint32_t totalRecords = totalCountResult->next() ? totalCountResult->getUInt("count") : 0;
|
||||
|
||||
// Get filtered count
|
||||
uint32_t filteredRecords = totalRecords;
|
||||
if (!search.empty()) {
|
||||
std::string filteredCountQuery = "SELECT COUNT(*) as count FROM bug_reports WHERE body LIKE CONCAT('%', ?, '%') OR other_player_id LIKE CONCAT('%', ?, '%');";
|
||||
auto filteredCountResult = ExecuteSelect(filteredCountQuery, search, search);
|
||||
filteredRecords = filteredCountResult->next() ? filteredCountResult->getUInt("count") : 0;
|
||||
}
|
||||
|
||||
// Execute main query
|
||||
std::unique_ptr<sql::ResultSet> result;
|
||||
if (!search.empty()) {
|
||||
result = ExecuteSelect(mainQuery, search, search, start, length);
|
||||
} else {
|
||||
result = ExecuteSelect(mainQuery, start, length);
|
||||
}
|
||||
|
||||
// Build response JSON
|
||||
nlohmann::json reportsArray = nlohmann::json::array();
|
||||
|
||||
while (result->next()) {
|
||||
nlohmann::json report = {
|
||||
{"id", result->getUInt("id")},
|
||||
{"other_player_id", result->getString("other_player_id")},
|
||||
{"client_version", result->getString("client_version")},
|
||||
{"selection", result->getString("selection")},
|
||||
{"submitted", result->getString("submitted")},
|
||||
{"body", result->getString("body")}
|
||||
};
|
||||
reportsArray.push_back(report);
|
||||
}
|
||||
|
||||
nlohmann::json response = {
|
||||
{"draw", 0},
|
||||
{"recordsTotal", totalRecords},
|
||||
{"recordsFiltered", filteredRecords},
|
||||
{"data", reportsArray}
|
||||
};
|
||||
|
||||
return response.dump();
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ std::vector<std::string> MySQLDatabase::GetApprovedCharacterNames() {
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
std::optional<ICharInfo::Info> CharInfoFromQueryResult(PreparedStmtResultSet& stmt) {
|
||||
std::optional<ICharInfo::Info> CharInfoFromQueryResult(std::unique_ptr<sql::ResultSet> stmt) {
|
||||
if (!stmt->next()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
@@ -31,13 +31,15 @@ std::optional<ICharInfo::Info> CharInfoFromQueryResult(PreparedStmtResultSet& st
|
||||
}
|
||||
|
||||
std::optional<ICharInfo::Info> MySQLDatabase::GetCharacterInfo(const LWOOBJID charId) {
|
||||
auto result = ExecuteSelect("SELECT name, pending_name, needs_rename, prop_clone_id, permission_map, id, account_id FROM charinfo WHERE id = ? LIMIT 1;", charId);
|
||||
return CharInfoFromQueryResult(result);
|
||||
return CharInfoFromQueryResult(
|
||||
ExecuteSelect("SELECT name, pending_name, needs_rename, prop_clone_id, permission_map, id, account_id FROM charinfo WHERE id = ? LIMIT 1;", charId)
|
||||
);
|
||||
}
|
||||
|
||||
std::optional<ICharInfo::Info> MySQLDatabase::GetCharacterInfo(const std::string_view name) {
|
||||
auto result = ExecuteSelect("SELECT name, pending_name, needs_rename, prop_clone_id, permission_map, id, account_id FROM charinfo WHERE name = ? LIMIT 1;", name);
|
||||
return CharInfoFromQueryResult(result);
|
||||
return CharInfoFromQueryResult(
|
||||
ExecuteSelect("SELECT name, pending_name, needs_rename, prop_clone_id, permission_map, id, account_id FROM charinfo WHERE name = ? LIMIT 1;", name)
|
||||
);
|
||||
}
|
||||
|
||||
std::vector<LWOOBJID> MySQLDatabase::GetAccountCharacterIds(const LWOOBJID accountId) {
|
||||
@@ -52,6 +54,11 @@ std::vector<LWOOBJID> MySQLDatabase::GetAccountCharacterIds(const LWOOBJID accou
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
uint32_t MySQLDatabase::GetCharacterCount() {
|
||||
auto res = ExecuteSelect("SELECT COUNT(*) as count FROM charinfo;");
|
||||
return res->next() ? res->getUInt("count") : 0;
|
||||
}
|
||||
|
||||
void MySQLDatabase::InsertNewCharacter(const ICharInfo::Info info) {
|
||||
ExecuteInsert(
|
||||
"INSERT INTO `charinfo`(`id`, `account_id`, `name`, `pending_name`, `needs_rename`, `last_login`) VALUES (?,?,?,?,?,?)",
|
||||
|
||||
@@ -17,3 +17,75 @@ void MySQLDatabase::UpdateCharacterXml(const LWOOBJID charId, const std::string_
|
||||
void MySQLDatabase::InsertCharacterXml(const LWOOBJID characterId, const std::string_view lxfml) {
|
||||
ExecuteInsert("INSERT INTO `charxml` (`id`, `xml_data`) VALUES (?,?)", characterId, lxfml);
|
||||
}
|
||||
|
||||
#include "json.hpp"
|
||||
|
||||
std::string MySQLDatabase::GetCharactersTable(uint32_t start, uint32_t length, const std::string_view search, uint32_t orderColumn, bool orderAsc) {
|
||||
// Build base query
|
||||
std::string baseQuery = "SELECT c.id, c.name, c.account_id, c.last_login, a.name as account_name FROM charinfo c JOIN accounts a ON c.account_id = a.id";
|
||||
std::string whereClause;
|
||||
std::string orderClause;
|
||||
|
||||
// Add search filter if provided
|
||||
if (!search.empty()) {
|
||||
whereClause = " WHERE c.name LIKE CONCAT('%', ?, '%')";
|
||||
}
|
||||
|
||||
// Map column indices to database columns
|
||||
std::string orderColumnName = "c.id";
|
||||
switch (orderColumn) {
|
||||
case 0: orderColumnName = "c.id"; break;
|
||||
case 1: orderColumnName = "c.name"; break;
|
||||
case 2: orderColumnName = "a.name"; break;
|
||||
case 3: orderColumnName = "c.last_login"; break;
|
||||
default: orderColumnName = "c.id";
|
||||
}
|
||||
|
||||
orderClause = " ORDER BY " + orderColumnName + (orderAsc ? " ASC" : " DESC");
|
||||
|
||||
// Build the main query
|
||||
std::string mainQuery = baseQuery + whereClause + orderClause + " LIMIT ?, ?;";
|
||||
|
||||
// Get total count
|
||||
std::string totalCountQuery = "SELECT COUNT(*) as count FROM charinfo;";
|
||||
auto totalCountResult = ExecuteSelect(totalCountQuery);
|
||||
uint32_t totalRecords = totalCountResult->next() ? totalCountResult->getUInt("count") : 0;
|
||||
|
||||
// Get filtered count
|
||||
uint32_t filteredRecords = totalRecords;
|
||||
if (!search.empty()) {
|
||||
std::string filteredCountQuery = "SELECT COUNT(*) as count FROM charinfo WHERE name LIKE CONCAT('%', ?, '%');";
|
||||
auto filteredCountResult = ExecuteSelect(filteredCountQuery, search);
|
||||
filteredRecords = filteredCountResult->next() ? filteredCountResult->getUInt("count") : 0;
|
||||
}
|
||||
|
||||
// Execute main query
|
||||
std::unique_ptr<sql::ResultSet> result;
|
||||
if (!search.empty()) {
|
||||
result = ExecuteSelect(mainQuery, search, start, length);
|
||||
} else {
|
||||
result = ExecuteSelect(mainQuery, start, length);
|
||||
}
|
||||
|
||||
// Build response JSON
|
||||
nlohmann::json charactersArray = nlohmann::json::array();
|
||||
|
||||
while (result->next()) {
|
||||
nlohmann::json character = {
|
||||
{"id", result->getUInt64("id")},
|
||||
{"name", result->getString("name")},
|
||||
{"account_name", result->getString("account_name")},
|
||||
{"last_login", result->getUInt64("last_login")}
|
||||
};
|
||||
charactersArray.push_back(character);
|
||||
}
|
||||
|
||||
nlohmann::json response = {
|
||||
{"draw", 0},
|
||||
{"recordsTotal", totalRecords},
|
||||
{"recordsFiltered", filteredRecords},
|
||||
{"data", charactersArray}
|
||||
};
|
||||
|
||||
return response.dump();
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ std::optional<uint32_t> MySQLDatabase::GetDonationTotal(const uint32_t activityI
|
||||
return donation_total->getUInt("donation_total");
|
||||
}
|
||||
|
||||
std::vector<ILeaderboard::Entry> ProcessQuery(PreparedStmtResultSet& rows) {
|
||||
std::vector<ILeaderboard::Entry> ProcessQuery(UniqueResultSet& rows) {
|
||||
std::vector<ILeaderboard::Entry> entries;
|
||||
entries.reserve(rows->rowsCount());
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ std::vector<MailInfo> MySQLDatabase::GetMailForPlayer(const LWOOBJID characterId
|
||||
}
|
||||
|
||||
std::optional<MailInfo> MySQLDatabase::GetMail(const uint64_t mailId) {
|
||||
auto res = ExecuteSelect("SELECT attachment_lot, attachment_count, receiver_id FROM mail WHERE id=? LIMIT 1;", mailId);
|
||||
auto res = ExecuteSelect("SELECT attachment_lot, attachment_count FROM mail WHERE id=? LIMIT 1;", mailId);
|
||||
|
||||
if (!res->next()) {
|
||||
return std::nullopt;
|
||||
@@ -57,7 +57,6 @@ std::optional<MailInfo> MySQLDatabase::GetMail(const uint64_t mailId) {
|
||||
MailInfo toReturn;
|
||||
toReturn.itemLOT = res->getInt("attachment_lot");
|
||||
toReturn.itemCount = res->getInt("attachment_count");
|
||||
toReturn.receiverId = res->getUInt64("receiver_id");
|
||||
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
@@ -9,3 +9,77 @@ std::optional<bool> MySQLDatabase::IsPlaykeyActive(const int32_t playkeyId) {
|
||||
|
||||
return keyCheckRes->getBoolean("active");
|
||||
}
|
||||
|
||||
#include "json.hpp"
|
||||
|
||||
std::string MySQLDatabase::GetPlayKeysTable(uint32_t start, uint32_t length, const std::string_view search, uint32_t orderColumn, bool orderAsc) {
|
||||
// Build base query
|
||||
std::string baseQuery = "SELECT id, key_string, key_uses, created_at, active FROM play_keys";
|
||||
std::string whereClause;
|
||||
std::string orderClause;
|
||||
|
||||
// Add search filter if provided
|
||||
if (!search.empty()) {
|
||||
whereClause = " WHERE key_string LIKE CONCAT('%', ?, '%')";
|
||||
}
|
||||
|
||||
// Map column indices to database columns
|
||||
std::string orderColumnName = "id";
|
||||
switch (orderColumn) {
|
||||
case 0: orderColumnName = "id"; break;
|
||||
case 1: orderColumnName = "key_string"; break;
|
||||
case 2: orderColumnName = "key_uses"; break;
|
||||
case 3: orderColumnName = "created_at"; break;
|
||||
case 4: orderColumnName = "active"; break;
|
||||
default: orderColumnName = "id";
|
||||
}
|
||||
|
||||
orderClause = " ORDER BY " + orderColumnName + (orderAsc ? " ASC" : " DESC");
|
||||
|
||||
// Build the main query
|
||||
std::string mainQuery = baseQuery + whereClause + orderClause + " LIMIT ?, ?;";
|
||||
|
||||
// Get total count
|
||||
std::string totalCountQuery = "SELECT COUNT(*) as count FROM play_keys;";
|
||||
auto totalCountResult = ExecuteSelect(totalCountQuery);
|
||||
uint32_t totalRecords = totalCountResult->next() ? totalCountResult->getUInt("count") : 0;
|
||||
|
||||
// Get filtered count
|
||||
uint32_t filteredRecords = totalRecords;
|
||||
if (!search.empty()) {
|
||||
std::string filteredCountQuery = "SELECT COUNT(*) as count FROM play_keys WHERE key_string LIKE CONCAT('%', ?, '%');";
|
||||
auto filteredCountResult = ExecuteSelect(filteredCountQuery, search);
|
||||
filteredRecords = filteredCountResult->next() ? filteredCountResult->getUInt("count") : 0;
|
||||
}
|
||||
|
||||
// Execute main query
|
||||
std::unique_ptr<sql::ResultSet> result;
|
||||
if (!search.empty()) {
|
||||
result = ExecuteSelect(mainQuery, search, start, length);
|
||||
} else {
|
||||
result = ExecuteSelect(mainQuery, start, length);
|
||||
}
|
||||
|
||||
// Build response JSON
|
||||
nlohmann::json keysArray = nlohmann::json::array();
|
||||
|
||||
while (result->next()) {
|
||||
nlohmann::json key = {
|
||||
{"id", result->getUInt("id")},
|
||||
{"key_string", result->getString("key_string")},
|
||||
{"key_uses", result->getUInt("key_uses")},
|
||||
{"created_at", result->getString("created_at")},
|
||||
{"active", result->getBoolean("active")}
|
||||
};
|
||||
keysArray.push_back(key);
|
||||
}
|
||||
|
||||
nlohmann::json response = {
|
||||
{"draw", 0},
|
||||
{"recordsTotal", totalRecords},
|
||||
{"recordsFiltered", filteredRecords},
|
||||
{"data", keysArray}
|
||||
};
|
||||
|
||||
return response.dump();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#include "MySQLDatabase.h"
|
||||
#include "ePropertySortType.h"
|
||||
|
||||
IProperty::Info ReadPropertyInfo(PreparedStmtResultSet& result) {
|
||||
IProperty::Info ReadPropertyInfo(UniqueResultSet& result) {
|
||||
IProperty::Info info;
|
||||
info.id = result->getUInt64("id");
|
||||
info.ownerId = result->getInt64("owner_id");
|
||||
@@ -18,10 +18,10 @@ IProperty::Info ReadPropertyInfo(PreparedStmtResultSet& result) {
|
||||
return info;
|
||||
}
|
||||
|
||||
IProperty::PropertyEntranceResult MySQLDatabase::GetProperties(const IProperty::PropertyLookup& params) {
|
||||
IProperty::PropertyEntranceResult result;
|
||||
std::optional<IProperty::PropertyEntranceResult> MySQLDatabase::GetProperties(const IProperty::PropertyLookup& params) {
|
||||
std::optional<IProperty::PropertyEntranceResult> result;
|
||||
std::string query;
|
||||
PreparedStmtResultSet properties;
|
||||
std::unique_ptr<sql::ResultSet> properties;
|
||||
|
||||
if (params.sortChoice == SORT_TYPE_FEATURED || params.sortChoice == SORT_TYPE_FRIENDS) {
|
||||
query = R"QUERY(
|
||||
@@ -73,7 +73,8 @@ IProperty::PropertyEntranceResult MySQLDatabase::GetProperties(const IProperty::
|
||||
params.playerId
|
||||
);
|
||||
if (count->next()) {
|
||||
result.totalEntriesMatchingQuery = count->getUInt("count");
|
||||
if (!result) result = IProperty::PropertyEntranceResult();
|
||||
result->totalEntriesMatchingQuery = count->getUInt("count");
|
||||
}
|
||||
} else {
|
||||
if (params.sortChoice == SORT_TYPE_REPUTATION) {
|
||||
@@ -126,12 +127,14 @@ IProperty::PropertyEntranceResult MySQLDatabase::GetProperties(const IProperty::
|
||||
params.playerSort
|
||||
);
|
||||
if (count->next()) {
|
||||
result.totalEntriesMatchingQuery = count->getUInt("count");
|
||||
if (!result) result = IProperty::PropertyEntranceResult();
|
||||
result->totalEntriesMatchingQuery = count->getUInt("count");
|
||||
}
|
||||
}
|
||||
|
||||
while (properties->next()) {
|
||||
result.entries.push_back(ReadPropertyInfo(properties));
|
||||
if (!result) result = IProperty::PropertyEntranceResult();
|
||||
result->entries.push_back(ReadPropertyInfo(properties));
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -195,3 +198,79 @@ std::optional<IProperty::Info> MySQLDatabase::GetPropertyInfo(const LWOOBJID id)
|
||||
|
||||
return ReadPropertyInfo(propertyEntry);
|
||||
}
|
||||
|
||||
#include "json.hpp"
|
||||
|
||||
std::string MySQLDatabase::GetPropertiesTable(uint32_t start, uint32_t length, const std::string_view search, uint32_t orderColumn, bool orderAsc) {
|
||||
// Build base query
|
||||
std::string baseQuery = "SELECT id, owner_id, name, mod_approved, reputation, zone_id FROM properties";
|
||||
std::string whereClause;
|
||||
std::string orderClause;
|
||||
|
||||
// Add search filter if provided
|
||||
if (!search.empty()) {
|
||||
whereClause = " WHERE name LIKE CONCAT('%', ?, '%')";
|
||||
}
|
||||
|
||||
// Map column indices to database columns
|
||||
std::string orderColumnName = "id";
|
||||
switch (orderColumn) {
|
||||
case 0: orderColumnName = "id"; break;
|
||||
case 1: orderColumnName = "name"; break;
|
||||
case 2: orderColumnName = "owner_id"; break;
|
||||
case 3: orderColumnName = "mod_approved"; break;
|
||||
case 4: orderColumnName = "reputation"; break;
|
||||
case 5: orderColumnName = "zone_id"; break;
|
||||
default: orderColumnName = "id";
|
||||
}
|
||||
|
||||
orderClause = " ORDER BY " + orderColumnName + (orderAsc ? " ASC" : " DESC");
|
||||
|
||||
// Build the main query
|
||||
std::string mainQuery = baseQuery + whereClause + orderClause + " LIMIT ?, ?;";
|
||||
|
||||
// Get total count
|
||||
std::string totalCountQuery = "SELECT COUNT(*) as count FROM properties;";
|
||||
auto totalCountResult = ExecuteSelect(totalCountQuery);
|
||||
uint32_t totalRecords = totalCountResult->next() ? totalCountResult->getUInt("count") : 0;
|
||||
|
||||
// Get filtered count
|
||||
uint32_t filteredRecords = totalRecords;
|
||||
if (!search.empty()) {
|
||||
std::string filteredCountQuery = "SELECT COUNT(*) as count FROM properties WHERE name LIKE CONCAT('%', ?, '%');";
|
||||
auto filteredCountResult = ExecuteSelect(filteredCountQuery, search);
|
||||
filteredRecords = filteredCountResult->next() ? filteredCountResult->getUInt("count") : 0;
|
||||
}
|
||||
|
||||
// Execute main query
|
||||
std::unique_ptr<sql::ResultSet> result;
|
||||
if (!search.empty()) {
|
||||
result = ExecuteSelect(mainQuery, search, start, length);
|
||||
} else {
|
||||
result = ExecuteSelect(mainQuery, start, length);
|
||||
}
|
||||
|
||||
// Build response JSON
|
||||
nlohmann::json propertiesArray = nlohmann::json::array();
|
||||
|
||||
while (result->next()) {
|
||||
nlohmann::json property = {
|
||||
{"id", result->getUInt64("id")},
|
||||
{"owner_id", result->getUInt64("owner_id")},
|
||||
{"name", result->getString("name")},
|
||||
{"mod_approved", result->getBoolean("mod_approved")},
|
||||
{"reputation", result->getUInt64("reputation")},
|
||||
{"zone_id", result->getUInt("zone_id")}
|
||||
};
|
||||
propertiesArray.push_back(property);
|
||||
}
|
||||
|
||||
nlohmann::json response = {
|
||||
{"draw", 0},
|
||||
{"recordsTotal", totalRecords},
|
||||
{"recordsFiltered", filteredRecords},
|
||||
{"data", propertiesArray}
|
||||
};
|
||||
|
||||
return response.dump();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "MySQLDatabase.h"
|
||||
|
||||
IUgc::Model ReadModel(PreparedStmtResultSet& result) {
|
||||
IUgc::Model ReadModel(UniqueResultSet& result) {
|
||||
IUgc::Model model;
|
||||
|
||||
// blob is owned by the query, so we need to do a deep copy :/
|
||||
|
||||
@@ -58,6 +58,7 @@ public:
|
||||
std::optional<IAccounts::Info> GetAccountInfo(const std::string_view username) override;
|
||||
void InsertNewCharacter(const ICharInfo::Info info) override;
|
||||
void InsertCharacterXml(const LWOOBJID accountId, const std::string_view lxfml) override;
|
||||
std::string GetCharactersTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) override;
|
||||
std::vector<LWOOBJID> GetAccountCharacterIds(LWOOBJID accountId) override;
|
||||
void DeleteCharacter(const LWOOBJID characterId) override;
|
||||
void SetCharacterName(const LWOOBJID characterId, const std::string_view name) override;
|
||||
@@ -77,6 +78,7 @@ public:
|
||||
void RemoveModel(const LWOOBJID& modelId) override;
|
||||
void UpdatePerformanceCost(const LWOZONEID& zoneId, const float performanceCost) override;
|
||||
void InsertNewBugReport(const IBugReports::Info& info) override;
|
||||
std::string GetBugReportsTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) override;
|
||||
void InsertCheatDetection(const IPlayerCheatDetections::Info& info) override;
|
||||
void InsertNewMail(const MailInfo& mail) override;
|
||||
void InsertNewUgcModel(
|
||||
@@ -101,6 +103,7 @@ public:
|
||||
void InsertDefaultPersistentId() override;
|
||||
std::optional<uint32_t> GetDonationTotal(const uint32_t activityId) override;
|
||||
std::optional<bool> IsPlaykeyActive(const int32_t playkeyId) override;
|
||||
std::string GetPlayKeysTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) override;
|
||||
std::vector<IUgc::Model> GetUgcModels(const LWOOBJID& propertyId) override;
|
||||
void AddIgnore(const LWOOBJID playerId, const LWOOBJID ignoredPlayerId) override;
|
||||
void RemoveIgnore(const LWOOBJID playerId, const LWOOBJID ignoredPlayerId) override;
|
||||
@@ -111,7 +114,7 @@ public:
|
||||
std::string GetBehavior(const LWOOBJID behaviorId) override;
|
||||
void RemoveBehavior(const LWOOBJID characterId) override;
|
||||
void UpdateAccountGmLevel(const uint32_t accountId, const eGameMasterLevel gmLevel) override;
|
||||
IProperty::PropertyEntranceResult GetProperties(const IProperty::PropertyLookup& params) override;
|
||||
std::optional<IProperty::PropertyEntranceResult> GetProperties(const IProperty::PropertyLookup& params) override;
|
||||
std::vector<ILeaderboard::Entry> GetDescendingLeaderboard(const uint32_t activityId) override;
|
||||
std::vector<ILeaderboard::Entry> GetAscendingLeaderboard(const uint32_t activityId) override;
|
||||
std::vector<ILeaderboard::Entry> GetNsLeaderboard(const uint32_t activityId) override;
|
||||
@@ -124,10 +127,19 @@ public:
|
||||
void InsertUgcBuild(const std::string& modules, const LWOOBJID bigId, const std::optional<LWOOBJID> characterId) override;
|
||||
void DeleteUgcBuild(const LWOOBJID bigId) override;
|
||||
uint32_t GetAccountCount() override;
|
||||
uint32_t GetCharacterCount() override;
|
||||
void RecordFailedAttempt(const uint32_t accountId) override;
|
||||
void ClearFailedAttempts(const uint32_t accountId) override;
|
||||
void SetLockout(const uint32_t accountId, const int64_t lockoutUntil) override;
|
||||
bool IsLockedOut(const uint32_t accountId) override;
|
||||
uint8_t GetFailedAttempts(const uint32_t accountId) override;
|
||||
nlohmann::json GetAccountsTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) override;
|
||||
nlohmann::json GetAccountById(uint32_t accountId) override;
|
||||
bool IsNameInUse(const std::string_view name) override;
|
||||
std::optional<IPropertyContents::Model> GetModel(const LWOOBJID modelID) override;
|
||||
std::optional<IUgc::Model> GetUgcModel(const LWOOBJID ugcId) override;
|
||||
std::optional<IProperty::Info> GetPropertyInfo(const LWOOBJID id) override;
|
||||
std::string GetPropertiesTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) override;
|
||||
private:
|
||||
CppSQLite3Statement CreatePreppedStmt(const std::string& query);
|
||||
|
||||
@@ -170,91 +182,91 @@ private:
|
||||
|
||||
template<>
|
||||
inline void SetParam(PreppedStmtRef stmt, const int index, const std::string_view param) {
|
||||
LOG_DEBUG("%s", param.data());
|
||||
LOG("%s", param.data());
|
||||
stmt.bind(index, param.data());
|
||||
}
|
||||
|
||||
template<>
|
||||
inline void SetParam(PreppedStmtRef stmt, const int index, const char* param) {
|
||||
LOG_DEBUG("%s", param);
|
||||
LOG("%s", param);
|
||||
stmt.bind(index, param);
|
||||
}
|
||||
|
||||
template<>
|
||||
inline void SetParam(PreppedStmtRef stmt, const int index, const std::string param) {
|
||||
LOG_DEBUG("%s", param.c_str());
|
||||
LOG("%s", param.c_str());
|
||||
stmt.bind(index, param.c_str());
|
||||
}
|
||||
|
||||
template<>
|
||||
inline void SetParam(PreppedStmtRef stmt, const int index, const int8_t param) {
|
||||
LOG_DEBUG("%u", param);
|
||||
LOG("%u", param);
|
||||
stmt.bind(index, param);
|
||||
}
|
||||
|
||||
template<>
|
||||
inline void SetParam(PreppedStmtRef stmt, const int index, const uint8_t param) {
|
||||
LOG_DEBUG("%d", param);
|
||||
LOG("%d", param);
|
||||
stmt.bind(index, param);
|
||||
}
|
||||
|
||||
template<>
|
||||
inline void SetParam(PreppedStmtRef stmt, const int index, const int16_t param) {
|
||||
LOG_DEBUG("%u", param);
|
||||
LOG("%u", param);
|
||||
stmt.bind(index, param);
|
||||
}
|
||||
|
||||
template<>
|
||||
inline void SetParam(PreppedStmtRef stmt, const int index, const uint16_t param) {
|
||||
LOG_DEBUG("%d", param);
|
||||
LOG("%d", param);
|
||||
stmt.bind(index, param);
|
||||
}
|
||||
|
||||
template<>
|
||||
inline void SetParam(PreppedStmtRef stmt, const int index, const uint32_t param) {
|
||||
LOG_DEBUG("%u", param);
|
||||
LOG("%u", param);
|
||||
stmt.bind(index, static_cast<int32_t>(param));
|
||||
}
|
||||
|
||||
template<>
|
||||
inline void SetParam(PreppedStmtRef stmt, const int index, const int32_t param) {
|
||||
LOG_DEBUG("%d", param);
|
||||
LOG("%d", param);
|
||||
stmt.bind(index, param);
|
||||
}
|
||||
|
||||
template<>
|
||||
inline void SetParam(PreppedStmtRef stmt, const int index, const int64_t param) {
|
||||
LOG_DEBUG("%llu", param);
|
||||
LOG("%llu", param);
|
||||
stmt.bind(index, static_cast<sqlite_int64>(param));
|
||||
}
|
||||
|
||||
template<>
|
||||
inline void SetParam(PreppedStmtRef stmt, const int index, const uint64_t param) {
|
||||
LOG_DEBUG("%llu", param);
|
||||
LOG("%llu", param);
|
||||
stmt.bind(index, static_cast<sqlite_int64>(param));
|
||||
}
|
||||
|
||||
template<>
|
||||
inline void SetParam(PreppedStmtRef stmt, const int index, const float param) {
|
||||
LOG_DEBUG("%f", param);
|
||||
LOG("%f", param);
|
||||
stmt.bind(index, param);
|
||||
}
|
||||
|
||||
template<>
|
||||
inline void SetParam(PreppedStmtRef stmt, const int index, const double param) {
|
||||
LOG_DEBUG("%f", param);
|
||||
LOG("%f", param);
|
||||
stmt.bind(index, param);
|
||||
}
|
||||
|
||||
template<>
|
||||
inline void SetParam(PreppedStmtRef stmt, const int index, const bool param) {
|
||||
LOG_DEBUG("%d", param);
|
||||
LOG("%d", param);
|
||||
stmt.bind(index, param);
|
||||
}
|
||||
|
||||
template<>
|
||||
inline void SetParam(PreppedStmtRef stmt, const int index, const std::istream* param) {
|
||||
LOG_DEBUG("Blob");
|
||||
LOG("Blob");
|
||||
// This is the one time you will ever see me use const_cast.
|
||||
std::stringstream stream;
|
||||
stream << param->rdbuf();
|
||||
@@ -264,10 +276,10 @@ inline void SetParam(PreppedStmtRef stmt, const int index, const std::istream* p
|
||||
template<>
|
||||
inline void SetParam(PreppedStmtRef stmt, const int index, const std::optional<uint32_t> param) {
|
||||
if (param) {
|
||||
LOG_DEBUG("%d", param.value());
|
||||
LOG("%d", param.value());
|
||||
stmt.bind(index, static_cast<int>(param.value()));
|
||||
} else {
|
||||
LOG_DEBUG("Null");
|
||||
LOG("Null");
|
||||
stmt.bindNull(index);
|
||||
}
|
||||
}
|
||||
@@ -275,10 +287,10 @@ inline void SetParam(PreppedStmtRef stmt, const int index, const std::optional<u
|
||||
template<>
|
||||
inline void SetParam(PreppedStmtRef stmt, const int index, const std::optional<LWOOBJID> param) {
|
||||
if (param) {
|
||||
LOG_DEBUG("%d", param.value());
|
||||
LOG("%d", param.value());
|
||||
stmt.bind(index, static_cast<sqlite_int64>(param.value()));
|
||||
} else {
|
||||
LOG_DEBUG("Null");
|
||||
LOG("Null");
|
||||
stmt.bindNull(index);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "eGameMasterLevel.h"
|
||||
#include "Database.h"
|
||||
#include "json.hpp"
|
||||
|
||||
std::optional<IAccounts::Info> SQLiteDatabase::GetAccountInfo(const std::string_view username) {
|
||||
auto [_, result] = ExecuteSelect("SELECT * FROM accounts WHERE name = ? LIMIT 1", username);
|
||||
@@ -48,3 +49,137 @@ uint32_t SQLiteDatabase::GetAccountCount() {
|
||||
|
||||
return res.getIntField("count");
|
||||
}
|
||||
void SQLiteDatabase::RecordFailedAttempt(const uint32_t accountId) {
|
||||
ExecuteUpdate("UPDATE accounts SET failed_attempts = failed_attempts + 1 WHERE id = ?;", accountId);
|
||||
}
|
||||
|
||||
void SQLiteDatabase::ClearFailedAttempts(const uint32_t accountId) {
|
||||
ExecuteUpdate("UPDATE accounts SET failed_attempts = 0, lockout_time = NULL, last_login = CURRENT_TIMESTAMP WHERE id = ?;", accountId);
|
||||
}
|
||||
|
||||
void SQLiteDatabase::SetLockout(const uint32_t accountId, const int64_t lockoutUntil) {
|
||||
ExecuteUpdate("UPDATE accounts SET lockout_time = datetime(?, 'unixepoch') WHERE id = ?;", lockoutUntil, accountId);
|
||||
}
|
||||
|
||||
bool SQLiteDatabase::IsLockedOut(const uint32_t accountId) {
|
||||
auto [_, result] = ExecuteSelect("SELECT lockout_time FROM accounts WHERE id = ?;", accountId);
|
||||
if (result.eof()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const char* lockoutTime = result.getStringField("lockout_time");
|
||||
if (lockoutTime == nullptr || strlen(lockoutTime) == 0 || strcmp(lockoutTime, "0") == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If lockout_time is set and in the future, account is locked
|
||||
// For now, simplified check - if lockout_time exists, it's locked
|
||||
return true;
|
||||
}
|
||||
|
||||
uint8_t SQLiteDatabase::GetFailedAttempts(const uint32_t accountId) {
|
||||
auto [_, result] = ExecuteSelect("SELECT failed_attempts FROM accounts WHERE id = ?;", accountId);
|
||||
if (result.eof()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return result.getIntField("failed_attempts");
|
||||
}
|
||||
|
||||
nlohmann::json SQLiteDatabase::GetAccountsTable(uint32_t start, uint32_t length, const std::string_view search, uint32_t orderColumn, bool orderAsc) {
|
||||
// Build base query
|
||||
std::string baseQuery = "SELECT id, name, banned, locked, gm_level, mute_expire, created_at FROM accounts";
|
||||
std::string whereClause;
|
||||
std::string orderClause;
|
||||
|
||||
// Add search filter if provided
|
||||
if (!search.empty()) {
|
||||
whereClause = " WHERE name LIKE '%' || ? || '%'";
|
||||
}
|
||||
|
||||
// Map column indices to database columns
|
||||
std::string orderColumnName = "id";
|
||||
switch (orderColumn) {
|
||||
case 0: orderColumnName = "id"; break;
|
||||
case 1: orderColumnName = "name"; break;
|
||||
case 2: orderColumnName = "banned"; break;
|
||||
case 3: orderColumnName = "locked"; break;
|
||||
case 4: orderColumnName = "gm_level"; break;
|
||||
case 5: orderColumnName = "mute_expire"; break;
|
||||
case 6: orderColumnName = "created_at"; break;
|
||||
default: orderColumnName = "id";
|
||||
}
|
||||
|
||||
orderClause = " ORDER BY " + orderColumnName + (orderAsc ? " ASC" : " DESC");
|
||||
|
||||
// Build the main query
|
||||
std::string mainQuery = baseQuery + whereClause + orderClause + " LIMIT ? OFFSET ?;";
|
||||
|
||||
// Get total count
|
||||
std::string totalCountQuery = "SELECT COUNT(*) as count FROM accounts;";
|
||||
auto [_, totalCountResult] = ExecuteSelect(totalCountQuery);
|
||||
uint32_t totalRecords = totalCountResult.eof() ? 0 : totalCountResult.getIntField("count");
|
||||
|
||||
// Get filtered count
|
||||
uint32_t filteredRecords = totalRecords;
|
||||
if (!search.empty()) {
|
||||
std::string filteredCountQuery = "SELECT COUNT(*) as count FROM accounts WHERE name LIKE '%' || ? || '%';";
|
||||
auto [__, filteredCountResult] = ExecuteSelect(filteredCountQuery, search);
|
||||
filteredRecords = filteredCountResult.eof() ? 0 : filteredCountResult.getIntField("count");
|
||||
}
|
||||
|
||||
// Execute main query
|
||||
auto [stmt, result] = !search.empty() ?
|
||||
ExecuteSelect(mainQuery, search, length, start) :
|
||||
ExecuteSelect(mainQuery, length, start);
|
||||
|
||||
// Build response JSON
|
||||
nlohmann::json accountsArray = nlohmann::json::array();
|
||||
|
||||
while (!result.eof()) {
|
||||
nlohmann::json account = {
|
||||
{"id", result.getIntField("id")},
|
||||
{"name", result.getStringField("name")},
|
||||
{"banned", result.getIntField("banned")},
|
||||
{"locked", result.getIntField("locked")},
|
||||
{"gm_level", result.getIntField("gm_level")},
|
||||
{"mute_expire", result.getInt64Field("mute_expire")},
|
||||
{"created_at", result.getStringField("created_at")}
|
||||
};
|
||||
accountsArray.push_back(account);
|
||||
result.nextRow();
|
||||
}
|
||||
|
||||
nlohmann::json response = {
|
||||
{"draw", 1},
|
||||
{"recordsTotal", totalRecords},
|
||||
{"recordsFiltered", filteredRecords},
|
||||
{"data", accountsArray}
|
||||
};
|
||||
|
||||
return response;
|
||||
}
|
||||
nlohmann::json SQLiteDatabase::GetAccountById(uint32_t accountId) {
|
||||
try {
|
||||
auto [_, result] = ExecuteSelect("SELECT * FROM accounts WHERE id = ? LIMIT 1;", accountId);
|
||||
|
||||
if (result.eof()) {
|
||||
return nlohmann::json{{"error", "Account not found"}};
|
||||
}
|
||||
|
||||
nlohmann::json account = {
|
||||
{"id", result.getIntField("id")},
|
||||
{"name", result.getStringField("name")},
|
||||
{"banned", result.getIntField("banned")},
|
||||
{"locked", result.getIntField("locked")},
|
||||
{"gm_level", result.getIntField("gm_level")},
|
||||
{"mute_expire", result.getInt64Field("mute_expire")},
|
||||
{"created_at", result.getStringField("created_at")}
|
||||
};
|
||||
|
||||
return account;
|
||||
} catch (const CppSQLite3Exception& e) {
|
||||
LOG_DEBUG("SQLite Error: %s", e.errorMessage());
|
||||
return nlohmann::json{{"error", "Database error"}};
|
||||
}
|
||||
}
|
||||
@@ -4,3 +4,75 @@ void SQLiteDatabase::InsertNewBugReport(const IBugReports::Info& info) {
|
||||
ExecuteInsert("INSERT INTO `bug_reports`(body, client_version, other_player_id, selection, reporter_id) VALUES (?, ?, ?, ?, ?)",
|
||||
info.body, info.clientVersion, info.otherPlayer, info.selection, info.characterId);
|
||||
}
|
||||
|
||||
#include "json.hpp"
|
||||
|
||||
std::string SQLiteDatabase::GetBugReportsTable(uint32_t start, uint32_t length, const std::string_view search, uint32_t orderColumn, bool orderAsc) {
|
||||
// Build base query
|
||||
std::string baseQuery = "SELECT id, body, client_version, other_player_id, selection, submitted FROM bug_reports";
|
||||
std::string whereClause;
|
||||
std::string orderClause;
|
||||
|
||||
// Add search filter if provided
|
||||
if (!search.empty()) {
|
||||
whereClause = " WHERE body LIKE '%' || ? || '%' OR other_player_id LIKE '%' || ? || '%'";
|
||||
}
|
||||
|
||||
// Map column indices to database columns
|
||||
std::string orderColumnName = "id";
|
||||
switch (orderColumn) {
|
||||
case 0: orderColumnName = "id"; break;
|
||||
case 1: orderColumnName = "other_player_id"; break;
|
||||
case 2: orderColumnName = "client_version"; break;
|
||||
case 3: orderColumnName = "submitted"; break;
|
||||
default: orderColumnName = "id";
|
||||
}
|
||||
|
||||
orderClause = " ORDER BY " + orderColumnName + (orderAsc ? " ASC" : " DESC");
|
||||
|
||||
// Build the main query
|
||||
std::string mainQuery = baseQuery + whereClause + orderClause + " LIMIT ? OFFSET ?;";
|
||||
|
||||
// Get total count
|
||||
std::string totalCountQuery = "SELECT COUNT(*) as count FROM bug_reports;";
|
||||
auto [__, totalCountResult] = ExecuteSelect(totalCountQuery);
|
||||
uint32_t totalRecords = totalCountResult.eof() ? 0 : totalCountResult.getIntField("count");
|
||||
|
||||
// Get filtered count
|
||||
uint32_t filteredRecords = totalRecords;
|
||||
if (!search.empty()) {
|
||||
std::string filteredCountQuery = "SELECT COUNT(*) as count FROM bug_reports WHERE body LIKE '%' || ? || '%' OR other_player_id LIKE '%' || ? || '%';";
|
||||
auto [___, filteredCountResult] = ExecuteSelect(filteredCountQuery, search, search);
|
||||
filteredRecords = filteredCountResult.eof() ? 0 : filteredCountResult.getIntField("count");
|
||||
}
|
||||
|
||||
// Execute main query
|
||||
auto [stmt, result] = !search.empty() ?
|
||||
ExecuteSelect(mainQuery, search, search, length, start) :
|
||||
ExecuteSelect(mainQuery, length, start);
|
||||
|
||||
// Build response JSON
|
||||
nlohmann::json reportsArray = nlohmann::json::array();
|
||||
|
||||
while (!result.eof()) {
|
||||
nlohmann::json report = {
|
||||
{"id", result.getIntField("id")},
|
||||
{"other_player_id", result.getStringField("other_player_id")},
|
||||
{"client_version", result.getStringField("client_version")},
|
||||
{"selection", result.getStringField("selection")},
|
||||
{"submitted", result.getStringField("submitted")},
|
||||
{"body", result.getStringField("body")}
|
||||
};
|
||||
reportsArray.push_back(report);
|
||||
result.nextRow();
|
||||
}
|
||||
|
||||
nlohmann::json response = {
|
||||
{"draw", 0},
|
||||
{"recordsTotal", totalRecords},
|
||||
{"recordsFiltered", filteredRecords},
|
||||
{"data", reportsArray}
|
||||
};
|
||||
|
||||
return response.dump();
|
||||
}
|
||||
|
||||
@@ -55,6 +55,13 @@ std::vector<LWOOBJID> SQLiteDatabase::GetAccountCharacterIds(const LWOOBJID acco
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
uint32_t SQLiteDatabase::GetCharacterCount() {
|
||||
auto [_, res] = ExecuteSelect("SELECT COUNT(*) as count FROM charinfo;");
|
||||
if (res.eof()) return 0;
|
||||
|
||||
return res.getIntField("count");
|
||||
}
|
||||
|
||||
void SQLiteDatabase::InsertNewCharacter(const ICharInfo::Info info) {
|
||||
ExecuteInsert(
|
||||
"INSERT INTO `charinfo`(`id`, `account_id`, `name`, `pending_name`, `needs_rename`, `last_login`, `prop_clone_id`) VALUES (?,?,?,?,?,?,(SELECT IFNULL(MAX(`prop_clone_id`), 0) + 1 FROM `charinfo`))",
|
||||
|
||||
@@ -17,3 +17,73 @@ void SQLiteDatabase::UpdateCharacterXml(const LWOOBJID charId, const std::string
|
||||
void SQLiteDatabase::InsertCharacterXml(const LWOOBJID characterId, const std::string_view lxfml) {
|
||||
ExecuteInsert("INSERT INTO `charxml` (`id`, `xml_data`) VALUES (?,?)", characterId, lxfml);
|
||||
}
|
||||
|
||||
#include "json.hpp"
|
||||
|
||||
std::string SQLiteDatabase::GetCharactersTable(uint32_t start, uint32_t length, const std::string_view search, uint32_t orderColumn, bool orderAsc) {
|
||||
// Build base query
|
||||
std::string baseQuery = "SELECT c.id, c.name, c.account_id, c.last_login, a.name as account_name FROM charinfo c JOIN accounts a ON c.account_id = a.id";
|
||||
std::string whereClause;
|
||||
std::string orderClause;
|
||||
|
||||
// Add search filter if provided
|
||||
if (!search.empty()) {
|
||||
whereClause = " WHERE c.name LIKE '%' || ? || '%'";
|
||||
}
|
||||
|
||||
// Map column indices to database columns
|
||||
std::string orderColumnName = "c.id";
|
||||
switch (orderColumn) {
|
||||
case 0: orderColumnName = "c.id"; break;
|
||||
case 1: orderColumnName = "c.name"; break;
|
||||
case 2: orderColumnName = "a.name"; break;
|
||||
case 3: orderColumnName = "c.last_login"; break;
|
||||
default: orderColumnName = "c.id";
|
||||
}
|
||||
|
||||
orderClause = " ORDER BY " + orderColumnName + (orderAsc ? " ASC" : " DESC");
|
||||
|
||||
// Build the main query
|
||||
std::string mainQuery = baseQuery + whereClause + orderClause + " LIMIT ? OFFSET ?;";
|
||||
|
||||
// Get total count
|
||||
std::string totalCountQuery = "SELECT COUNT(*) as count FROM charinfo;";
|
||||
auto [__, totalCountResult] = ExecuteSelect(totalCountQuery);
|
||||
uint32_t totalRecords = totalCountResult.eof() ? 0 : totalCountResult.getIntField("count");
|
||||
|
||||
// Get filtered count
|
||||
uint32_t filteredRecords = totalRecords;
|
||||
if (!search.empty()) {
|
||||
std::string filteredCountQuery = "SELECT COUNT(*) as count FROM charinfo WHERE name LIKE '%' || ? || '%';";
|
||||
auto [___, filteredCountResult] = ExecuteSelect(filteredCountQuery, search);
|
||||
filteredRecords = filteredCountResult.eof() ? 0 : filteredCountResult.getIntField("count");
|
||||
}
|
||||
|
||||
// Execute main query
|
||||
auto [stmt, result] = !search.empty() ?
|
||||
ExecuteSelect(mainQuery, search, length, start) :
|
||||
ExecuteSelect(mainQuery, length, start);
|
||||
|
||||
// Build response JSON
|
||||
nlohmann::json charactersArray = nlohmann::json::array();
|
||||
|
||||
while (!result.eof()) {
|
||||
nlohmann::json character = {
|
||||
{"id", result.getInt64Field("id")},
|
||||
{"name", result.getStringField("name")},
|
||||
{"account_name", result.getStringField("account_name")},
|
||||
{"last_login", result.getInt64Field("last_login")}
|
||||
};
|
||||
charactersArray.push_back(character);
|
||||
result.nextRow();
|
||||
}
|
||||
|
||||
nlohmann::json response = {
|
||||
{"draw", 0},
|
||||
{"recordsTotal", totalRecords},
|
||||
{"recordsFiltered", filteredRecords},
|
||||
{"data", charactersArray}
|
||||
};
|
||||
|
||||
return response.dump();
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ std::vector<MailInfo> SQLiteDatabase::GetMailForPlayer(const LWOOBJID characterI
|
||||
}
|
||||
|
||||
std::optional<MailInfo> SQLiteDatabase::GetMail(const uint64_t mailId) {
|
||||
auto [_, res] = ExecuteSelect("SELECT attachment_lot, attachment_count, receiver_id FROM mail WHERE id=? LIMIT 1;", mailId);
|
||||
auto [_, res] = ExecuteSelect("SELECT attachment_lot, attachment_count FROM mail WHERE id=? LIMIT 1;", mailId);
|
||||
|
||||
if (res.eof()) {
|
||||
return std::nullopt;
|
||||
@@ -56,7 +56,6 @@ std::optional<MailInfo> SQLiteDatabase::GetMail(const uint64_t mailId) {
|
||||
MailInfo toReturn;
|
||||
toReturn.itemLOT = res.getIntField("attachment_lot");
|
||||
toReturn.itemCount = res.getIntField("attachment_count");
|
||||
toReturn.receiverId = res.getInt64Field("receiver_id");
|
||||
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
@@ -9,3 +9,75 @@ std::optional<bool> SQLiteDatabase::IsPlaykeyActive(const int32_t playkeyId) {
|
||||
|
||||
return keyCheckRes.getIntField("active");
|
||||
}
|
||||
|
||||
#include "json.hpp"
|
||||
|
||||
std::string SQLiteDatabase::GetPlayKeysTable(uint32_t start, uint32_t length, const std::string_view search, uint32_t orderColumn, bool orderAsc) {
|
||||
// Build base query
|
||||
std::string baseQuery = "SELECT id, key_string, key_uses, created_at, active FROM play_keys";
|
||||
std::string whereClause;
|
||||
std::string orderClause;
|
||||
|
||||
// Add search filter if provided
|
||||
if (!search.empty()) {
|
||||
whereClause = " WHERE key_string LIKE '%' || ? || '%'";
|
||||
}
|
||||
|
||||
// Map column indices to database columns
|
||||
std::string orderColumnName = "id";
|
||||
switch (orderColumn) {
|
||||
case 0: orderColumnName = "id"; break;
|
||||
case 1: orderColumnName = "key_string"; break;
|
||||
case 2: orderColumnName = "key_uses"; break;
|
||||
case 3: orderColumnName = "created_at"; break;
|
||||
case 4: orderColumnName = "active"; break;
|
||||
default: orderColumnName = "id";
|
||||
}
|
||||
|
||||
orderClause = " ORDER BY " + orderColumnName + (orderAsc ? " ASC" : " DESC");
|
||||
|
||||
// Build the main query
|
||||
std::string mainQuery = baseQuery + whereClause + orderClause + " LIMIT ? OFFSET ?;";
|
||||
|
||||
// Get total count
|
||||
std::string totalCountQuery = "SELECT COUNT(*) as count FROM play_keys;";
|
||||
auto [__, totalCountResult] = ExecuteSelect(totalCountQuery);
|
||||
uint32_t totalRecords = totalCountResult.eof() ? 0 : totalCountResult.getIntField("count");
|
||||
|
||||
// Get filtered count
|
||||
uint32_t filteredRecords = totalRecords;
|
||||
if (!search.empty()) {
|
||||
std::string filteredCountQuery = "SELECT COUNT(*) as count FROM play_keys WHERE key_string LIKE '%' || ? || '%';";
|
||||
auto [___, filteredCountResult] = ExecuteSelect(filteredCountQuery, search);
|
||||
filteredRecords = filteredCountResult.eof() ? 0 : filteredCountResult.getIntField("count");
|
||||
}
|
||||
|
||||
// Execute main query
|
||||
auto [stmt, result] = !search.empty() ?
|
||||
ExecuteSelect(mainQuery, search, length, start) :
|
||||
ExecuteSelect(mainQuery, length, start);
|
||||
|
||||
// Build response JSON
|
||||
nlohmann::json keysArray = nlohmann::json::array();
|
||||
|
||||
while (!result.eof()) {
|
||||
nlohmann::json key = {
|
||||
{"id", result.getIntField("id")},
|
||||
{"key_string", result.getStringField("key_string")},
|
||||
{"key_uses", result.getIntField("key_uses")},
|
||||
{"created_at", result.getStringField("created_at")},
|
||||
{"active", result.getIntField("active")}
|
||||
};
|
||||
keysArray.push_back(key);
|
||||
result.nextRow();
|
||||
}
|
||||
|
||||
nlohmann::json response = {
|
||||
{"draw", 0},
|
||||
{"recordsTotal", totalRecords},
|
||||
{"recordsFiltered", filteredRecords},
|
||||
{"data", keysArray}
|
||||
};
|
||||
|
||||
return response.dump();
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@ IProperty::Info ReadPropertyInfo(CppSQLite3Query& propertyEntry) {
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
IProperty::PropertyEntranceResult SQLiteDatabase::GetProperties(const IProperty::PropertyLookup& params) {
|
||||
IProperty::PropertyEntranceResult result;
|
||||
std::optional<IProperty::PropertyEntranceResult> SQLiteDatabase::GetProperties(const IProperty::PropertyLookup& params) {
|
||||
std::optional<IProperty::PropertyEntranceResult> result;
|
||||
std::string query;
|
||||
std::pair<CppSQLite3Statement, CppSQLite3Query> propertiesRes;
|
||||
|
||||
@@ -73,7 +73,8 @@ IProperty::PropertyEntranceResult SQLiteDatabase::GetProperties(const IProperty:
|
||||
params.playerId
|
||||
);
|
||||
if (!count.eof()) {
|
||||
result.totalEntriesMatchingQuery = count.getIntField("count");
|
||||
result = IProperty::PropertyEntranceResult();
|
||||
result->totalEntriesMatchingQuery = count.getIntField("count");
|
||||
}
|
||||
} else {
|
||||
if (params.sortChoice == SORT_TYPE_REPUTATION) {
|
||||
@@ -126,13 +127,15 @@ IProperty::PropertyEntranceResult SQLiteDatabase::GetProperties(const IProperty:
|
||||
params.playerSort
|
||||
);
|
||||
if (!count.eof()) {
|
||||
result.totalEntriesMatchingQuery = count.getIntField("count");
|
||||
result = IProperty::PropertyEntranceResult();
|
||||
result->totalEntriesMatchingQuery = count.getIntField("count");
|
||||
}
|
||||
}
|
||||
|
||||
auto& [_, properties] = propertiesRes;
|
||||
if (!properties.eof() && !result.has_value()) result = IProperty::PropertyEntranceResult();
|
||||
while (!properties.eof()) {
|
||||
result.entries.push_back(ReadPropertyInfo(properties));
|
||||
result->entries.push_back(ReadPropertyInfo(properties));
|
||||
properties.nextRow();
|
||||
}
|
||||
|
||||
@@ -197,3 +200,77 @@ std::optional<IProperty::Info> SQLiteDatabase::GetPropertyInfo(const LWOOBJID id
|
||||
|
||||
return ReadPropertyInfo(propertyEntry);
|
||||
}
|
||||
|
||||
#include "json.hpp"
|
||||
|
||||
std::string SQLiteDatabase::GetPropertiesTable(uint32_t start, uint32_t length, const std::string_view search, uint32_t orderColumn, bool orderAsc) {
|
||||
// Build base query
|
||||
std::string baseQuery = "SELECT id, owner_id, name, mod_approved, reputation, zone_id FROM properties";
|
||||
std::string whereClause;
|
||||
std::string orderClause;
|
||||
|
||||
// Add search filter if provided
|
||||
if (!search.empty()) {
|
||||
whereClause = " WHERE name LIKE '%' || ? || '%'";
|
||||
}
|
||||
|
||||
// Map column indices to database columns
|
||||
std::string orderColumnName = "id";
|
||||
switch (orderColumn) {
|
||||
case 0: orderColumnName = "id"; break;
|
||||
case 1: orderColumnName = "name"; break;
|
||||
case 2: orderColumnName = "owner_id"; break;
|
||||
case 3: orderColumnName = "mod_approved"; break;
|
||||
case 4: orderColumnName = "reputation"; break;
|
||||
case 5: orderColumnName = "zone_id"; break;
|
||||
default: orderColumnName = "id";
|
||||
}
|
||||
|
||||
orderClause = " ORDER BY " + orderColumnName + (orderAsc ? " ASC" : " DESC");
|
||||
|
||||
// Build the main query
|
||||
std::string mainQuery = baseQuery + whereClause + orderClause + " LIMIT ? OFFSET ?;";
|
||||
|
||||
// Get total count
|
||||
std::string totalCountQuery = "SELECT COUNT(*) as count FROM properties;";
|
||||
auto [__, totalCountResult] = ExecuteSelect(totalCountQuery);
|
||||
uint32_t totalRecords = totalCountResult.eof() ? 0 : totalCountResult.getIntField("count");
|
||||
|
||||
// Get filtered count
|
||||
uint32_t filteredRecords = totalRecords;
|
||||
if (!search.empty()) {
|
||||
std::string filteredCountQuery = "SELECT COUNT(*) as count FROM properties WHERE name LIKE '%' || ? || '%';";
|
||||
auto [___, filteredCountResult] = ExecuteSelect(filteredCountQuery, search);
|
||||
filteredRecords = filteredCountResult.eof() ? 0 : filteredCountResult.getIntField("count");
|
||||
}
|
||||
|
||||
// Execute main query
|
||||
auto [stmt, result] = !search.empty() ?
|
||||
ExecuteSelect(mainQuery, search, length, start) :
|
||||
ExecuteSelect(mainQuery, length, start);
|
||||
|
||||
// Build response JSON
|
||||
nlohmann::json propertiesArray = nlohmann::json::array();
|
||||
|
||||
while (!result.eof()) {
|
||||
nlohmann::json property = {
|
||||
{"id", result.getInt64Field("id")},
|
||||
{"owner_id", result.getInt64Field("owner_id")},
|
||||
{"name", result.getStringField("name")},
|
||||
{"mod_approved", result.getIntField("mod_approved")},
|
||||
{"reputation", result.getInt64Field("reputation")},
|
||||
{"zone_id", result.getIntField("zone_id")}
|
||||
};
|
||||
propertiesArray.push_back(property);
|
||||
result.nextRow();
|
||||
}
|
||||
|
||||
nlohmann::json response = {
|
||||
{"draw", 0},
|
||||
{"recordsTotal", totalRecords},
|
||||
{"recordsFiltered", filteredRecords},
|
||||
{"data", propertiesArray}
|
||||
};
|
||||
|
||||
return response.dump();
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user