Initial commit
This commit is contained in:
241
includes/auth.php
Normal file
241
includes/auth.php
Normal file
@@ -0,0 +1,241 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/csrf.php';
|
||||
|
||||
const ADMIN_REMEMBER_COOKIE = 'admin_remember';
|
||||
const ADMIN_REMEMBER_TTL = 120 * 24 * 60 * 60; // 120 days
|
||||
|
||||
function admin_cookie_path(): string {
|
||||
$base = rtrim((string)($GLOBALS['BASE_PATH'] ?? ''), '/');
|
||||
return $base ? $base . '/' : '/';
|
||||
}
|
||||
|
||||
function admin_ensure_remember_table(): void {
|
||||
static $done = false;
|
||||
if ($done) return;
|
||||
|
||||
try {
|
||||
pdo()->exec("
|
||||
CREATE TABLE IF NOT EXISTS admin_remember_tokens (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
admin_id INT UNSIGNED NOT NULL,
|
||||
selector CHAR(24) NOT NULL UNIQUE,
|
||||
token_hash CHAR(64) NOT NULL,
|
||||
expires_at INT UNSIGNED NOT NULL,
|
||||
created_at INT UNSIGNED NOT NULL,
|
||||
INDEX idx_admin_expires (admin_id, expires_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
");
|
||||
} catch (Throwable $e) {
|
||||
// fail silently; remember-me stays disabled
|
||||
}
|
||||
|
||||
$done = true;
|
||||
}
|
||||
|
||||
function admin_clear_remember_cookie(): void {
|
||||
setcookie(ADMIN_REMEMBER_COOKIE, '', [
|
||||
'expires' => time() - 3600,
|
||||
'path' => admin_cookie_path(),
|
||||
'secure' => is_https(),
|
||||
'httponly' => true,
|
||||
'samesite' => 'Lax',
|
||||
]);
|
||||
unset($_COOKIE[ADMIN_REMEMBER_COOKIE]);
|
||||
}
|
||||
|
||||
function admin_delete_remember_tokens(?int $adminId = null, ?string $selector = null): void {
|
||||
admin_ensure_remember_table();
|
||||
|
||||
$now = time();
|
||||
$sql = '';
|
||||
$args = [];
|
||||
|
||||
if ($selector !== null) {
|
||||
$sql = "DELETE FROM admin_remember_tokens WHERE selector = ?";
|
||||
$args = [$selector];
|
||||
} elseif ($adminId !== null) {
|
||||
$sql = "DELETE FROM admin_remember_tokens WHERE admin_id = ? OR expires_at < ?";
|
||||
$args = [$adminId, $now];
|
||||
} else {
|
||||
$sql = "DELETE FROM admin_remember_tokens WHERE expires_at < ?";
|
||||
$args = [$now];
|
||||
}
|
||||
|
||||
try {
|
||||
pdo()->prepare($sql)->execute($args);
|
||||
} catch (Throwable $e) {
|
||||
// ignore cleanup issues
|
||||
}
|
||||
}
|
||||
|
||||
function admin_issue_remember_token(int $adminId): void {
|
||||
if ($adminId <= 0) return;
|
||||
|
||||
admin_delete_remember_tokens($adminId);
|
||||
|
||||
$selector = bin2hex(random_bytes(12)); // 24 chars, indexed lookup
|
||||
$token = bin2hex(random_bytes(32)); // 64 chars secret
|
||||
$hash = hash('sha256', $token);
|
||||
$expires = time() + ADMIN_REMEMBER_TTL;
|
||||
|
||||
try {
|
||||
pdo()->prepare("
|
||||
INSERT INTO admin_remember_tokens (admin_id, selector, token_hash, expires_at, created_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
")->execute([$adminId, $selector, $hash, $expires, time()]);
|
||||
} catch (Throwable $e) {
|
||||
return;
|
||||
}
|
||||
|
||||
$cookieVal = $selector . ':' . $token;
|
||||
setcookie(ADMIN_REMEMBER_COOKIE, $cookieVal, [
|
||||
'expires' => $expires,
|
||||
'path' => admin_cookie_path(),
|
||||
'secure' => is_https(),
|
||||
'httponly' => true,
|
||||
'samesite' => 'Lax',
|
||||
]);
|
||||
$_COOKIE[ADMIN_REMEMBER_COOKIE] = $cookieVal;
|
||||
}
|
||||
|
||||
function admin_start_session(array $adminRow): void {
|
||||
if (session_status() === PHP_SESSION_ACTIVE) {
|
||||
session_regenerate_id(true);
|
||||
}
|
||||
$_SESSION['admin_id'] = (int)($adminRow['id'] ?? 0);
|
||||
$_SESSION['admin_user'] = (string)($adminRow['username'] ?? '');
|
||||
}
|
||||
|
||||
function admin_forget_remember_me(): void {
|
||||
$selector = null;
|
||||
$cookie = (string)($_COOKIE[ADMIN_REMEMBER_COOKIE] ?? '');
|
||||
if (strpos($cookie, ':') !== false) {
|
||||
[$selector] = explode(':', $cookie, 2);
|
||||
}
|
||||
|
||||
if ($selector) {
|
||||
admin_delete_remember_tokens(null, $selector);
|
||||
}
|
||||
if (!empty($_SESSION['admin_id'])) {
|
||||
admin_delete_remember_tokens((int)$_SESSION['admin_id']);
|
||||
} else {
|
||||
admin_delete_remember_tokens();
|
||||
}
|
||||
|
||||
admin_clear_remember_cookie();
|
||||
}
|
||||
|
||||
function admin_try_remember_login(): bool {
|
||||
static $checked = false;
|
||||
if ($checked) return !empty($_SESSION['admin_id']);
|
||||
$checked = true;
|
||||
|
||||
$cookie = (string)($_COOKIE[ADMIN_REMEMBER_COOKIE] ?? '');
|
||||
if ($cookie === '' || strpos($cookie, ':') === false) return false;
|
||||
|
||||
[$selector, $token] = explode(':', $cookie, 2);
|
||||
if ($selector === '' || $token === '') {
|
||||
admin_clear_remember_cookie();
|
||||
return false;
|
||||
}
|
||||
|
||||
// guard against oversized/invalid payloads before hitting DB
|
||||
if (!preg_match('/^[a-f0-9]{24}$/i', $selector) || !preg_match('/^[a-f0-9]{64}$/i', $token)) {
|
||||
admin_clear_remember_cookie();
|
||||
return false;
|
||||
}
|
||||
|
||||
admin_delete_remember_tokens(); // prune expired
|
||||
|
||||
try {
|
||||
$st = pdo()->prepare("SELECT admin_id, token_hash, expires_at FROM admin_remember_tokens WHERE selector = ? LIMIT 1");
|
||||
$st->execute([$selector]);
|
||||
$row = $st->fetch();
|
||||
} catch (Throwable $e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$row) {
|
||||
admin_clear_remember_cookie();
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((int)$row['expires_at'] < time()) {
|
||||
admin_delete_remember_tokens(null, $selector);
|
||||
admin_clear_remember_cookie();
|
||||
return false;
|
||||
}
|
||||
|
||||
$expected = (string)($row['token_hash'] ?? '');
|
||||
if (!hash_equals($expected, hash('sha256', $token))) {
|
||||
admin_delete_remember_tokens(null, $selector);
|
||||
admin_clear_remember_cookie();
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$u = pdo()->prepare("SELECT id, username FROM admin_users WHERE id = ? LIMIT 1");
|
||||
$u->execute([(int)$row['admin_id']]);
|
||||
$adminRow = $u->fetch() ?: [];
|
||||
} catch (Throwable $e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (empty($adminRow)) {
|
||||
admin_delete_remember_tokens(null, $selector);
|
||||
admin_clear_remember_cookie();
|
||||
return false;
|
||||
}
|
||||
|
||||
admin_start_session($adminRow);
|
||||
admin_issue_remember_token((int)$adminRow['id']); // rotate token after successful auto-login
|
||||
return true;
|
||||
}
|
||||
|
||||
function admin_is_logged_in(): bool {
|
||||
if (!empty($_SESSION['admin_id'])) return true;
|
||||
return admin_try_remember_login();
|
||||
}
|
||||
|
||||
function admin_login(array $adminRow, bool $remember = false): void {
|
||||
admin_start_session($adminRow);
|
||||
if ($remember) {
|
||||
admin_issue_remember_token((int)$_SESSION['admin_id']);
|
||||
} else {
|
||||
admin_forget_remember_me();
|
||||
}
|
||||
}
|
||||
|
||||
function admin_logout(): void {
|
||||
admin_forget_remember_me();
|
||||
unset($_SESSION['admin_id'], $_SESSION['admin_user']);
|
||||
}
|
||||
|
||||
function require_admin_login(): void {
|
||||
if (!admin_is_logged_in()) {
|
||||
header('Location: ' . url_path('/public/admin/login.php'));
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Optional: normal site users (not admin panel)
|
||||
function user_is_logged_in(): bool {
|
||||
return !empty($_SESSION['uid']);
|
||||
}
|
||||
function user_login(array $user): void {
|
||||
if (session_status() === PHP_SESSION_ACTIVE) {
|
||||
session_regenerate_id(true);
|
||||
}
|
||||
$_SESSION['uid'] = (int)($user['id'] ?? 0);
|
||||
}
|
||||
function user_logout(): void {
|
||||
unset($_SESSION['uid']);
|
||||
}
|
||||
function require_user_login(): void {
|
||||
if (!user_is_logged_in()) {
|
||||
header('Location: ' . url_path('/login.php'));
|
||||
exit;
|
||||
}
|
||||
}
|
||||
43
includes/bootstrap.php
Normal file
43
includes/bootstrap.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
$app = require __DIR__ . '/../config/app.php';
|
||||
$sp = require __DIR__ . '/../config/spotify_secrets.php';
|
||||
|
||||
define('SPOTIFY_CLIENT_ID', (string)($sp['client_id'] ?? ''));
|
||||
define('SPOTIFY_CLIENT_SECRET', (string)($sp['client_secret'] ?? ''));
|
||||
define('SPOTIFY_REDIRECT_URI', (string)($sp['redirect_uri'] ?? ''));
|
||||
|
||||
$basePath = rtrim((string)($app['base_path'] ?? ''), '/');
|
||||
$GLOBALS['APP_CFG'] = $app;
|
||||
$GLOBALS['BASE_PATH'] = $basePath;
|
||||
|
||||
function url_path(string $path): string {
|
||||
$base = rtrim((string)($GLOBALS['BASE_PATH'] ?? ''), '/');
|
||||
$path = '/' . ltrim($path, '/');
|
||||
return $base === '' ? $path : $base . $path;
|
||||
}
|
||||
|
||||
function is_https(): bool {
|
||||
if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') return true;
|
||||
if (!empty($_SERVER['SERVER_PORT']) && (int)$_SERVER['SERVER_PORT'] === 443) return true;
|
||||
if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower((string)$_SERVER['HTTP_X_FORWARDED_PROTO']) === 'https') return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_set_cookie_params([
|
||||
'lifetime' => 0,
|
||||
'path' => $basePath ? $basePath . '/' : '/',
|
||||
'secure' => is_https(),
|
||||
'httponly' => true,
|
||||
'samesite' => 'Lax',
|
||||
]);
|
||||
session_start();
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/db.php';
|
||||
require_once __DIR__ . '/flash.php';
|
||||
require_once __DIR__ . '/csrf.php';
|
||||
require_once __DIR__ . '/auth.php';
|
||||
require_once __DIR__ . '/media.php';
|
||||
23
includes/csrf.php
Normal file
23
includes/csrf.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
function csrf_token(): string {
|
||||
if (empty($_SESSION['_csrf'])) {
|
||||
$_SESSION['_csrf'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
return (string)$_SESSION['_csrf'];
|
||||
}
|
||||
|
||||
function csrf_field(): string {
|
||||
$t = htmlspecialchars(csrf_token(), ENT_QUOTES, 'UTF-8');
|
||||
return '<input type="hidden" name="csrf" value="'.$t.'">';
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a CSRF token (uses provided token or POST body if omitted).
|
||||
*/
|
||||
function csrf_check(?string $token = null): bool {
|
||||
$sent = $token ?? (string)($_POST['csrf'] ?? '');
|
||||
$stored = (string)($_SESSION['_csrf'] ?? ($_SESSION['csrf'] ?? ''));
|
||||
return ($sent !== '' && $stored !== '' && hash_equals($stored, $sent));
|
||||
}
|
||||
25
includes/db.php
Normal file
25
includes/db.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
function pdo(): PDO {
|
||||
static $pdo;
|
||||
if ($pdo instanceof PDO) return $pdo;
|
||||
|
||||
$app = require __DIR__ . '/../config/app.php';
|
||||
$db = $app['db'];
|
||||
|
||||
$dsn = sprintf(
|
||||
'mysql:host=%s;dbname=%s;charset=%s',
|
||||
$db['host'],
|
||||
$db['name'],
|
||||
$db['charset']
|
||||
);
|
||||
|
||||
$pdo = new PDO($dsn, $db['user'], $db['pass'], [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
]);
|
||||
|
||||
return $pdo;
|
||||
}
|
||||
13
includes/flash.php
Normal file
13
includes/flash.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
function flash_set(string $type, string $msg): void {
|
||||
$_SESSION['_flash'] = ['type' => $type, 'msg' => $msg];
|
||||
}
|
||||
|
||||
function flash_get(): ?array {
|
||||
if (empty($_SESSION['_flash'])) return null;
|
||||
$f = $_SESSION['_flash'];
|
||||
unset($_SESSION['_flash']);
|
||||
return $f;
|
||||
}
|
||||
80
includes/media.php
Normal file
80
includes/media.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
// Absolute path to the shared "Project imgs" directory.
|
||||
function project_media_base_path(): string
|
||||
{
|
||||
return dirname(__DIR__) . DIRECTORY_SEPARATOR . 'Project imgs';
|
||||
}
|
||||
|
||||
// Compute a safe folder name for a project.
|
||||
function project_media_folder(array $project): string
|
||||
{
|
||||
$slug = trim((string)($project['slug'] ?? ''));
|
||||
if ($slug !== '' && preg_match('/^[a-zA-Z0-9-]+$/', $slug)) {
|
||||
return strtolower($slug);
|
||||
}
|
||||
|
||||
$title = trim((string)($project['title'] ?? ''));
|
||||
if ($title !== '') {
|
||||
$folder = strtolower((string)preg_replace('/[^a-z0-9]+/i', '-', $title));
|
||||
$folder = trim($folder, '-');
|
||||
if ($folder !== '') return $folder;
|
||||
}
|
||||
|
||||
if (!empty($project['id'])) {
|
||||
return 'project-' . (int)$project['id'];
|
||||
}
|
||||
|
||||
return 'project-media';
|
||||
}
|
||||
|
||||
|
||||
// Return the full filesystem path for the project's media directory.
|
||||
function project_media_dir(array $project): string
|
||||
{
|
||||
$dir = project_media_base_path() . DIRECTORY_SEPARATOR . project_media_folder($project);
|
||||
if (!is_dir($dir)) {
|
||||
@mkdir($dir, 0755, true);
|
||||
}
|
||||
return $dir;
|
||||
}
|
||||
|
||||
|
||||
// Build a public URL for a stored media file.
|
||||
|
||||
function project_media_url(string $folder, string $filename): string
|
||||
{
|
||||
$base = '/Project%20imgs';
|
||||
return url_path($base . '/' . rawurlencode($folder) . '/' . rawurlencode($filename));
|
||||
}
|
||||
|
||||
// Return a list of image URLs for a project (sorted newest first).
|
||||
function project_media_files(array $project): array
|
||||
{
|
||||
$folder = project_media_folder($project);
|
||||
$dir = project_media_base_path() . DIRECTORY_SEPARATOR . $folder;
|
||||
if (!is_dir($dir)) return [];
|
||||
$files = [];
|
||||
foreach (glob($dir . DIRECTORY_SEPARATOR . '*.{jpg,jpeg,png,webp}', GLOB_BRACE) ?: [] as $path) {
|
||||
if (!is_file($path)) continue;
|
||||
$stat = @stat($path) ?: [];
|
||||
$files[] = [
|
||||
'url' => project_media_url($folder, basename($path)),
|
||||
'modified' => (int)($stat['mtime'] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
usort($files, fn($a, $b) => ($b['modified'] ?? 0) <=> ($a['modified'] ?? 0));
|
||||
return array_values(array_map(fn($f) => (string)$f['url'], $files));
|
||||
}
|
||||
|
||||
// Slugify a string to lower-case letters/numbers/hyphens.
|
||||
function project_slugify(string $value): string
|
||||
{
|
||||
$value = trim($value);
|
||||
$value = preg_replace('/[^a-z0-9]+/i', '-', $value) ?? '';
|
||||
$value = trim($value, '-');
|
||||
$value = function_exists('mb_strtolower') ? mb_strtolower($value) : strtolower($value);
|
||||
return $value;
|
||||
}
|
||||
144
includes/spotify.php
Normal file
144
includes/spotify.php
Normal file
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
function spotify_cfg(string $key, string $default = ''): string {
|
||||
$v = getenv($key);
|
||||
if ($v !== false && $v !== '') return $v;
|
||||
return $default;
|
||||
}
|
||||
|
||||
function spotify_client_id(): string { return spotify_cfg('SPOTIFY_CLIENT_ID'); }
|
||||
function spotify_client_secret(): string { return spotify_cfg('SPOTIFY_CLIENT_SECRET'); }
|
||||
function spotify_redirect_uri(): string { return spotify_cfg('SPOTIFY_REDIRECT_URI'); }
|
||||
|
||||
function spotify_http_post(string $url, array $fields, array $headers = []): array {
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => http_build_query($fields),
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 12,
|
||||
CURLOPT_HTTPHEADER => array_merge([
|
||||
'Content-Type: application/x-www-form-urlencoded'
|
||||
], $headers),
|
||||
]);
|
||||
$raw = curl_exec($ch);
|
||||
$code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$err = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($raw === false) return ['ok'=>false,'code'=>$code,'error'=>$err ?: 'curl_error','data'=>null];
|
||||
$data = json_decode($raw, true);
|
||||
return ['ok'=>($code >= 200 && $code < 300),'code'=>$code,'error'=>$err ?: null,'data'=>$data];
|
||||
}
|
||||
|
||||
function spotify_http_get(string $url, string $accessToken): array {
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 10,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Authorization: Bearer ' . $accessToken,
|
||||
'Accept: application/json',
|
||||
],
|
||||
]);
|
||||
$raw = curl_exec($ch);
|
||||
$code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$err = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($raw === false) return ['ok'=>false,'code'=>$code,'error'=>$err ?: 'curl_error','data'=>null];
|
||||
$data = ($raw !== '' ? json_decode($raw, true) : null);
|
||||
return ['ok'=>($code >= 200 && $code < 300),'code'=>$code,'error'=>$err ?: null,'data'=>$data];
|
||||
}
|
||||
|
||||
function spotify_save_tokens(?string $refresh, ?string $access, int $expiresIn): void {
|
||||
$expiresAt = time() + max(0, $expiresIn) - 30; // safety margin
|
||||
$stmt = pdo()->prepare("
|
||||
UPDATE spotify_tokens
|
||||
SET refresh_token = COALESCE(?, refresh_token),
|
||||
access_token = ?,
|
||||
access_expires = ?,
|
||||
updated_at = ?
|
||||
WHERE id = 1
|
||||
");
|
||||
$stmt->execute([$refresh, $access, $expiresAt, time()]);
|
||||
}
|
||||
|
||||
function spotify_get_stored_row(): ?array {
|
||||
try {
|
||||
$row = pdo()->query("SELECT refresh_token, access_token, access_expires FROM spotify_tokens WHERE id=1")->fetch();
|
||||
return $row ?: null;
|
||||
} catch (Throwable $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function spotify_exchange_code_for_tokens(string $code): bool {
|
||||
$cid = spotify_client_id();
|
||||
$sec = spotify_client_secret();
|
||||
$redir = spotify_redirect_uri();
|
||||
if ($cid === '' || $sec === '' || $redir === '') return false;
|
||||
|
||||
$basic = base64_encode($cid . ':' . $sec);
|
||||
$res = spotify_http_post(
|
||||
'https://accounts.spotify.com/api/token',
|
||||
[
|
||||
'grant_type' => 'authorization_code',
|
||||
'code' => $code,
|
||||
'redirect_uri' => $redir,
|
||||
],
|
||||
['Authorization: Basic ' . $basic]
|
||||
);
|
||||
|
||||
if (!$res['ok'] || !is_array($res['data'])) return false;
|
||||
|
||||
$access = (string)($res['data']['access_token'] ?? '');
|
||||
$refresh = (string)($res['data']['refresh_token'] ?? '');
|
||||
$expires = (int)($res['data']['expires_in'] ?? 0);
|
||||
|
||||
if ($access === '' || $refresh === '' || $expires <= 0) return false;
|
||||
spotify_save_tokens($refresh, $access, $expires);
|
||||
return true;
|
||||
}
|
||||
|
||||
function spotify_refresh_access_token(): ?string {
|
||||
$row = spotify_get_stored_row();
|
||||
$refresh = (string)($row['refresh_token'] ?? '');
|
||||
if ($refresh === '') return null;
|
||||
|
||||
$cid = spotify_client_id();
|
||||
$sec = spotify_client_secret();
|
||||
if ($cid === '' || $sec === '') return null;
|
||||
|
||||
$basic = base64_encode($cid . ':' . $sec);
|
||||
$res = spotify_http_post(
|
||||
'https://accounts.spotify.com/api/token',
|
||||
[
|
||||
'grant_type' => 'refresh_token',
|
||||
'refresh_token' => $refresh,
|
||||
],
|
||||
['Authorization: Basic ' . $basic]
|
||||
);
|
||||
|
||||
if (!$res['ok'] || !is_array($res['data'])) return null;
|
||||
|
||||
$access = (string)($res['data']['access_token'] ?? '');
|
||||
$expires = (int)($res['data']['expires_in'] ?? 0);
|
||||
if ($access === '' || $expires <= 0) return null;
|
||||
|
||||
// refresh token usually not returned here; keep existing one
|
||||
spotify_save_tokens(null, $access, $expires);
|
||||
return $access;
|
||||
}
|
||||
|
||||
function spotify_get_access_token(): ?string {
|
||||
$row = spotify_get_stored_row();
|
||||
if (!$row) return null;
|
||||
|
||||
$access = (string)($row['access_token'] ?? '');
|
||||
$exp = (int)($row['access_expires'] ?? 0);
|
||||
|
||||
if ($access !== '' && $exp > time() + 30) return $access;
|
||||
return spotify_refresh_access_token();
|
||||
}
|
||||
Reference in New Issue
Block a user