**vaessl: Login Architecture**

# Backend

The login architecture is designed to make future additions to this bridging app as frictionless as possible. Abstraction and inheritance are used to keep refactorings to a minimum. The first app to bridge is Homebox, which uses a classic username/password and Bearer token login process. The second hypothetical app, WikiJS, uses a user-generated API key. The abstractions cater to both authentication methods.

## API Endpoints

The server context path is `/api`. The three endpoints that drive the connection flow:

| Method | Path | Description |
|--------|------|-------------|
| `POST` | `/api/login` | Authenticate a service; stores connection ID in session |
| `GET` | `/api/connections/status` | List connected services for the current session |
| `DELETE` | `/api/connections/{serviceType}` | Remove a service from the session; invalidates the session if no connections remain |

## Session Management

Authentication state is held server-side in a JDBC-backed Spring Session (stored in the same PostgreSQL database). After a successful login the controller stores `{serviceType}_CONNECTION_ID` in the `HttpSession`. The session cookie is `HttpOnly`, `SameSite=Lax`, and set to max age.

`CorsConfig` allows credentials from the two configured origins (`FRONTEND_LOCAL_URL`, `FRONTEND_PUBLIC_URL`). `allowCredentials(true)` is required for the session cookie to travel cross-origin.

## Single Table Inheritance

Database entities use Single Table Inheritance — one `connections` table with a `service_type` discriminator column and all app-specific nullable columns. A unique constraint on `(appUrl, username)` prevents duplicate entries per user per instance.

***ConnectionEntity.java***

```java
@Entity
@Table(name = "connections", uniqueConstraints = { @UniqueConstraint(columnNames = { "appUrl", "username" }) })
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "service_type")
public abstract class ConnectionEntity {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String appUrl;
    private String username;
}
```

***HomeboxEntity.java***

```java
@Entity
@DiscriminatorValue("HOMEBOX")
public class HomeboxEntity extends ConnectionEntity {

    private String token;
    private String attachmentToken;
    private Instant expiresAt;

    public static HomeboxEntity from(ConnectionRequest request, ConnectionResponse response) {
        HomeboxEntity he = new HomeboxEntity();
        he.setAppUrl(request.appUrl());
        he.setUsername(request.username());
        he.setToken(response.token());
        he.setAttachmentToken(response.getExtraVar("attachmentToken"));
        he.setExpiresAt(response.expiresAt());
        return he;
    }
}
```

## The Provider Pattern (Logic Layer)

Each integrated app implements `ConnectionProvider`. This separates how an app authenticates (the specific) from what the system does with that result (the general).

***ConnectionProvider.java***

```java
public interface ConnectionProvider {

    String getServiceType();

    void checkCredentials(ConnectionRequest request);

    ConnectionResponse authenticate(ConnectionRequest request);

    ConnectionEntity findUniqueConnectionEntry(ConnectionRequest request);

    ConnectionEntity connectionToEntity(ConnectionRequest request, ConnectionResponse response);

    void updateToRepository(ConnectionEntity existing, ConnectionResponse response);

    default Instant getTokenExpiry(ConnectionEntity entity) {
        return null;
    }
}
```

- `checkCredentials` — validates that required fields are present before attempting the remote call (throws `EmptyCredentialsException` with a list of missing fields)
- `findUniqueConnectionEntry` — looks up an existing record to decide insert vs. update
- `getTokenExpiry` — default returns `null` (no expiry); token-based providers override this so `ConnectionService` can compute the `connected` flag

***HomeboxConnectionProvider.java***

```java
@Component
public class HomeboxConnectionProvider implements ConnectionProvider {

    @Override
    public String getServiceType() { return "HOMEBOX"; }

    @Override
    public void checkCredentials(ConnectionRequest request) {
        // throws EmptyCredentialsException if username or password is null
    }

    @Override
    public ConnectionResponse authenticate(ConnectionRequest request) {
        // POST to Homebox /api/v1/users/login, returns token + attachmentToken + expiresAt
    }

    @Override
    public ConnectionEntity findUniqueConnectionEntry(ConnectionRequest request) {
        return cRepository.findByAppUrlAndUsername(request.appUrl(), request.username());
    }

    @Override
    public ConnectionEntity connectionToEntity(ConnectionRequest request, ConnectionResponse response) {
        return HomeboxEntity.from(request, response);
    }

    @Override
    public void updateToRepository(ConnectionEntity existing, ConnectionResponse response) {
        // casts to HomeboxEntity and updates token, attachmentToken, expiresAt
    }

    @Override
    public Instant getTokenExpiry(ConnectionEntity entity) {
        return (entity instanceof HomeboxEntity he) ? he.getExpiresAt() : null;
    }
}
```

***ConnectionService.java***

```java
@Service
public class ConnectionService {

    // auto-discovers all ConnectionProvider beans into a serviceType → provider map

    public LoginResult login(ConnectionRequest request) {
        provider.checkCredentials(request);
        ConnectionResponse response = provider.authenticate(request);
        ConnectionEntity existing = provider.findUniqueConnectionEntry(request);
        // insert or update, then return LoginResult(connectionId, expiresAt)
    }

    public ConnectionStatusResponse getConnectionStatus(String serviceType, Long connectionId) {
        // looks up entity by id, calls provider.getTokenExpiry() to compute connected flag
    }
}
```

## DTOs

### Request / Internal

- **`ConnectionRequest`** — fields: `appUrl`, `serviceType`, `username`, `password`, `apiKey`, `stayLoggedIn`. Typed fields (not a credentials map) let providers validate exactly what they need via `checkCredentials()`. `apiKey` covers future API-key-only providers (e.g. WikiJS).

- **`ConnectionResponse`** — internal DTO between provider and service: `token`, `expiresAt`, `Map<String, Object> extraResponseData`. The `getExtraVar(key)` helper safely extracts app-specific values (e.g. Homebox's `attachmentToken`).

### Response (returned to frontend)

- **`LoginResult`** — `connectionId`, `expiresAt`. Returned by `ConnectionService.login()` and used by the controller to populate the session.

- **`AuthResponse`** — `serviceType`, `expiresAt`. Returned by `POST /api/login` to the frontend.

- **`ConnectionStatusResponse`** — `serviceType`, `appUrl`, `username`, `expiresAt`, `connected`. Returned by `GET /api/connections/status`.

## Supported Service Types

Registered in the `ServiceType` enum. Currently: `HOMEBOX`. Adding a new service means implementing `ConnectionProvider` and adding the discriminator value — no changes to `ConnectionService` or `ConnectionController` required.
