Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 52 additions & 50 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,54 @@ concurrency:
cancel-in-progress: true

jobs:
rustfmt-check:
name: Rustfmt Check
runs-on: ubuntu-latest
timeout-minutes: 10
continue-on-error: false

steps:
- uses: actions/checkout@v4
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_VERSION }}
components: rustfmt
- name: Check formatting (jive-api)
working-directory: jive-api
run: |
cargo fmt --all -- --check

- name: Check formatting (jive-core)
working-directory: jive-core
run: |
cargo fmt --all -- --check

cargo-deny:
name: Cargo Deny Check
runs-on: ubuntu-latest
timeout-minutes: 10
continue-on-error: true

steps:
- uses: actions/checkout@v4
- name: Install cargo-deny
run: |
curl -sSfL https://github.com/EmbarkStudios/cargo-deny/releases/download/0.14.24/cargo-deny-0.14.24-x86_64-unknown-linux-musl.tar.gz | tar xz
Copy link

Copilot AI Sep 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Downloading and executing binaries from external URLs without verification poses a security risk. Consider using a GitHub Action like EmbarkStudios/cargo-deny-action or verify the binary checksum after download.

Suggested change
curl -sSfL https://github.com/EmbarkStudios/cargo-deny/releases/download/0.14.24/cargo-deny-0.14.24-x86_64-unknown-linux-musl.tar.gz | tar xz
set -e
VERSION=0.14.24
FILE="cargo-deny-${VERSION}-x86_64-unknown-linux-musl.tar.gz"
URL="https://github.com/EmbarkStudios/cargo-deny/releases/download/${VERSION}/${FILE}"
SHA256_URL="${URL}.sha256"
# Download tarball and checksum
curl -sSfL "$URL" -o "$FILE"
curl -sSfL "$SHA256_URL" -o "${FILE}.sha256"
# The .sha256 file contains the checksum and filename, but we need to match the filename
grep "$FILE" "${FILE}.sha256" > checksum.txt
sha256sum -c checksum.txt
# Extract and install
tar xzf "$FILE"

Copilot uses AI. Check for mistakes.

sudo mv cargo-deny*/cargo-deny /usr/local/bin/cargo-deny
cargo-deny --version
- name: Run cargo-deny (API)
working-directory: jive-api
run: |
set -o pipefail
cargo-deny check -c ../deny.toml 2>&1 | tee ../cargo-deny-output.txt || true

- name: Upload cargo-deny output
if: always()
uses: actions/upload-artifact@v4
with:
name: cargo-deny-output
path: cargo-deny-output.txt
flutter-test:
name: Flutter Tests
runs-on: ubuntu-latest
Expand Down Expand Up @@ -511,54 +559,7 @@ jobs:
path: field-compare-report.md
if-no-files-found: ignore

cargo-deny:
name: Cargo Deny Check
runs-on: ubuntu-latest
timeout-minutes: 10
continue-on-error: true # Non-blocking initially
steps:
- uses: actions/checkout@v4

- name: Install cargo-deny
run: cargo install cargo-deny --locked

- name: Run cargo-deny
working-directory: jive-api
run: |
cargo deny check 2>&1 | tee ../cargo-deny-output.txt || true

- name: Upload cargo-deny output
if: always()
uses: actions/upload-artifact@v4
with:
name: cargo-deny-output
path: cargo-deny-output.txt

rustfmt-check:
name: Rustfmt Check
runs-on: ubuntu-latest
timeout-minutes: 10
continue-on-error: true # Non-blocking initially
steps:
- uses: actions/checkout@v4

- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_VERSION }}
components: rustfmt

- name: Run rustfmt check
run: |
cd jive-api && cargo fmt --all -- --check 2>&1 | tee ../rustfmt-output.txt || true
cd ../jive-core && cargo fmt --all -- --check 2>&1 | tee -a ../rustfmt-output.txt || true

