Skip to content
Open
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,6 @@ yarn-error.log
###> phpstan/phpstan ###
phpstan.neon
###< phpstan/phpstan ###

# Claude Code project documentation (local development only)
CLAUDE.md
3 changes: 2 additions & 1 deletion config/parameters.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ parameters:
######################################################################################################################
partdb.saml.enabled: '%env(bool:SAML_ENABLED)%' # If this is set to true, SAML authentication is enabled


######################################################################################################################
# Miscellaneous
######################################################################################################################
Expand Down Expand Up @@ -104,3 +103,5 @@ parameters:
env(SAML_ROLE_MAPPING): '{}'

env(DATABASE_EMULATE_NATURAL_SORT): 0

env(INITIAL_ADMIN_API_KEY): ''
8 changes: 8 additions & 0 deletions docs/api/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ tokens as you want and also delete them again.
When deleting a token, it is immediately invalidated and can not be used anymore, which means that the application can
not access the API anymore with this token.

### Initial Admin API Token

For automated deployments and CI/CD pipelines, Part-DB supports automatically creating an initial admin API token
during database setup. Set the `INITIAL_ADMIN_API_KEY` environment variable to a 64-character random string
(generate with `openssl rand -hex 32`) before running database migrations. Part-DB will create an API token named
"Initial Admin Token" with FULL scope that expires after 1 year. The token can be used immediately with the format
`Bearer tcp_<your-64-char-key>` in the Authorization header.

### Token permissions and scopes

API tokens are ultimately limited by the permissions of the user, which belongs to the token. That means that the token
Expand Down
5 changes: 5 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@ bundled with Part-DB. Set `DATABASE_MYSQL_SSL_VERIFY_CERT` if you want to accept
particularly for securing and protecting various aspects of your application. It's a secret key that is used for
cryptographic operations and security measures (session management, CSRF protection, etc..). Therefore this
value should be handled as confidential data and not shared publicly.
* `INITIAL_ADMIN_API_KEY` (env only): When set to a 64-character random string (generate with `openssl rand -hex 32`),
Part-DB will automatically create an API token named "Initial Admin Token" for the admin user during database
migrations. This token will have FULL scope and expire after 1 year. This is useful for automated deployments,
CI/CD pipelines, and Docker setups where you need immediate API access without manual token creation. The token
can be used with the format `Bearer tcp_<your-64-char-key>` in the Authorization header.
* `SHOW_PART_IMAGE_OVERLAY`: Set to 0 to disable the part image overlay, which appears if you hover over an image in the
part image gallery

Expand Down
71 changes: 71 additions & 0 deletions migrations/Version20250907000000.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use App\Migration\AbstractMultiPlatformMigration;
use Doctrine\DBAL\Schema\Schema;

final class Version20250907000000 extends AbstractMultiPlatformMigration
{
public function getDescription(): string
{
return 'Create initial admin API token if INITIAL_ADMIN_API_KEY environment variable is set';
}

private function createInitialAdminApiToken(): void
{
$apiToken = $this->getInitialAdminApiToken();
if (empty($apiToken)) {
return;
}

// Create a proper API token with the 'tcp_' prefix and the provided key
$fullToken = 'tcp_' . $apiToken;

// Set expiration to 1 year from now
$validUntil = date('Y-m-d H:i:s', strtotime('+1 year'));
$currentDateTime = date('Y-m-d H:i:s');

// Insert the API token for the admin user (user_id = 2)
// Level 4 = FULL access (can do everything the user can do)
$sql = "INSERT INTO api_tokens (user_id, name, token, level, valid_until, datetime_added, last_modified)
VALUES (2, 'Initial Admin Token', ?, 4, ?, ?, ?)";

$this->addSql($sql, [$fullToken, $validUntil, $currentDateTime, $currentDateTime]);
}

public function mySQLUp(Schema $schema): void
{
$this->createInitialAdminApiToken();
}

public function mySQLDown(Schema $schema): void
{
// Remove the initial admin token if it exists
$this->addSql("DELETE FROM api_tokens WHERE name = 'Initial Admin Token' AND user_id = 2");
}

public function sqLiteUp(Schema $schema): void
{
$this->createInitialAdminApiToken();
}

public function sqLiteDown(Schema $schema): void
{
// Remove the initial admin token if it exists
$this->addSql("DELETE FROM api_tokens WHERE name = 'Initial Admin Token' AND user_id = 2");
}

public function postgreSQLUp(Schema $schema): void
{
$this->createInitialAdminApiToken();
}

public function postgreSQLDown(Schema $schema): void
{
// Remove the initial admin token if it exists
$this->addSql("DELETE FROM api_tokens WHERE name = 'Initial Admin Token' AND user_id = 2");
}
}
34 changes: 32 additions & 2 deletions src/Migration/AbstractMultiPlatformMigration.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
abstract class AbstractMultiPlatformMigration extends AbstractMigration
{
final public const ADMIN_PW_LENGTH = 10;
protected string $admin_pw = '';
protected ?string $admin_pw = null;
protected ?string $admin_api_token = null;

/** @noinspection SenselessProxyMethodInspection
* This method is required to redefine the logger type hint to protected
Expand Down Expand Up @@ -96,7 +97,7 @@ public function getOldDBVersion(): int
*/
public function getInitalAdminPW(): string
{
if ($this->admin_pw === '') {
if ($this->admin_pw === null) {
if (!empty($_ENV['INITIAL_ADMIN_PW'])) {
$this->admin_pw = $_ENV['INITIAL_ADMIN_PW'];
} else {
Expand All @@ -108,6 +109,28 @@ public function getInitalAdminPW(): string
return password_hash((string) $this->admin_pw, PASSWORD_DEFAULT);
}

/**
* Returns the initial admin API token if configured via environment variable.
* If not configured, returns empty string (no token will be created).
*/
public function getInitialAdminApiToken(): string
{
if ($this->admin_api_token === null) {
$apiKey = $_ENV('INITIAL_ADMIN_API_KEY');
if (!empty($apiKey)) {
//Ensure the length of the API key is correct
if (strlen($apiKey) < 64) {
$this->abortIf(true, 'The provided INITIAL_ADMIN_API_KEY is too short! It must be at least 64 characters long! You can generate a valid key with "openssl rand -hex 32"');
}

// Use the provided API key directly (should be generated with openssl rand -hex 32)
$this->admin_api_token = $apiKey;
}
}

return $this->admin_api_token;
}

public function postUp(Schema $schema): void
{
parent::postUp($schema);
Expand All @@ -117,6 +140,13 @@ public function postUp(Schema $schema): void
$this->logger->warning('<bg=yellow;fg=black>The initial password for the "admin" user is: '.$this->admin_pw.'</>');
$this->logger->warning('');
}

if ($this->admin_api_token !== '') {
$this->logger->warning('');
$this->logger->warning('<bg=green;fg=black>Initial admin API token has been created with the provided key</>');
$this->logger->warning('<bg=yellow;fg=black>Use this token in Authorization header: Bearer tcp_'.$this->admin_api_token.'</>');
$this->logger->warning('');
}
}

/**
Expand Down