diff --git a/api/src/api/routes/redirect.py b/api/src/api/routes/redirect.py deleted file mode 100644 index 79a232c..0000000 --- a/api/src/api/routes/redirect.py +++ /dev/null @@ -1,49 +0,0 @@ -from flask import redirect, jsonify, request, Response - -from api.route import Route -from core.logger import Logger -from data.schemas.public.short_url import ShortUrl -from data.schemas.public.short_url_dao import shortUrlDao -from data.schemas.public.short_url_visit import ShortUrlVisit -from data.schemas.public.short_url_visit_dao import shortUrlVisitDao - -logger = Logger(__name__) - - -@Route.get(f"/api/find/") -async def find(path: str): - from_db = await shortUrlDao.find_single_by({ShortUrl.short_url: path}) - if from_db is None: - return jsonify(None) - - return jsonify(from_db.to_dto()) - - -@Route.get(f"/api/redirect/") -async def handle_short_url(path: str): - if path.startswith("api/"): - path = path.replace("api/", "") - - from_db = await shortUrlDao.find_single_by({ShortUrl.short_url: path}) - if from_db is None: - return {"error": "Short URL not found"}, 404 - - try: - await shortUrlVisitDao.create( - ShortUrlVisit( - 0, - from_db.id, - request.headers.get("User-Agent") - ) - ) - except Exception as e: - logger.error(f"Failed to update short url {from_db.short_url} with error", e) - - return _do_redirect(from_db.target_url) - -def _do_redirect(url: str) -> Response: - # todo: multiple protocols like ts3:// - if not url.startswith("http://") and not url.startswith("https://"): - url = f"http://{url}" - - return redirect(url) diff --git a/api/src/core/database/database.py b/api/src/core/database/database.py index 347dc65..36adc9f 100644 --- a/api/src/core/database/database.py +++ b/api/src/core/database/database.py @@ -1,4 +1,9 @@ +from core.database.database_settings import DatabaseSettings from core.database.db_context import DBContext +from core.environment import Environment +from core.logger import DBLogger + +logger = DBLogger(__name__) class Database: @@ -14,3 +19,30 @@ class Database: @classmethod def connect(cls): cls._db.connect() + + @classmethod + async def startup_db(cls): + from data.service.migration_service import MigrationService + + logger.info("Init DB") + db = DBContext() + host = Environment.get("DB_HOST", str) + port = Environment.get("DB_PORT", int) + user = Environment.get("DB_USER", str) + password = Environment.get("DB_PASSWORD", str) + database = Environment.get("DB_DATABASE", str) + + if None in [host, port, user, password, database]: + logger.fatal( + "DB settings are not set correctly", + EnvironmentError("DB settings are not set correctly"), + ) + + await db.connect( + DatabaseSettings( + host=host, port=port, user=user, password=password, database=database + ) + ) + Database.init(db) + migrations = MigrationService(db) + await migrations.migrate() diff --git a/api/src/redirector.py b/api/src/redirector.py new file mode 100644 index 0000000..85b760c --- /dev/null +++ b/api/src/redirector.py @@ -0,0 +1,117 @@ +import asyncio +import sys + +import eventlet +from eventlet import wsgi +from flask import Flask, request, Response, redirect, render_template + +from core.database.database import Database +from core.environment import Environment +from core.logger import Logger +from data.schemas.public.short_url import ShortUrl +from data.schemas.public.short_url_dao import shortUrlDao +from data.schemas.public.short_url_visit import ShortUrlVisit +from data.schemas.public.short_url_visit_dao import shortUrlVisitDao + +logger = Logger(__name__) + + +class Redirector(Flask): + + def __init__(self, *args, **kwargs): + Flask.__init__(self, *args, **kwargs) + + +app = Redirector(__name__) + + +@app.route("/") +async def _handle_request(path: str): + short_url = await _find_short_url_by_url(path) + if short_url is None: + return render_template("404.html"), 404 + + user_agent = request.headers.get("User-Agent", "").lower() + + if "wheregoes" in user_agent or "someothertool" in user_agent: + return await _handle_short_url(path, short_url) + + if short_url.loading_screen: + return render_template( + "redirect.html", + key=short_url.short_url, + target_url=_get_redirect_url(short_url.target_url), + ) + + return await _handle_short_url(path, short_url) + + +async def _handle_short_url(path: str, short_url: ShortUrl): + if path.startswith("api/"): + path = path.replace("api/", "") + + try: + await shortUrlVisitDao.create( + ShortUrlVisit(0, short_url.id, request.headers.get("User-Agent")) + ) + except Exception as e: + logger.error(f"Failed to update short url {short_url.short_url} with error", e) + + return _do_redirect(short_url.target_url) + + +async def _find_short_url_by_url(url: str) -> ShortUrl: + return await shortUrlDao.find_single_by({ShortUrl.short_url: url}) + + +def _get_redirect_url(url: str) -> str: + # todo: multiple protocols like ts3:// + if not url.startswith("http://") and not url.startswith("https://"): + url = f"http://{url}" + + return url + + +def _do_redirect(url: str) -> Response: + return redirect(_get_redirect_url(url)) + + +async def configure(): + Logger.set_level(Environment.get("LOG_LEVEL", str, "info")) + Environment.set_environment(Environment.get("ENVIRONMENT", str, "production")) + logger.info(f"Environment: {Environment.get_environment()}") + + app.debug = Environment.get_environment() == "development" + + await Database.startup_db() + + +def main(): + if sys.platform == "win32": + from asyncio import WindowsSelectorEventLoopPolicy + + asyncio.set_event_loop_policy(WindowsSelectorEventLoopPolicy()) + + loop = asyncio.new_event_loop() + loop.run_until_complete(configure()) + loop.close() + + port = Environment.get("PORT", int, 5001) + logger.info(f"Start API on port: {port}") + if Environment.get_environment() == "development": + logger.info(f"Playground: http://localhost:{port}/") + wsgi.server(eventlet.listen(("0.0.0.0", port)), app, log_output=False) + + +if __name__ == "__main__": + main() + +# (( +# ( ) +# ; / , +# / \/ +# / | +# / ~/ +# / ) ) ~ edraft +# ___// | / +# --' \_~-, diff --git a/api/src/startup.py b/api/src/startup.py index 8682fb6..582f661 100644 --- a/api/src/startup.py +++ b/api/src/startup.py @@ -19,30 +19,6 @@ logger = Logger(__name__) class Startup: - @staticmethod - async def _startup_db(): - logger.info("Init DB") - db = DBContext() - host = Environment.get("DB_HOST", str) - port = Environment.get("DB_PORT", int) - user = Environment.get("DB_USER", str) - password = Environment.get("DB_PASSWORD", str) - database = Environment.get("DB_DATABASE", str) - - if None in [host, port, user, password, database]: - logger.fatal( - "DB settings are not set correctly", - EnvironmentError("DB settings are not set correctly"), - ) - - await db.connect( - DatabaseSettings( - host=host, port=port, user=user, password=password, database=database - ) - ) - Database.init(db) - migrations = MigrationService(db) - await migrations.migrate() @staticmethod async def _seed_data(): @@ -69,7 +45,7 @@ class Startup: app.debug = Environment.get_environment() == "development" - await cls._startup_db() + await Database.startup_db() await FileService.clean_files() await cls._seed_data() diff --git a/api/src/static/custom/background.jpg b/api/src/static/custom/background.jpg new file mode 100644 index 0000000..7025a96 Binary files /dev/null and b/api/src/static/custom/background.jpg differ diff --git a/api/src/static/custom/logo.png b/api/src/static/custom/logo.png new file mode 100644 index 0000000..312aad5 Binary files /dev/null and b/api/src/static/custom/logo.png differ diff --git a/api/src/static/not_found.gif b/api/src/static/not_found.gif new file mode 100644 index 0000000..1d72137 Binary files /dev/null and b/api/src/static/not_found.gif differ diff --git a/api/src/static/styles.css b/api/src/static/styles.css new file mode 100644 index 0000000..3e5d34d --- /dev/null +++ b/api/src/static/styles.css @@ -0,0 +1,27 @@ +body { + background-color: #1e293b; + padding: 0; + margin: 0; +} + +h1 { + color: #a2271f; + font-size: 3rem !important; + font-weight: 800 !important; +} + +.bg-2 { + background-color: #0f172a; +} + +.tile { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + gap: 40px; + + padding: 35px; + border-radius: 20px; +} diff --git a/api/src/templates/404.html b/api/src/templates/404.html new file mode 100644 index 0000000..c40e0f0 --- /dev/null +++ b/api/src/templates/404.html @@ -0,0 +1,18 @@ + + + + + 404 - Not found + + + + + +
+
+