- name: Upload rustfmt output
if: always()
uses: actions/upload-artifact@v4
with:
name: rustfmt-output
path: rustfmt-output.txt


rust-api-clippy:
name: Rust API Clippy (blocking)
Expand Down Expand Up @@ -626,9 +627,10 @@ jobs:
echo "- Flutter Tests: ${{ needs.flutter-test.result }}" >> ci-summary.md
echo "- Rust Tests: ${{ needs.rust-test.result }}" >> ci-summary.md
echo "- Rust Core Check: ${{ needs.rust-core-check.result }}" >> ci-summary.md
echo "- Field Comparison: ${{ needs.field-compare.result }}" >> ci-summary.md
echo "- Rust API Clippy: ${{ needs.rust-api-clippy.result }}" >> ci-summary.md
echo "- Cargo Deny: ${{ needs.cargo-deny.result }}" >> ci-summary.md
echo "- Rustfmt Check: ${{ needs.rustfmt-check.result }}" >> ci-summary.md
echo "- Field Comparison: ${{ needs.field-compare.result }}" >> ci-summary.md
echo "" >> ci-summary.md

if [ -f test-report/test-report.md ]; then
Expand Down Expand Up @@ -668,7 +670,7 @@ jobs:

# Rust API Clippy 结果
echo "" >> ci-summary.md
echo "## Rust API Clippy (Non-blocking)" >> ci-summary.md
echo "## Rust API Clippy" >> ci-summary.md
echo "- Status: ${{ needs.rust-api-clippy.result }}" >> ci-summary.md
echo "- Artifact: api-clippy-output.txt" >> ci-summary.md

Expand Down
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@ Link related issue IDs. Request review from a Rust + a Flutter reviewer for cros
## Security & Configuration
Never commit real secrets—use `.env.example` for new vars. Run `make check` before pushing (ensures ports & env). Validate input at service boundary (API layer) and keep domain invariants enforced in constructors or smart methods. Log sensitive data only in anonymized form.

### CSV Export
- Transactions CSV endpoints accept `include_header` to control header row output.
- POST `/api/v1/transactions/export` body: `{ "format":"csv", ..., "include_header": true|false }`
- GET `/api/v1/transactions/export.csv?include_header=true|false`
- Defaults to `true`. Clients can pass `include_header=false` for programmatic appends.

### CORS Modes
- Development: `make api-dev` (sets `CORS_DEV=1`) allows any origin/headers for rapid iteration.
- Secure: `make api-safe` enforces origin whitelist & explicit header list. Add new custom headers in `middleware/cors.rs`.
Expand Down
139 changes: 18 additions & 121 deletions deny.toml
Original file line number Diff line number Diff line change
@@ -1,135 +1,32 @@
# cargo-deny configuration for Jive Money project
# This configuration helps ensure security, licensing compliance, and dependency management

# The path where the deny.toml file is located relative to the workspace root
[graph]
# The file system path to the graph file to use
# targets = []

# Deny certain platforms from being used
[targets]
# The field that will be checked, this value must be one of
# - triple
# - arch
# - os
# - env
#
# cfg = "triple"
# The value to match
# value = "x86_64-unknown-linux-gnu"
############################
# Minimal cargo-deny config
############################

[advisories]
# The lint level for advisories that are for crates that are not direct dependencies
db-path = "~/.cargo/advisory-db"
db-urls = ["https://github.com/rustsec/advisory-db"]
# The lint level for crates that have a vulnerability
vulnerability = "deny"
# The lint level for crates that have been marked as unmaintained
unmaintained = "warn"
# The lint level for crates that have been yanked from crates.io
yanked = "deny"
# A list of advisory IDs to ignore
ignore = [
# These are known issues that we've evaluated and determined acceptable
# Add RUSTSEC advisory IDs here if needed
]
vulnerabilities = "deny"
unmaintained = "warn"
yanked = "warn"
notice = "warn"
ignore = []

