Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
Tokens
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
4 / 4
8
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 create
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
5
 list
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 revoke
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace Cosray\User;
6
7use Celema\Quma\Database;
8use Cosray\Actor;
9use Cosray\Uid;
10use Cosray\User;
11use DateTimeImmutable;
12use DateTimeInterface;
13use ValueError;
14
15/**
16 * A user's API tokens. A token acts as its user, so the user's roles decide
17 * what it may do; only its sha256 is stored.
18 */
19final class Tokens
20{
21    public const int MAX_NAME_LENGTH = 255;
22
23    public function __construct(
24        private readonly Database $db,
25    ) {}
26
27    /**
28     * The token itself is returned only here; it cannot be read again.
29     *
30     * @return array{uid: string, token: string}
31     */
32    public function create(User $user, string $name, Actor $actor, ?DateTimeInterface $expires = null): array
33    {
34        $name = trim($name);
35
36        if ($name === '' || mb_strlen($name) > self::MAX_NAME_LENGTH) {
37            throw new ValueError('A token needs a name of 1 to ' . self::MAX_NAME_LENGTH . ' characters');
38        }
39
40        if ($expires !== null && $expires <= new DateTimeImmutable()) {
41            throw new ValueError('A token must expire in the future');
42        }
43
44        $token = bin2hex(random_bytes(32));
45        $uid = new Uid(Uid::ALPHABET_LOWERCASE_WORD_SAFE, 13)->generate();
46
47        $this->db->users->createToken([
48            'token' => hash('sha256', $token),
49            'uid' => $uid,
50            'usr' => $user->id,
51            'name' => $name,
52            'expires' => $expires?->format(DATE_ATOM),
53            'actor' => $actor->id,
54        ])->run();
55
56        return ['uid' => $uid, 'token' => $token];
57    }
58
59    /** @return list<array{uid: string, name: string, created: string, expires: ?string, used: ?string}> */
60    public function list(User $user): array
61    {
62        /** @var list<array{uid: string, name: string, created: string, expires: ?string, used: ?string}> */
63        return $this->db->users->tokens(['usr' => $user->id])->all();
64    }
65
66    public function revoke(User $user, string $uid): bool
67    {
68        return $this->db->users->deleteToken(['uid' => $uid, 'usr' => $user->id])->len() > 0;
69    }
70}