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
42 changes: 42 additions & 0 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Contributing standards

Conventions for issues, branches, commits, and pull requests in Jackil. The
templates in `.github/` enforce most of this — this is the why.

## Issues

- Use the Bug report or Feature request form (blank issues are disabled).
- One issue = one problem or one request.
- Security issues go through a private advisory, never a public issue.
- Never paste secrets, SMTP/IMAP credentials, or customer data.

## Branches

Short, prefixed, kebab-case off `main`:

```
fix/sla-pause-on-pending
feat/kb-article-versioning
docs/proxy-deployment
chore/bump-2026.2.1
```

## Commits

- Plain style. Short subject with the version in parens when it's a release,
e.g. `Pause the SLA clock on Pending and recompute on priority change (2026.2.1)`.
- One sentence per change on its own line in the body; no bullet lists.
- **No AI / Co-Authored-By trailers.**
- Don't commit secrets, keys, or customer data.

## Pull requests

- Fill in the PR template, including the proposed commit message.
- Tests must pass: `USE_SQLITE=1 .venv/bin/python manage.py test`.
- Run `makemigrations` if a model changed, and commit the migration.
- Keep lint clean: `.venv/bin/ruff check .` and `.venv/bin/ruff format --check .`.

## Releases

Jackil's version lives in git tags (there is no in-code version string). A
release is a `vYYYY.N.P` tag on `main` after the PR merges.
71 changes: 71 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
name: Bug report
description: Something in Jackil isn't working as expected
labels: ["bug", "needs-triage"]
body:
- type: markdown
attributes:
value: |
Thanks for filing a bug. One issue = one problem. Never paste secrets,
API keys, SMTP/IMAP passwords, or customer data — redact before posting.
- type: input
id: version
attributes:
label: Jackil version
description: Release tag (e.g. v2026.2.1) or the deployed commit SHA.
placeholder: "v2026.2.1"
validations:
required: true
- type: dropdown
id: component
attributes:
label: Area
options:
- Tickets
- SLA / escalation / time sensitivity
- Inbox / inbound & outbound email
- Knowledge base / Help Center
- Automation / macros
- Custom fields / request forms
- Reports & analytics
- REST API / webhooks
- Notifications
- Admin console / settings / branding
- Civil SSO
- Deployment (Docker / proxy / CSRF)
- Other
validations:
required: true
- type: dropdown
id: deployment
attributes:
label: Deployment
options:
- Docker Compose
- Manual / from source
- Other
- type: textarea
id: what-happened
attributes:
label: What happened
description: What you did, what you expected, and what actually happened.
validations:
required: true
- type: textarea
id: repro
attributes:
label: Steps to reproduce
placeholder: |
1. ...
2. ...
3. ...
- type: textarea
id: logs
attributes:
label: Relevant logs / traceback
description: Django logs or the traceback. Redact secrets and customer data.
render: shell
- type: input
id: env
attributes:
label: Browser / OS (for UI bugs)
placeholder: "Firefox 141 · Ubuntu 24.04"
5 changes: 5 additions & 0 deletions .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Report a security vulnerability
url: https://github.com/Susquehanna-Syntax/Jackil/security/advisories/new
about: Disclose security issues privately. Never open a public issue for a vulnerability.
43 changes: 43 additions & 0 deletions .github/ISSUE_TEMPLATE/feature_request.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Feature request
description: Suggest an improvement to Jackil
labels: ["enhancement", "needs-triage"]
body:
- type: markdown
attributes:
value: |
One request per issue. Describe the problem before the solution — it
helps us find the best fix, which isn't always the one first imagined.
- type: textarea
id: problem
attributes:
label: Problem / use case
description: What are you trying to do, and what's missing or awkward today?
validations:
required: true
- type: textarea
id: proposal
attributes:
label: Proposed solution
- type: dropdown
id: area
attributes:
label: Area
options:
- Tickets
- SLA / escalation
- Inbox / email
- Knowledge base
- Automation / macros
- Custom fields / forms
- Reports & analytics
- REST API / webhooks
- Notifications
- Admin console / settings
- Civil SSO
- Other
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives considered
35 changes: 35 additions & 0 deletions .github/SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Security Policy

Jackil handles support tickets, inbound/outbound email, and customer data, so
we ask that security problems be disclosed privately.

## Reporting a vulnerability

**Do not open a public issue, PR, or discussion for a security problem.**

Report it privately through GitHub:

1. Go to the repository's **Security** tab → **Report a vulnerability**
(<https://github.com/Susquehanna-Syntax/Jackil/security/advisories/new>).
2. Include the details below.

This opens a private advisory visible only to you and the maintainers.

Please include:

- Affected area (a specific view/endpoint, email ingestion, the API) and version.
- Steps to reproduce or a proof of concept.
- Impact — what an attacker can read, change, or do.
- Any suggested remediation.

## Scope

In scope: authentication and session handling, ticket/attachment access
control, email ingestion, the REST API and webhooks, CSRF/host handling, and
privilege boundaries between roles (customer / agent / admin).

Out of scope: findings that require a pre-compromised host or admin account,
and issues in third-party dependencies without a Jackil-specific exploit path.

We aim to acknowledge reports within a few days and to coordinate a fix and
disclosure timeline with you.
38 changes: 38 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
## Summary

<!-- One or two sentences: what this changes and why. -->

## Type

- [ ] Bug fix
- [ ] Feature
- [ ] Refactor / cleanup
- [ ] Docs

## Proposed commit message

<!--
Plain style: short subject with the version in parens, one sentence per change
on its own line, no bullet lists, no AI / Co-Authored-By trailer.
e.g. "Pause the SLA clock on Pending and recompute on priority change (2026.2.1)"
-->

```
```

## Checklist

- [ ] Tests pass: `USE_SQLITE=1 .venv/bin/python manage.py test`
- [ ] Migrations added if any model changed (`makemigrations`)
- [ ] Lint clean: `.venv/bin/ruff check .` and `.venv/bin/ruff format --check .`
- [ ] Version bumped / tag planned if this is a release
- [ ] No AI / Co-Authored-By attribution in commits
- [ ] No secrets, keys, SMTP/IMAP passwords, or customer data committed

## Testing

<!-- What you ran and what you observed. -->

## Screenshots

<!-- UI changes only. -->
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,19 @@ Visit `http://localhost:8000`. Demo logins: `admin` / `admin12345` (admin),
docker compose up --build
```

### Behind a reverse proxy / external hostname

When Jackil is reached through a proxy or a hostname other than `localhost`,
set both env vars so Django accepts logins and other POSTs:

```bash
ALLOWED_HOSTS=jackil-tail.kingdom.local,help.acme.com
CSRF_TRUSTED_ORIGINS=https://jackil-tail.kingdom.local,https://help.acme.com
```

`CSRF_TRUSTED_ORIGINS` must include the scheme. Without it, POSTs fail with
`Forbidden (Origin checking failed … does not match any trusted origins.)`.

## Background jobs (cron)

```bash
Expand Down
10 changes: 7 additions & 3 deletions apps/civilsso/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

def _db_config():
from apps.civilsso.models import CivilConfig

try:
return CivilConfig.current()
except Exception: # noqa: BLE001 — pre-migration or DB down: env-only mode
Expand All @@ -29,7 +30,7 @@ def civil_url() -> str:
if env:
return env
cfg = _db_config()
return (cfg.url.rstrip("/") if cfg and cfg.enabled and cfg.url else "")
return cfg.url.rstrip("/") if cfg and cfg.enabled and cfg.url else ""


def app_slug() -> str:
Expand Down Expand Up @@ -75,8 +76,11 @@ def verify_sso_token(token: str) -> dict | None:
return None
try:
return jwt.decode(
token, pem, algorithms=["EdDSA"],
audience=app_slug(), issuer="civil",
token,
pem,
algorithms=["EdDSA"],
audience=app_slug(),
issuer="civil",
)
except jwt.PyJWTError as exc:
logger.warning("Civil SSO token rejected: %s", exc)
Expand Down
12 changes: 8 additions & 4 deletions apps/civilsso/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ class CivilIdentity(models.Model):
"""

user = models.OneToOneField(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE,
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="civil_identity",
)
civil_id = models.UUIDField(unique=True, db_index=True)
Expand All @@ -41,6 +42,9 @@ class CachedCivilKey(models.Model):
fetched_from = models.URLField()
fetched_at = models.DateTimeField(auto_now=True)

def __str__(self) -> str:
return f"civil key from {self.fetched_from or '(unset)'}"

@classmethod
def current(cls) -> str:
row = cls.objects.order_by("-fetched_at").first()
Expand All @@ -60,10 +64,10 @@ class CivilConfig(models.Model):
app_slug = models.SlugField(max_length=50, blank=True, default="")
updated_at = models.DateTimeField(auto_now=True)

def __str__(self) -> str:
return f"civil:{self.url or '(unset)'} ({'on' if self.enabled else 'off'})"

@classmethod
def current(cls) -> "CivilConfig":
row = cls.objects.first()
return row if row is not None else cls.objects.create()

def __str__(self) -> str:
return f"civil:{self.url or '(unset)'} ({'on' if self.enabled else 'off'})"
Loading
Loading