404 - Not found

+ +
+
+ + \ No newline at end of file diff --git a/api/src/templates/redirect.html b/api/src/templates/redirect.html new file mode 100644 index 0000000..ef8d573 --- /dev/null +++ b/api/src/templates/redirect.html @@ -0,0 +1,39 @@ + + + + + Redirecting {{ key }} + + + + +
+
+
+ +
+
+ + +

Redirecting...

+

You will be redirected in 5 seconds.

+
+
+
+ + + \ No newline at end of file diff --git a/version.txt b/version.txt index 7dff5b8..9325c3c 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.2.1 \ No newline at end of file +0.3.0 \ No newline at end of file diff --git a/web/src/app/app-routing.module.ts b/web/src/app/app-routing.module.ts index 40c40bb..5496efc 100644 --- a/web/src/app/app-routing.module.ts +++ b/web/src/app/app-routing.module.ts @@ -1,23 +1,22 @@ -import { NgModule } from "@angular/core"; -import { RouterModule, Routes } from "@angular/router"; -import { NotFoundComponent } from "src/app/components/error/not-found/not-found.component"; -import { AuthGuard } from "src/app/core/guard/auth.guard"; -import { HomeComponent } from "src/app/components/home/home.component"; -import { RedirectComponent } from "src/app/components/redirect/redirect.component"; +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { NotFoundComponent } from 'src/app/components/error/not-found/not-found.component'; +import { AuthGuard } from 'src/app/core/guard/auth.guard'; +import { HomeComponent } from 'src/app/components/home/home.component'; const routes: Routes = [ { - path: "", + path: '', component: HomeComponent, }, { - path: "admin", + path: 'admin', loadChildren: () => - import("./modules/admin/admin.module").then((m) => m.AdminModule), + import('./modules/admin/admin.module').then(m => m.AdminModule), canActivate: [AuthGuard], }, - { path: "404", component: NotFoundComponent }, - { path: "**", component: RedirectComponent }, + { path: '404', component: NotFoundComponent }, + { path: '**', redirectTo: '/404', pathMatch: 'full' }, ]; @NgModule({ diff --git a/web/src/app/app.module.ts b/web/src/app/app.module.ts index 8945836..4bba424 100644 --- a/web/src/app/app.module.ts +++ b/web/src/app/app.module.ts @@ -1,28 +1,27 @@ -import { APP_INITIALIZER, ErrorHandler, NgModule } from "@angular/core"; -import { BrowserModule } from "@angular/platform-browser"; +import { APP_INITIALIZER, ErrorHandler, NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; -import { AppRoutingModule } from "./app-routing.module"; -import { AppComponent } from "./app.component"; -import { KeycloakService } from "keycloak-angular"; -import { initializeKeycloak } from "./core/init-keycloak"; -import { HttpClient } from "@angular/common/http"; -import { environment } from "../environments/environment"; -import { FooterComponent } from "src/app/components/footer/footer.component"; -import { HeaderComponent } from "src/app/components/header/header.component"; -import { NotFoundComponent } from "src/app/components/error/not-found/not-found.component"; -import { TranslateLoader, TranslateModule } from "@ngx-translate/core"; -import { TranslateHttpLoader } from "@ngx-translate/http-loader"; -import { Logger } from "src/app/service/logger.service"; -import { SharedModule } from "src/app/modules/shared/shared.module"; -import { SpinnerComponent } from "src/app/components/spinner/spinner.component"; -import { ConfirmationService, MessageService } from "primeng/api"; -import { DialogService } from "primeng/dynamicdialog"; -import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; -import { SidebarComponent } from "./components/sidebar/sidebar.component"; -import { ErrorHandlingService } from "src/app/service/error-handling.service"; -import { HomeComponent } from "./components/home/home.component"; -import { RedirectComponent } from "./components/redirect/redirect.component"; -import { SettingsService } from "src/app/service/settings.service"; +import { AppRoutingModule } from './app-routing.module'; +import { AppComponent } from './app.component'; +import { KeycloakService } from 'keycloak-angular'; +import { initializeKeycloak } from './core/init-keycloak'; +import { HttpClient } from '@angular/common/http'; +import { environment } from '../environments/environment'; +import { FooterComponent } from 'src/app/components/footer/footer.component'; +import { HeaderComponent } from 'src/app/components/header/header.component'; +import { NotFoundComponent } from 'src/app/components/error/not-found/not-found.component'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { TranslateHttpLoader } from '@ngx-translate/http-loader'; +import { Logger } from 'src/app/service/logger.service'; +import { SharedModule } from 'src/app/modules/shared/shared.module'; +import { SpinnerComponent } from 'src/app/components/spinner/spinner.component'; +import { ConfirmationService, MessageService } from 'primeng/api'; +import { DialogService } from 'primeng/dynamicdialog'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { SidebarComponent } from './components/sidebar/sidebar.component'; +import { ErrorHandlingService } from 'src/app/service/error-handling.service'; +import { HomeComponent } from './components/home/home.component'; +import { SettingsService } from 'src/app/service/settings.service'; if (environment.production) { Logger.enableProductionMode(); @@ -36,7 +35,7 @@ export function HttpLoaderFactory(http: HttpClient) { export function appInitializerFactory( keycloak: KeycloakService, - settings: SettingsService, + settings: SettingsService ): () => Promise { return (): Promise => new Promise((resolve, reject) => { @@ -44,7 +43,7 @@ export function appInitializerFactory( .loadSettings() .then(() => initializeKeycloak(keycloak, settings)) .then(() => resolve()) - .catch((error) => reject(error)); + .catch(error => reject(error)); }); } @@ -57,7 +56,6 @@ export function appInitializerFactory( SpinnerComponent, SidebarComponent, HomeComponent, - RedirectComponent, ], imports: [ BrowserModule, @@ -65,7 +63,7 @@ export function appInitializerFactory( AppRoutingModule, SharedModule, TranslateModule.forRoot({ - defaultLanguage: "en", + defaultLanguage: 'en', loader: { provide: TranslateLoader, useFactory: HttpLoaderFactory, diff --git a/web/src/app/components/redirect/redirect.component.html b/web/src/app/components/redirect/redirect.component.html deleted file mode 100644 index 52e15e4..0000000 --- a/web/src/app/components/redirect/redirect.component.html +++ /dev/null @@ -1,16 +0,0 @@ -
-
- -
-
- - -

Redirecting...

-

You will be redirected in {{ secs }} seconds.

-
-
\ No newline at end of file diff --git a/web/src/app/components/redirect/redirect.component.scss b/web/src/app/components/redirect/redirect.component.scss deleted file mode 100644 index 50c5f64..0000000 --- a/web/src/app/components/redirect/redirect.component.scss +++ /dev/null @@ -1,5 +0,0 @@ -.bg-image { - background-position: top center; - background-repeat: no-repeat; - background-size: cover; -} \ No newline at end of file diff --git a/web/src/app/components/redirect/redirect.component.spec.ts b/web/src/app/components/redirect/redirect.component.spec.ts deleted file mode 100644 index 496700d..0000000 --- a/web/src/app/components/redirect/redirect.component.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ComponentFixture, TestBed } from "@angular/core/testing"; - -import { RedirectComponent } from "./redirect.component"; - -describe("RedirectComponent", () => { - let component: RedirectComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [RedirectComponent], - }).compileComponents(); - - fixture = TestBed.createComponent(RedirectComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it("should create", () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/web/src/app/components/redirect/redirect.component.ts b/web/src/app/components/redirect/redirect.component.ts deleted file mode 100644 index 30e1ea4..0000000 --- a/web/src/app/components/redirect/redirect.component.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { Component, OnInit } from '@angular/core'; -import { SidebarService } from 'src/app/service/sidebar.service'; -import { Router } from '@angular/router'; -import { HttpClient } from '@angular/common/http'; -import { ShortUrlDto } from 'src/app/model/entities/short-url'; -import { SettingsService } from 'src/app/service/settings.service'; -import { AppSettings } from 'src/app/model/config/app-settings'; -import { GuiService } from 'src/app/service/gui.service'; - -@Component({ - selector: 'app-redirect', - templateUrl: './redirect.component.html', - styleUrl: './redirect.component.scss', -}) -export class RedirectComponent implements OnInit { - defaultSecs = 5; - secs = -1; - - settings: AppSettings = this.settingsService.settings; - - constructor( - private sidebar: SidebarService, - private router: Router, - private http: HttpClient, - private settingsService: SettingsService, - private gui: GuiService - ) { - this.sidebar.hide(); - this.gui.hide(); - } - - ngOnInit() { - this.handleUrl(); - } - - handleUrl() { - let shortUrl = this.router.url; - if (shortUrl === '/') { - return; - } - - if (shortUrl.startsWith('/')) { - shortUrl = shortUrl.substring(1); - } - - this.http - .get(`${this.settings.api.url}/find/${shortUrl}`) - .subscribe(async response => { - if (!response) { - await this.router.navigate(['/404']); - return; - } - - if (!response.loadingScreen) { - window.location.href = `${this.settings.api.url}/redirect/${shortUrl}`; - return; - } - - this.secs = this.defaultSecs; - setInterval(() => { - this.secs--; - - if (this.secs === 0) { - window.location.href = `${this.settings.api.url}/redirect/${shortUrl}`; - } - }, 1000); - }); - } -} diff --git a/web/src/app/model/config/app-settings.ts b/web/src/app/model/config/app-settings.ts index 52847e9..6046efc 100644 --- a/web/src/app/model/config/app-settings.ts +++ b/web/src/app/model/config/app-settings.ts @@ -1,4 +1,4 @@ -import { Theme } from "src/app/model/view/theme"; +import { Theme } from 'src/app/model/view/theme'; export interface AppSettings { termsUrl: string; @@ -23,4 +23,5 @@ export interface KeycloakSettings { export interface ApiSettings { url: string; + redirector: string; } diff --git a/web/src/app/modules/admin/short-urls/short-urls.page.ts b/web/src/app/modules/admin/short-urls/short-urls.page.ts index ee30890..fd5dde1 100644 --- a/web/src/app/modules/admin/short-urls/short-urls.page.ts +++ b/web/src/app/modules/admin/short-urls/short-urls.page.ts @@ -8,6 +8,7 @@ import { ShortUrlsDataService } from 'src/app/modules/admin/short-urls/short-url import { ShortUrlsColumns } from 'src/app/modules/admin/short-urls/short-urls.columns'; import { AuthService } from 'src/app/service/auth.service'; import { Filter } from 'src/app/model/graphql/filter/filter.model'; +import { SettingsService } from 'src/app/service/settings.service'; @Component({ selector: 'app-short-urls', @@ -43,7 +44,8 @@ export class ShortUrlsPage constructor( private toast: ToastService, private confirmation: ConfirmationDialogService, - private auth: AuthService + private auth: AuthService, + private settings: SettingsService ) { super(true, { read: [PermissionsEnum.shortUrls], @@ -137,13 +139,15 @@ export class ShortUrlsPage } open(url: string) { - window.open(url, '_blank'); + window.open(`${this.settings.settings.api.redirector}/${url}`, '_blank'); } copy(val: string) { - navigator.clipboard.writeText(`${window.origin}/${val}`).then(() => { - this.toast.info('common.copied', 'common.copied_to_clipboard'); - }); + navigator.clipboard + .writeText(`${this.settings.settings.api.redirector}/${val}`) + .then(() => { + this.toast.info('common.copied', 'common.copied_to_clipboard'); + }); } getShortUrlsWithoutGroup(): ShortUrl[] { diff --git a/web/src/assets/config/config.json b/web/src/assets/config/config.json index 5a11e79..e605683 100644 --- a/web/src/assets/config/config.json +++ b/web/src/assets/config/config.json @@ -9,15 +9,12 @@ } ], "api": { - "url": "" + "url": "", + "redirector": "" }, "keycloak": { "url": "", "realm": "", "clientId": "" - }, - "loadingScreen": { - "background": "https://www.sh-edraft.de/wp-content/uploads/2020/03/IMG_20170731_213039-scaled.jpg", - "logo": "https://www.sh-edraft.de/wp-content/uploads/2020/04/klein_transparent-e1735827600932.png" } }