# AA Customizer

📖 **[Interactive Documentation](https://thrainkrilleve.github.io/aa-customizer/)** — Installation guide, field reference, media strategy

A branding & customization plugin for [Alliance Auth](https://allianceauth.readthedocs.io).

Gives administrators a simple admin-panel UI to customize their Alliance Auth installation without touching code or replacing static files.


## Screenshots

### Split Screen layout
![Split Screen layout](https://r3v-w.com/Screenshot_20260320_215103.png)

### Centered Card layout (default)
![Centered Card layout](https://r3v-w.com/Screenshot_20260320_201644.png)

### Side Bar Icon
![Sidebar Icon](https://r3v-w.com/Screenshot_20260320_215038.png)

### Custom Login CSS/HTML (Design how you want!)

![Login Landing](https://r3v-w.com/login.png)

![Login Popup](https://r3v-w.com/login2.png)
## Features

| Feature | What it does |
|---|---|
| **Custom site name** | Overrides `SITE_NAME` from `local.py` across the whole site |
| **Login background** | URL or uploaded image/video; falls back to a CSS color, then the default AA space background. `.mp4`, `.webm`, `.ogv` URLs play as a fullscreen muted video with configurable loop count (forever, once, or N times) |
| **Login layout** | Centered Card, Split Screen (background left), or Split Screen (login left) |
| **Login logo** | URL or uploaded image shown at the top of the login card |
| **Login title & subtitle** | Welcome heading and description text on the login card |
| **Login extra HTML** | Raw HTML injected below the EVE SSO button (notices, Discord links, etc.) |
| **Custom favicon** | URL or uploaded image replacing all Alliance Auth browser-tab icons |
| **Navbar logo** | URL or uploaded image alongside the site name in the top navigation bar |
| **Sidebar logo** | URL or uploaded image replacing the Alliance Auth logo in the sidebar |
| **Custom CSS — URL** | External stylesheet linked in every page `<head>`, loaded after the active theme |
| **Custom CSS — inline** | CSS text injected via `<style>` on every page, loaded after the active theme |
| **Extra `<head>` HTML** | Raw HTML at the end of `<head>` on every page (analytics, font imports, meta tags) |
| **Login Page — CSS URL** | External stylesheet linked only on the login page `<head>`, after global CSS |
| **Login Page — CSS (Inline)** | CSS injected only on the login page, after global CSS; use to fully restyle or completely replace the login card with a custom design |
| **Login Page — Extra `<head>` HTML** | Raw HTML injected at the end of `<head>` on the login page only (font imports, meta tags) |
| **Login Page — Extra Body HTML** | Raw HTML injected before `</body>` on the login page only; combine with the CSS field to build a full custom landing page over the existing login card |
| **Login Page SPA mode** | Turns the login page into a mini multi-page website with a navigation bar — visitors can browse corp info, check requirements, and then click Sign In |
| **Member dashboard injection** | Inject custom CSS and HTML into the dashboard that all logged-in members see |
| **Admin dashboard injection** | Inject custom CSS and HTML into the superuser admin overview page |
| **Restricted admin access** | Limit who can edit branding settings to specific user IDs via a `local.py` setting |


## Requirements

- Alliance Auth ≥ 4.0.0 (Bootstrap 5 template set)
- `django-solo`
- `Pillow`

***

To prevent confusion on how to get this installed, I figured I would break it down a little more.

After the initial install, mirgate, etc. the app will function without having to do any of the extra folder creations by using image URLs.

The upside is, it will work, the downside is, for any user that is located in a country that is blocking certain websites, like Imgur, the images will not appear. (A UK user in our Corp pointed this out to me.) 

![alt](https://i.imgur.com/ozx29tk.png)

So, the work around is either: 

* A: Use a service that can provide global hosting for images. For this, I used Cloudflare's R2 Object storage, linked to the custom domain (your Auth URL), which then created a DNS record for the storage. This in turn will expose anything put into the bucket, so it's recommended to only store what is needed, I.E.: pictures. (Tested this by using Proton VPN)

* B: Do the setup to allow the images to be hosted directly in Auth:
***

## Installation

### Bare Metal install

**1 — Install the package**

```bash
# Activate your Alliance Auth virtualenv first, then:
pip install aa-customizer
```

**2 — Add to `INSTALLED_APPS`**

Open your `local.py` and add above INSTALLED_APPS =:
```python
INSTALLED_APPS.insert(0, 'aa_customizer')
```

> Django searches each app's `templates/` folder in `INSTALLED_APPS` order and uses the first match.
> Placing `aa_customizer` first ensures its template overrides are picked up before the Alliance Auth originals.

**3 — Add the context processor**

In `local.py`, add to the settings section to append the existing `TEMPLATES` list:

```python
TEMPLATES[0]["OPTIONS"]["context_processors"].append(
    "aa_customizer.context_processors.aa_customizer"
)

MEDIA_ROOT = "/path/to/your/media/"
MEDIA_URL  = "/media/"

```

**4 — Run migrations**

```bash
python manage.py migrate aa_customizer
```

**5 — Collect static files**

```bash
python manage.py collectstatic
```
Make sure your web server (nginx, Apache, etc.) is configured to serve files from `MEDIA_ROOT` at `MEDIA_URL`.

In your `nginx.conf`, add inside the `server {}` block:

```nginx
# Allow uploads up to 20 MB (nginx default is 1 MB — raise this if you
# get "413 Request Entity Too Large" when uploading background images).
client_max_body_size 20m;
# add after location /static
location /media {
    alias /var/www/myauth/media;
    autoindex off;
}
```

> Adjust `client_max_body_size` to suit your largest file. A 1920×1080 JPEG is typically 1–5 MB; a short MP4 video background can be 10–20 MB. Set it slightly above your expected maximum.

**7 — (Optional) Populate the Media Library**

Once media is configured, go to **AA Customizer → Media Library** in the admin panel to upload images. Give each one a descriptive name (e.g. "Corp logo v2", "Winter background"). You can then pick from your library in **Custom Branding** without re-uploading — just change the dropdown selection to switch images instantly.


### Docker install

The recommended approach for Docker is to use **URL fields** for all images (point at an external CDN, Imgur, GitHub raw assets, etc.) so you don't need to mount a media volume just to serve a few files.

**1 — Install the package inside the container**

```bash
# Activate your Alliance Auth virtualenv first, then:
pip install aa-customizer
```

**2 — Add to `INSTALLED_APPS`**

Open your `local.py` and add above INSTALLED_APPS =:

```python
INSTALLED_APPS.insert(0, 'aa_customizer')
```

> Django searches each app's `templates/` folder in `INSTALLED_APPS` order and uses the first match.
> Placing `aa_customizer` first ensures its template overrides are picked up before the Alliance Auth originals.

**3 — Add the context processor**

In `local.py`, add to the settings section to append the existing `TEMPLATES` list:

```python
TEMPLATES[0]["OPTIONS"]["context_processors"].append(
    "aa_customizer.context_processors.aa_customizer"
)

MEDIA_ROOT = "/path/to/your/media/"
MEDIA_URL  = "/media/"
```

**b) Add the named volume to `docker-compose.yml`**

The standard AA Docker setup uses a named Docker volume for media. Add it in three places:

1. In `x-allianceauth-base` volumes (so gunicorn, beat, and workers all share it):
```yaml
x-allianceauth-base: &allianceauth-base
  volumes:
    # ... your existing volume mounts ...
    - media-data:/var/www/myauth/media
```

2. In the `nginx` service volumes:
```yaml
services:
  nginx:
    volumes:
      # ... your existing volume mounts ...
      - media-data:/var/www/myauth/media
```

3. In the top-level `volumes:` section:
```yaml
volumes:
  media-data:
```

**c) Configure nginx**

In your `nginx.conf`, add the following inside the `server {}` block:

```nginx
# Allow uploads up to 20 MB (nginx default is 1 MB — raise this if you
# get "413 Request Entity Too Large" when uploading background images).
client_max_body_size 20m;
# add after location /static
location /media {
    alias /var/www/myauth/media;
    autoindex off;
}
```

> Adjust `client_max_body_size` to suit your largest file. A 1920×1080 JPEG is typically 1–5 MB; a short MP4 video background can be 10–20 MB. Set it slightly above your expected maximum.

**d) Bring the stack up**

```bash
docker compose up -d
```

**e) Fix volume permissions**

Docker creates named volumes owned by root. The AA container runs as uid/gid `61000`. Run this once after first bringing the stack up:

Confimr the uid/gid

```bash
docker compose exec allianceauth_gunicorn ls -la /var/www/myauth/media
#change using
docker compose exec -u root allianceauth_gunicorn chown -R 61000:61000 /var/www/myauth/media
```

After this, uploads made through `/admin/aa_customizer/aacustomizersettings/` will be stored under `/var/www/myauth/media/aa_customizer/` and served immediately by nginx.

**5 — Build and Restart**

```
docker compose build
docker compose down
docker compose up -d
```

## Usage

1. Log in to the Alliance Auth **admin panel** (`/admin/`).
2. **(Optional) Build your image library** — find **AA Customizer → Media Library** and upload your images there, giving each a name. You can upload as many as you like and switch between them without re-uploading.
3. Find **AA Customizer → Custom Branding** in the left-hand sidebar.
4. For each image slot, choose how to set it:
   - **Library** — pick from an image you uploaded to the Media Library (recommended when self-hosting media)
   - **URL** — paste an external link (CDN, Imgur, etc.) — always takes priority
   - **Upload** — upload a file directly into the field
5. Fill in any other fields you want and click **Save**.

Changes take effect immediately on the next page load — no server restart needed.


## Field reference

### Site

| Field | Description |
|---|---|
| **Site Name** | Overrides `SITE_NAME` from `local.py` |

### Login Page — Background

| Field | Description |
|---|---|
| **Login Background — URL** | URL of a background image **or video** (takes priority over everything). Video files (`.mp4`, `.webm`, `.ogv`) play fullscreen, muted, and auto-paused by the browser when the tab is hidden. Loop behavior is controlled by the Loop Count field below |
| **Login Background — Library** | Select an image uploaded to the Media Library (takes priority over a direct upload) |
| **Login Background — Upload** | Upload a background image directly into this field |
| **Login Background Color** | CSS color fallback when no image is set, or the color shown behind a video while it loads (e.g. `#1a1a2e`) |
| **Background Video — Loop Count** | `0` (default) = loop forever. `1` = play once then freeze on the last frame. `N` = play exactly N times then freeze. Only applies to video backgrounds |

### Login Page — Layout

| Field | Description |
|---|---|
| **Login Page Layout** | `Centered Card` — login card centered over the full-page background (default). `Split Screen — Background Left` — background on the left, dark login panel on the right. `Split Screen — Login Left` — mirrors it. |
| **Split Panel — Show Overlay Text** | Tick to show text on the background panel; untick to hide it entirely |
| **Split Panel — Overlay Text** | Text shown on the background panel. Leave blank to auto-display the site name |
| **Split Panel — Text Position** | Vertical position of the overlay text: `Top`, `Center`, or `Bottom` |

### Login Page — Branding

| Field | Description |
|---|---|
| **Login Logo — URL** | URL of a logo image (takes priority over everything) |
| **Login Logo — Library** | Select an image from the Media Library (takes priority over a direct upload) |
| **Login Logo — Upload** | Upload a logo image directly into this field |
| **Login Logo Max Width (px)** | Maximum display width of the login logo |
| **Login Page Title** | Custom heading shown above the SSO button |
| **Login Page Subtitle** | Optional description text below the title |
| **Login Page Extra HTML** | Raw HTML injected below the EVE SSO button (notices, links, etc.) |

### Favicon

| Field | Description |
|---|---|
| **Favicon — URL** | URL of a favicon image (takes priority over everything) |
| **Favicon — Library** | Select an image from the Media Library (takes priority over a direct upload) |
| **Favicon — Upload** | Upload a favicon image directly into this field |

### Navigation Bar Logo

| Field | Description |
|---|---|
| **Navbar Logo — URL** | URL of a navbar logo image (takes priority over everything) |
| **Navbar Logo — Library** | Select an image from the Media Library (takes priority over a direct upload) |
| **Navbar Logo — Upload** | Upload a navbar logo image directly into this field |
| **Navbar Logo Height (px)** | Display height of the navbar logo |

### Sidebar Logo

| Field | Description |
|---|---|
| **Sidebar Logo — URL** | URL of an image to replace the AA logo in the sidebar (takes priority over everything) |
| **Sidebar Logo — Library** | Select an image from the Media Library (takes priority over a direct upload) |
| **Sidebar Logo — Upload** | Upload a sidebar logo image directly into this field |
| **Sidebar Logo Width (px)** | Display width of the sidebar logo |

### Site-Wide CSS & HTML

| Field | Description |
|---|---|
| **Custom CSS — URL** | External stylesheet URL linked in every page `<head>` |
| **Custom CSS** | Inline CSS injected via `<style>` on every page |
| **Extra `<head>` HTML** | Raw HTML injected at the end of `<head>` on every page |

### Login Page — SPA Mode

SPA mode turns your login page into a **mini website** with a navigation bar. Visitors can browse multiple pages (About, Requirements, etc.) before clicking Sign In to get the normal EVE SSO login card.

| Field | Description |
|---|---|
| **Enable Login Page SPA** | Toggle this on to activate SPA mode |
| **SPA Nav Brand Text** | The name shown in the top-left corner of the nav bar. Leave blank to use your site name |

**How to build your SPA pages**

With SPA mode on, go to **Login Page — Extra Body HTML** and paste your pages using this structure:

```html
<div id="aac-spa-content">

  <section data-route="home" data-label="Home">
    <div class="aac-spa-page">
      <p class="aac-spa-corp-tag">YOUR CORP TAG</p>
      <h1 class="aac-spa-hero-title">My Corporation</h1>
      <p class="aac-spa-hero-sub">Short tagline here</p>
      <div class="aac-spa-hero-actions">
        <a href="#about" class="aac-spa-btn aac-spa-btn-outline">About Us</a>
        <a href="#signin" class="aac-spa-btn aac-spa-btn-primary">Sign In →</a>
      </div>
    </div>
  </section>

  <section data-route="about" data-label="About Us">
    <div class="aac-spa-page">
      <h2 class="aac-spa-page-title">About Us</h2>
      <p class="aac-spa-body-text">Tell visitors about your corp here.</p>
    </div>
  </section>

</div>
```

- Each `<section>` becomes one page. The `data-route` is its internal name; `data-label` is the nav link text.
- Nav links are built automatically — no extra setup needed.
- Linking to `href="#signin"` closes the SPA and shows the login card.
- A full ready-to-use scaffold is available in `login-spa.html` in this repository — copy and paste it into the Extra Body HTML field and edit the text.

### Member Dashboard — Custom Code

Inject custom CSS or HTML into the dashboard all logged-in members see.

| Field | Description |
|---|---|
| **Dashboard CSS** | Inline CSS injected on the member dashboard page only |
| **Dashboard — Extra `<head>` HTML** | Raw HTML injected into `<head>` on the member dashboard only |
| **Dashboard — Extra Body HTML** | Raw HTML injected at the bottom of the member dashboard page |

### Superuser Dashboard — Custom Code

Inject custom CSS or HTML into the admin status overview page (`/admin/`). Only superusers and staff see this.

| Field | Description |
|---|---|
| **Superuser Dashboard CSS** | Inline CSS injected on the admin overview page only |
| **Superuser Dashboard — Extra `<head>` HTML** | Raw HTML injected into `<head>` on the admin overview only |
| **Superuser Dashboard — Extra Body HTML** | Raw HTML injected at the top of the admin overview page, above the dashboard panels |


## Image recommendations

| Field | Recommended size / format |
|---|---|
| Login background (image) | ≥ 1920 × 1080 px, JPEG, PNG, or GIF |
| Login background (video) | 1920 × 1080 px, MP4 (H.264) or WebM (VP9); keep under ~15 MB for fast loads |
| Login logo | ≥ 256 × 256 px, transparent PNG |
| Favicon | ≥ 192 × 192 px, PNG or ICO |
| Navbar logo | Transparent PNG, height ≤ 64 px |
| Sidebar logo | Transparent PNG, width ≤ 256 px |


## Restricting admin access

By default, any superuser can edit **Custom Branding** and the **Media Library** in admin.

If you want to lock those pages down to specific people only — even if other superusers exist — add this to `local.py`:

```python
AA_CUSTOMIZER_TRUSTED_USER_IDS = [1, 42]
```

Replace `1` and `42` with the real user IDs of the people who should have access. Anyone not on the list will be turned away even if they are a superuser.

**How to find a user's ID:** Go to **Admin → Authentication → Users**, click the user's name, and look at the number in the URL. For example, `/admin/auth/user/5/change/` means their ID is `5`.

If you leave this setting out of `local.py` entirely, all superusers retain access as normal.


## Security notes

- **Raw HTML fields** (`Login Page Extra HTML`, `Extra <head> HTML`) are rendered without sanitization and are only editable by Django admin users (staff/superusers).
- **Custom CSS URL** is validated as a URL by Django's field validator; only `http`/`https` schemes are accepted.
- AA Customizer works alongside Alliance Auth's built-in **Custom CSS** admin (`/admin/custom_css/customcss/`). The customizer's CSS loads after the built-in one, so it takes precedence.


## License

MIT
