Handle short urls with loading screen

Closes #1
Closes #3
This commit is contained in:
Sven Heidemann 2025-01-02 15:56:53 +01:00
parent dd5769823f
commit 5ffd66d06d
16 changed files with 92 additions and 14 deletions

View File

@ -1,6 +1,4 @@
import json from flask import redirect, jsonify, request, Response
from flask import redirect, jsonify, request
from api.route import Route from api.route import Route
from core.logger import Logger from core.logger import Logger
@ -41,4 +39,11 @@ async def handle_short_url(path: str):
except Exception as e: except Exception as e:
logger.error(f"Failed to update short url {from_db.short_url} with error", e) logger.error(f"Failed to update short url {from_db.short_url} with error", e)
return redirect(from_db.target_url) 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)

View File

@ -11,6 +11,7 @@ type ShortUrl implements DbModel {
description: String description: String
visits: Int visits: Int
group: Group group: Group
loadingScreen: Boolean
deleted: Boolean deleted: Boolean
editor: User editor: User
@ -22,6 +23,7 @@ input ShortUrlSort {
id: SortOrder id: SortOrder
name: SortOrder name: SortOrder
description: SortOrder description: SortOrder
loadingScreen: SortOrder
deleted: SortOrder deleted: SortOrder
editorId: SortOrder editorId: SortOrder
@ -33,6 +35,7 @@ input ShortUrlFilter {
id: IntFilter id: IntFilter
name: StringFilter name: StringFilter
description: StringFilter description: StringFilter
loadingScreen: BooleanFilter
deleted: BooleanFilter deleted: BooleanFilter
editor: IntFilter editor: IntFilter
@ -52,6 +55,7 @@ input ShortUrlCreateInput {
targetUrl: String! targetUrl: String!
description: String description: String
groupId: ID groupId: ID
loadingScreen: Boolean
} }
input ShortUrlUpdateInput { input ShortUrlUpdateInput {
@ -60,4 +64,5 @@ input ShortUrlUpdateInput {
targetUrl: String targetUrl: String
description: String description: String
groupId: ID groupId: ID
loadingScreen: Boolean
} }

View File

@ -12,6 +12,7 @@ class ShortUrlCreateInput(InputABC):
self._target_url = self.option("targetUrl", str, required=True) self._target_url = self.option("targetUrl", str, required=True)
self._description = self.option("description", str) self._description = self.option("description", str)
self._group_id = self.option("groupId", int) self._group_id = self.option("groupId", int)
self._loading_screen = self.option("loadingScreen", str)
@property @property
def short_url(self) -> str: def short_url(self) -> str:
@ -28,3 +29,7 @@ class ShortUrlCreateInput(InputABC):
@property @property
def group_id(self) -> Optional[int]: def group_id(self) -> Optional[int]:
return self._group_id return self._group_id
@property
def loading_screen(self) -> Optional[str]:
return self._loading_screen

View File

@ -13,6 +13,7 @@ class ShortUrlUpdateInput(InputABC):
self._target_url = self.option("targetUrl", str) self._target_url = self.option("targetUrl", str)
self._description = self.option("description", str) self._description = self.option("description", str)
self._group_id = self.option("groupId", int) self._group_id = self.option("groupId", int)
self._loading_screen = self.option("loadingScreen", str)
@property @property
def id(self) -> int: def id(self) -> int:
@ -33,3 +34,7 @@ class ShortUrlUpdateInput(InputABC):
@property @property
def group_id(self) -> Optional[int]: def group_id(self) -> Optional[int]:
return self._group_id return self._group_id
@property
def loading_screen(self) -> Optional[str]:
return self._loading_screen

View File

@ -49,6 +49,7 @@ class ShortUrlMutation(MutationABC):
obj.target_url, obj.target_url,
obj.description, obj.description,
obj.group_id, obj.group_id,
obj.loading_screen,
) )
nid = await shortUrlDao.create(short_url) nid = await shortUrlDao.create(short_url)
return await shortUrlDao.get_by_id(nid) return await shortUrlDao.get_by_id(nid)
@ -74,6 +75,9 @@ class ShortUrlMutation(MutationABC):
raise NotFound(f"Group with id {obj.group_id} does not exist") raise NotFound(f"Group with id {obj.group_id} does not exist")
short_url.group_id = obj.group_id short_url.group_id = obj.group_id
if obj.loading_screen is not None:
short_url.loading_screen = obj.loading_screen
await shortUrlDao.update(short_url) await shortUrlDao.update(short_url)
return await shortUrlDao.get_by_id(obj.id) return await shortUrlDao.get_by_id(obj.id)

View File

@ -10,3 +10,4 @@ class ShortUrlQuery(DbModelQueryABC):
self.set_field("description", lambda x, *_: x.description) self.set_field("description", lambda x, *_: x.description)
self.set_field("group", lambda x, *_: x.group) self.set_field("group", lambda x, *_: x.group)
self.set_field("visits", lambda x, *_: x.visit_count) self.set_field("visits", lambda x, *_: x.visit_count)
self.set_field("loadingScreen", lambda x, *_: x.loading_screen)

View File

@ -16,6 +16,7 @@ class ShortUrl(DbModelABC):
target_url: str, target_url: str,
description: Optional[str], description: Optional[str],
group_id: Optional[SerialId], group_id: Optional[SerialId],
loading_screen: Optional[str] = None,
deleted: bool = False, deleted: bool = False,
editor_id: Optional[SerialId] = None, editor_id: Optional[SerialId] = None,
created: Optional[datetime] = None, created: Optional[datetime] = None,
@ -26,6 +27,7 @@ class ShortUrl(DbModelABC):
self._target_url = target_url self._target_url = target_url
self._description = description self._description = description
self._group_id = group_id self._group_id = group_id
self._loading_screen = loading_screen
@property @property
def short_url(self) -> str: def short_url(self) -> str:
@ -74,8 +76,17 @@ class ShortUrl(DbModelABC):
return await shortUrlVisitDao.count_by_id(self.id) return await shortUrlVisitDao.count_by_id(self.id)
@property
def loading_screen(self) -> Optional[str]:
return self._loading_screen
@loading_screen.setter
def loading_screen(self, value: Optional[str]):
self._loading_screen = value
def to_dto(self) -> dict: def to_dto(self) -> dict:
return { return {
"id": self.id, "id": self.id,
"target_url": self.target_url, "target_url": self.target_url,
"loadingScreen": self.loading_screen,
} }

View File

@ -13,6 +13,7 @@ class ShortUrlDao(DbModelDaoABC[ShortUrl]):
self.attribute(ShortUrl.target_url, str) self.attribute(ShortUrl.target_url, str)
self.attribute(ShortUrl.description, str) self.attribute(ShortUrl.description, str)
self.attribute(ShortUrl.group_id, int) self.attribute(ShortUrl.group_id, int)
self.attribute(ShortUrl.loading_screen, bool)
shortUrlDao = ShortUrlDao() shortUrlDao = ShortUrlDao()

View File

@ -0,0 +1,6 @@
ALTER TABLE public.short_urls
ADD COLUMN IF NOT EXISTS loadingScreen boolean DEFAULT true;
ALTER TABLE public.short_urls_history
ADD COLUMN IF NOT EXISTS loadingScreen boolean NULL;

View File

@ -1,8 +1,8 @@
<main *ngIf="isLoggedIn; else home"> <main *ngIf="isLoggedIn && showSidebar; else home">
<app-header></app-header> <app-header></app-header>
<div class="app"> <div class="app">
<aside *ngIf="showSidebar"> <aside>
<app-sidebar></app-sidebar> <app-sidebar></app-sidebar>
</aside> </aside>
<section class="component"> <section class="component">

View File

@ -22,6 +22,7 @@ export class AppComponent implements OnDestroy {
this.auth.user$.pipe(takeUntil(this.unsubscribe$)).subscribe((user) => { this.auth.user$.pipe(takeUntil(this.unsubscribe$)).subscribe((user) => {
this.isLoggedIn = user !== null && user !== undefined; this.isLoggedIn = user !== null && user !== undefined;
console.warn("isLoggedIn");
}); });
this.sidebar.visible$ this.sidebar.visible$

View File

@ -1,5 +1,5 @@
import { Component } from "@angular/core"; import { Component } from "@angular/core";
import { AuthService } from "src/app/service/auth.service"; import { KeycloakService } from "keycloak-angular";
@Component({ @Component({
selector: "app-home", selector: "app-home",
@ -7,11 +7,9 @@ import { AuthService } from "src/app/service/auth.service";
styleUrl: "./home.component.scss", styleUrl: "./home.component.scss",
}) })
export class HomeComponent { export class HomeComponent {
constructor(private auth: AuthService) { constructor(private keycloak: KeycloakService) {
if (this.auth.user$.value !== undefined) { if (!this.keycloak.isLoggedIn()) {
return; this.keycloak.login().then(() => {});
} }
this.auth.login().then(() => {});
} }
} }

View File

@ -1 +1,16 @@
<p>redirect works!</p> <div class="flex items-center justify-center">
<div class="relative w-screen h-screen bg-cover bg-center"
style="background-image: url(https://www.sh-edraft.de/wp-content/uploads/2020/03/IMG_20170731_213039-scaled.jpg)"></div>
<div class="absolute w-1/3 h-2/5 rounded-xl p-5 flex flex-col gap-5 justify-center items-center">
<div class="absolute inset-0 bg-black opacity-70 rounded-xl"></div>
<div class="relative logo flex justify-center items-center">
<img class="h-48"
src="https://www.sh-edraft.de/wp-content/uploads/2020/04/klein_transparent-e1735827600932.png"
alt="logo">
</div>
<h1 class="relative text-3xl text-white">Redirecting...</h1>
<p class="relative text-white" *ngIf="secs > -1">You will be redirected in {{ secs }} seconds.</p>
</div>
</div>

View File

@ -0,0 +1,5 @@
.bg-image {
background-position: top center;
background-repeat: no-repeat;
background-size: cover;
}

View File

@ -11,6 +11,9 @@ import { ShortUrlDto } from "src/app/model/entities/short-url";
styleUrl: "./redirect.component.scss", styleUrl: "./redirect.component.scss",
}) })
export class RedirectComponent implements OnInit { export class RedirectComponent implements OnInit {
defaultSecs = 5;
secs = -1;
constructor( constructor(
private sidebar: SidebarService, private sidebar: SidebarService,
private router: Router, private router: Router,
@ -41,7 +44,19 @@ export class RedirectComponent implements OnInit {
return; return;
} }
if (!response.loadingScreen) {
window.location.href = `${environment.api.url}/redirect/${shortUrl}`; window.location.href = `${environment.api.url}/redirect/${shortUrl}`;
return;
}
this.secs = this.defaultSecs;
setInterval(() => {
this.secs--;
if (this.secs === 0) {
window.location.href = `${environment.api.url}/redirect/${shortUrl}`;
}
}, 1000);
}); });
} }
} }

View File

@ -13,6 +13,7 @@ export interface ShortUrl extends DbModel {
export interface ShortUrlDto { export interface ShortUrlDto {
id: number; id: number;
targetUrl: string; targetUrl: string;
loadingScreen: boolean;
} }
export interface ShortUrlCreateInput { export interface ShortUrlCreateInput {