[licenses]
# List of explicitly allowed licenses
# See https://spdx.org/licenses/ for list of valid identifiers
unlicensed = "deny"
allow = [
"MIT",
"Apache-2.0",
"Apache-2.0 WITH LLVM-exception",
"BSD-2-Clause",
"BSD-3-Clause",
"ISC",
"Unicode-DFS-2016",
"OpenSSL",
"MPL-2.0",
"CC0-1.0",
"BSL-1.0", # Boost Software License
"Zlib",
"Unlicense",
]

# List of explicitly disallowed licenses
deny = [
"GPL-2.0",
"GPL-3.0",
"AGPL-3.0",
"LGPL-2.0",
"LGPL-2.1",
"LGPL-3.0",
"SSPL-1.0", # Server Side Public License (MongoDB)
"MIT", "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause", "ISC",
"Unicode-DFS-2016", "MPL-2.0", "Zlib", "BSL-1.0", "CC0-1.0", "Unlicense", "OpenSSL"
]

# Lint level for when multiple versions of the same license are detected
copyleft = "deny"
# Blanket approval or denial for OSI-approved or FSF-approved licenses
allow-osi-fsf-free = "both"
# Lint level used when no license is detected
default = "deny"
# The confidence threshold for detecting a license from a license text.
# Expressed as a floating point number between 0.0 and 1.0
copyleft = "warn"
confidence-threshold = 0.8

# Allow certain licenses for specific crates only
[[licenses.exceptions]]
allow = ["ring", "webpki"]
name = "ISC"

[bans]
# Lint level for when multiple versions of the same crate are detected
multiple-versions = "warn"
# Lint level for when a crate version requirement is `*`
wildcards = "allow"
# The graph highlighting used when creating dotgraphs for crates
highlight = "all"

# List of crates to deny
deny = [
# Deny old/insecure crypto libraries
{ name = "openssl", version = "<0.10" },
# Deny old/vulnerable versions of common crates
{ name = "serde", version = "<1.0" },
# Deny yanked crates
{ name = "chrono", version = "=0.4.20" }, # Had a security issue
]

# Certain crates/versions that will be skipped when doing duplicate detection.
skip = [
# Skip certain crates that commonly have multiple versions
{ name = "windows-sys" }, # Often multiple versions in dependency tree
{ name = "syn", version = "1.0" }, # v1 and v2 coexist
]

# Similarly to `skip` allows you to skip certain crates from being checked. Unlike `skip`,
# `skip-tree` skips the crate and all of its dependencies entirely.
skip-tree = [
# Skip crates and their entire dependency trees
]
deny = []
skip = []
skip-tree = []

[sources]
# Lint level for what to happen when a crate from a crate registry that is
# not in the allow list is encountered
unknown-registry = "warn"
# Lint level for what to happen when a crate from a git repository that is not
# in the allow list is encountered
unknown-git = "warn"

# List of allowed registries
allow-registry = [
"https://github.com/rust-lang/crates.io-index",
]

# List of allowed Git repositories
allow-git = [
# Allow specific git dependencies if needed
# "https://github.com/organization/repository"
]

# Configuration specific to the jive-api workspace
[[sources.allow-org]]
github = ["jive-org"] # Replace with actual GitHub organization
gitlab = ["jive-gitlab"] # Replace with actual GitLab organization if used
unknown-git = "warn"
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
allow-git = []
14 changes: 14 additions & 0 deletions jive-api/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,17 @@ quick-start: build dev
@echo "API: http://localhost:8012"
@echo "Adminer: http://localhost:8080"
@echo "RedisInsight: http://localhost:8001"

# 便捷:导出/审计(支持 include_header 传参)
Copy link

Copilot AI Sep 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The comment is in Chinese while the rest of the file uses English. For consistency and accessibility, consider translating to English: # Convenience: export/audit (supports include_header parameter)

Suggested change
# 便捷:导出/审计(支持 include_header 传参)
# Convenience: export/audit (supports include_header parameter)

Copilot uses AI. Check for mistakes.

