Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
4rthem committed Sep 25, 2023
1 parent bb96530 commit d7dd3f4
Show file tree
Hide file tree
Showing 25 changed files with 541 additions and 75 deletions.
1 change: 1 addition & 0 deletions databox/api/config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ services:
string $databoxClientBaseUrl: '%env(DATABOX_CLIENT_URL)%'
ApiPlatform\State\ProviderInterface $itemProvider: '@api_platform.doctrine.orm.state.item_provider'
ApiPlatform\State\ProviderInterface $itemsProvider: '@api_platform.doctrine.orm.state.collection_provider'
bool $useAlias: '%elastica.use_alias%'

_instanceof:
Alchemy\Workflow\Executor\Action\ActionInterface:
Expand Down
242 changes: 242 additions & 0 deletions databox/api/migrations/Version20230925140946.php

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions databox/api/src/Attribute/Type/DateTimeAttributeType.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@ public function getGroupValueLabel($value): ?string

public function createFilterQuery(string $field, $value): AbstractQuery
{
$startFloor = new \DateTime();
$startFloor->setTimestamp((int) $value[0]);
$startFloor = (new \DateTimeImmutable())
->setTimestamp((int) $value[0]);

$endCeil = new \DateTime();
$endCeil->setTimestamp((int) $value[1]);
$endCeil = (new \DateTimeImmutable())
->setTimestamp((int) $value[1]);

return new Range($field, [
'gte' => $startFloor->getTimestamp() * 1000,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@

readonly class ElasticsearchClient
{
public function __construct(private Client $client)
public function __construct(
private Client $client,
private bool $useAlias,
)
{
}

Expand All @@ -29,6 +32,10 @@ public function updateMapping(string $indexName, array $mapping): void
*/
public function getAliasedIndex(string $aliasName): ?string
{
if (!$this->useAlias) {
return $aliasName;
}

$aliasesInfo = $this->client->request('_aliases', 'GET')->getData();
$aliasedIndexes = [];

Expand Down
9 changes: 7 additions & 2 deletions databox/api/src/Elasticsearch/Mapping/IndexMappingUpdater.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,13 @@ class IndexMappingUpdater
{
final public const NO_LOCALE = '_';

public function __construct(private readonly ElasticsearchClient $client, private readonly Index $index, private readonly EntityManagerInterface $em, private readonly AttributeTypeRegistry $attributeTypeRegistry, private readonly FieldNameResolver $fieldNameResolver)
{
public function __construct(
private readonly ElasticsearchClient $client,
private readonly Index $index,
private readonly EntityManagerInterface $em,
private readonly AttributeTypeRegistry $attributeTypeRegistry,
private readonly FieldNameResolver $fieldNameResolver,
) {
}

public function assignAttributeToMapping(array &$mapping, string $locale, AttributeDefinition $definition): void
Expand Down
6 changes: 3 additions & 3 deletions databox/api/src/Entity/FailedEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,19 @@ class FailedEvent extends BaseFailedEvent
protected $id;

#[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
private ?\DateTime $createdAt = null;
private ?\DateTimeImmutable $createdAt = null;

public function __construct()
{
$this->createdAt = new \DateTime();
$this->createdAt = new \DateTimeImmutable();
}

public function getId(): string
{
return $this->id->__toString();
}

public function getCreatedAt(): \DateTime
public function getCreatedAt(): \DateTimeImmutable
{
return $this->createdAt;
}
Expand Down
4 changes: 2 additions & 2 deletions databox/api/src/Util/Time.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ public static function time2string(int $time): string
return '0 seconds';
}

$t1 = new \DateTime();
$t2 = new \DateTime("+$time seconds");
$t1 = new \DateTimeImmutable();
$t2 = new \DateTimeImmutable("+$time seconds");
$diff = $t1->diff($t2);
$units = [
'days' => 'day',
Expand Down
2 changes: 1 addition & 1 deletion databox/client/src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export default function App() {
switch (status) {
case 401:
toast.error(t('error.session_expired', 'Your session has expired'));
userContext.logout && userContext.logout();
userContext.logout && userContext.logout(false);
break;
case 403:
toast.error(t('error.http_unauthorized', 'Unauthorized'));
Expand Down
21 changes: 15 additions & 6 deletions databox/client/src/components/Root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ import {BrowserRouter} from "react-router-dom";
import ModalStack from "../hooks/useModalStack";
import UserPreferencesProvider from "./User/Preferences/UserPreferencesProvider";
import {oauthClient} from "../api/api-client";
import {toast} from "react-toastify";
import {
loginEventType,
logoutEventType,
sessionExpiredEventType,
} from 'react-ps';

type Props = {};

Expand All @@ -28,8 +34,11 @@ export default function Root({}: Props) {
setUser(undefined);
};

oauthClient.registerListener('login', onLogin);
oauthClient.registerListener('logout', onLogout);
oauthClient.registerListener(loginEventType, onLogin);
oauthClient.registerListener(logoutEventType, onLogout);
oauthClient.registerListener(sessionExpiredEventType, async () => {
toast.warning('Session has expired')
});

if (oauthClient.isAuthenticated()) {
onLogin();
Expand All @@ -38,13 +47,13 @@ export default function Root({}: Props) {
}

return () => {
oauthClient.unregisterListener('login', onLogin);
oauthClient.unregisterListener('logout', onLogout);
oauthClient.unregisterListener(loginEventType, onLogin);
oauthClient.unregisterListener(logoutEventType, onLogout);
}
}, [setUser]);

const logout = React.useCallback(() => {
oauthClient.logout();
const logout = React.useCallback((redirectUri: string|false = '/') => {
oauthClient.logout(redirectUri);
}, []);

return <UserContext.Provider value={{
Expand Down
2 changes: 1 addition & 1 deletion databox/client/src/components/Security/UserContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {User} from "../../types";

export type TUserContext = {
user?: User | undefined;
logout?: () => void | undefined;
logout?: (redirectUri?: string |false) => void | undefined;
}

export const UserContext = React.createContext<TUserContext>({});
34 changes: 34 additions & 0 deletions expose/api/src/Migrations/Version20230925141126.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20230925141126 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}

public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE multipart_upload ALTER created_at TYPE TIMESTAMP(0) WITHOUT TIME ZONE');
$this->addSql('COMMENT ON COLUMN multipart_upload.created_at IS \'(DC2Type:datetime_immutable)\'');
}

public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('ALTER TABLE multipart_upload ALTER created_at TYPE TIMESTAMP(0) WITHOUT TIME ZONE');
$this->addSql('COMMENT ON COLUMN multipart_upload.created_at IS NULL');
}
}
16 changes: 14 additions & 2 deletions lib/js/react-ps/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,17 @@ import IdentityProviders from "./components/IdentityProviders";
import FormLayout from "./components/FormLayout";
import DashboardMenu from "./components/DashboardMenu/DashboardMenu";
import OAuthClient, {
authenticationEventType,
loginEventType,
logoutEventType,
sessionExpiredEventType,
refreshTokenEventType,
RequestConfigWithAuth,
configureClientAuthentication,
RefreshTokenEvent,
AuthEventHandler,
LoginEvent,
AuthEvent,
LogoutEvent,
} from "./lib/oauth-client";

import {
Expand All @@ -22,11 +28,17 @@ export {
OAuthClient,
RequestConfigWithAuth,
configureClientAuthentication,
authenticationEventType,
loginEventType,
logoutEventType,
sessionExpiredEventType,
refreshTokenEventType,
DashboardMenu,
createHttpClient,
RequestConfig,
useEffectOnce,
RefreshTokenEvent,
AuthEventHandler,
LoginEvent,
AuthEvent,
LogoutEvent,
};
94 changes: 69 additions & 25 deletions lib/js/react-ps/src/lib/oauth-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,25 +26,26 @@ type UserInfoResponse = {
sub: string;
}

type AuthEvent = {
export type AuthEvent = {
type: string;
};

type LoginEvent = {
accessToken: string;
export type LoginEvent = {
response: TokenResponse;
} & AuthEvent;

type AuthenticationEvent = {
user: UserInfoResponse;
export type RefreshTokenEvent = {
response: TokenResponse;
} & AuthEvent;

type LogoutEvent = AuthEvent;
export type LogoutEvent = AuthEvent;

type AuthEventHandler = (event: AuthEvent) => Promise<void>;
export type AuthEventHandler<E extends AuthEvent = AuthEvent> = (event: E) => Promise<void>;

export const authenticationEventType = 'authentication';
export const loginEventType = 'login';
export const refreshTokenEventType = 'refreshToken';
export const logoutEventType = 'logout';
export const sessionExpiredEventType = 'sessionExpired';

export interface IStorage {
getItem(key: string): string | null;
Expand All @@ -61,12 +62,13 @@ type Options = {
}

export default class OAuthClient {
public tokenPromise: Promise<any> | undefined;
private listeners: Record<string, AuthEventHandler[]> = {};
private clientId: string;
private baseUrl: string;
private storage: IStorage;
private tokensCache: TokenResponse | undefined;
public tokenPromise: Promise<any> | undefined;
private sessionTimeout: ReturnType<typeof setTimeout> | undefined;

constructor({
clientId,
Expand Down Expand Up @@ -128,12 +130,14 @@ export default class OAuthClient {
return jwtDecode<UserInfoResponse>(accessToken);
}

logout(redirectPath: string = '/'): void {
this.storage.removeItem(tokenStorageKey);
this.tokensCache = undefined;
this.triggerEvent(logoutEventType);
logout(redirectPath: string | false = '/'): void {
this.clearSessionTimeout();

this.doLogout();

document.location.href = this.createLogoutUrl({redirectPath});
if (false !== redirectPath) {
document.location.href = this.createLogoutUrl({redirectPath});
}
}

registerListener(event: string, callback: AuthEventHandler): void {
Expand All @@ -154,16 +158,6 @@ export default class OAuthClient {
}
}

private async triggerEvent<E extends AuthEvent = AuthEvent>(type: string, event: Partial<E> = {}): Promise<void> {
event.type = type;

if (!this.listeners[type]) {
return Promise.resolve();
}

await Promise.all(this.listeners[type].map(func => func(event as E)).filter(f => !!f));
}

public async getAccessTokenFromAuthCode(code: string, redirectUri: string): Promise<TokenResponse> {
const res = await this.getToken({
code,
Expand All @@ -173,7 +167,11 @@ export default class OAuthClient {

this.persistTokens(res);

await this.triggerEvent(loginEventType);
this.handleSessionTimeout(res);

await this.triggerEvent<LoginEvent>(loginEventType, {
response: res,
});

return res;
}
Expand All @@ -185,6 +183,12 @@ export default class OAuthClient {
grant_type: 'refresh_token',
});

this.handleSessionTimeout(res);

await this.triggerEvent<RefreshTokenEvent>(refreshTokenEventType, {
response: res,
});

return res;
} catch (e: any) {
console.log('e', e);
Expand Down Expand Up @@ -244,6 +248,46 @@ export default class OAuthClient {
return `${this.baseUrl}/logout?${queryString}`;
}

public getTokenResponse(): TokenResponse | undefined {
return this.fetchTokens();
}

private doLogout(): void {
this.triggerEvent(logoutEventType);
this.storage.removeItem(tokenStorageKey);
this.tokensCache = undefined;
}

private async triggerEvent<E extends AuthEvent = AuthEvent>(type: string, event: Partial<E> = {}): Promise<void> {
event.type = type;

if (!this.listeners[type]) {
return Promise.resolve();
}

await Promise.all(this.listeners[type].map(func => func(event as E)).filter(f => !!f));
}

private handleSessionTimeout(res: TokenResponse): void {
this.clearSessionTimeout();

this.sessionTimeout = setTimeout(() => {
this.sessionExpired();
}, res.refresh_expires_in * 1000);
}

private sessionExpired(): void {
this.triggerEvent<LogoutEvent>(sessionExpiredEventType);
this.doLogout();
}

private clearSessionTimeout(): void {
if (this.sessionTimeout) {
clearTimeout(this.sessionTimeout);
this.sessionTimeout = undefined;
}
}

private persistTokens(token: TokenResponse): void {
const now = Math.ceil(new Date().getTime() / 1000);
token.expires_at = now + token.expires_in;
Expand Down
Loading

0 comments on commit d7dd3f4

Please sign in to comment.