.PHONY: export-csv export-csv-stream
export-csv:
@echo "POST 导出 CSV (data:URL):make export-csv TOKEN=... START=2024-09-01 END=2024-09-30 HEADER=true|false"
curl -s -H "Authorization: Bearer $${TOKEN}" -H "Content-Type: application/json" \
-d '{"format":"csv","start_date":"'$${START:-2024-09-01}'","end_date":"'$${END:-2024-09-30}'","include_header":'$${HEADER:-true}'}' \
http://localhost:$${API_PORT:-8012}/api/v1/transactions/export | jq .

export-csv-stream:
@echo "GET 流式导出 CSV:make export-csv-stream TOKEN=... HEADER=true|false"
curl -s -D - -H "Authorization: Bearer $${TOKEN}" \
"http://localhost:$${API_PORT:-8012}/api/v1/transactions/export.csv?include_header=$${HEADER:-true}" \
-o /tmp/transactions_export.csv | head -n 20
Comment on lines +152 to +154

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The pipe | head -n 20 in the export-csv-stream target is likely not behaving as intended. The curl command uses -o /tmp/transactions_export.csv to write the response body to a file, and -D - to write headers to standard output. As a result, head -n 20 will only process the response headers, not the CSV content.

To preview the first 20 lines of the downloaded CSV file, you should first let curl finish downloading and then run head on the output file. I've also removed the -D - flag which prints headers to standard output, as it would interfere with previewing the file content.

	curl -s -H "Authorization: Bearer $${TOKEN}" \
	  "http://localhost:$${API_PORT:-8012}/api/v1/transactions/export.csv?include_header=$${HEADER:-true}" \
	  -o /tmp/transactions_export.csv && head -n 20 /tmp/transactions_export.csv

36 changes: 23 additions & 13 deletions jive-api/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ impl Claims {
&EncodingKey::from_secret(JWT_SECRET.as_ref()),
)
.map_err(|_| AuthError::TokenCreation)?;

Ok(token)
}

Expand All @@ -66,7 +66,7 @@ impl Claims {
&Validation::default(),
)
.map_err(|_| AuthError::InvalidToken)?;

Ok(token_data.claims)
}

Expand Down Expand Up @@ -94,11 +94,11 @@ impl IntoResponse for AuthError {
AuthError::TokenCreation => (StatusCode::INTERNAL_SERVER_ERROR, "Token creation error"),
AuthError::InvalidToken => (StatusCode::UNAUTHORIZED, "Invalid token"),
};

let body = Json(serde_json::json!({
"error": error_message,
}));

(status, body).into_response()
}
}
Expand Down Expand Up @@ -126,18 +126,18 @@ where
.get("Authorization")
.and_then(|value| value.to_str().ok())
.ok_or(AuthError::MissingCredentials)?;

// 检查Bearer前缀
if !auth_header.starts_with("Bearer ") {
return Err(AuthError::InvalidToken);
}

// 提取token
let token = &auth_header[7..];

// 验证令牌并提取claims
let claims = Claims::from_token(token)?;

Ok(claims)
}
}
Expand Down Expand Up @@ -177,11 +177,21 @@ pub struct RegisterRequest {
}

// Default values for registration
fn default_country() -> String { "CN".to_string() }
fn default_currency() -> String { "CNY".to_string() }
fn default_language() -> String { "zh-CN".to_string() }
fn default_timezone() -> String { "Asia/Shanghai".to_string() }
fn default_date_format() -> String { "YYYY-MM-DD".to_string() }
fn default_country() -> String {
"CN".to_string()
}
fn default_currency() -> String {
"CNY".to_string()
}
fn default_language() -> String {
"zh-CN".to_string()
}
fn default_timezone() -> String {
"Asia/Shanghai".to_string()
}
fn default_date_format() -> String {
"YYYY-MM-DD".to_string()
}

/// 注册响应
#[derive(Debug, Serialize)]
Expand Down
Loading
Loading