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
from flask import redirect, jsonify, request, Response
from api.route import Route
from core.logger import Logger
@ -41,4 +39,11 @@ async def handle_short_url(path: str):
except Exception as 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
visits: Int
group: Group
loadingScreen: Boolean
deleted: Boolean
editor: User
@ -22,6 +23,7 @@ input ShortUrlSort {
id: SortOrder
name: SortOrder
description: SortOrder
loadingScreen: SortOrder
deleted: SortOrder
editorId: SortOrder
@ -33,6 +35,7 @@ input ShortUrlFilter {
id: IntFilter
name: StringFilter
description: StringFilter
loadingScreen: BooleanFilter
deleted: BooleanFilter
editor: IntFilter
@ -52,6 +55,7 @@ input ShortUrlCreateInput {
targetUrl: String!
description: String
groupId: ID
loadingScreen: Boolean
}
input ShortUrlUpdateInput {
@ -60,4 +64,5 @@ input ShortUrlUpdateInput {
targetUrl: String
description: String
groupId: ID
loadingScreen: Boolean
}

View File

@ -12,6 +12,7 @@ class ShortUrlCreateInput(InputABC):
self._target_url = self.option("targetUrl", str, required=True)
self._description = self.option("description", str)
self._group_id = self.option("groupId", int)
self._loading_screen = self.option("loadingScreen", str)
@property
def short_url(self) -> str:
@ -28,3 +29,7 @@ class ShortUrlCreateInput(InputABC):
@property
def group_id(self) -> Optional[int]:
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._description = self.option("description", str)
self._group_id = self.option("groupId", int)
self._loading_screen = self.option("loadingScreen", str)
@property
def id(self) -> int:
@ -33,3 +34,7 @@ class ShortUrlUpdateInput(InputABC):
@property
def group_id(self) -> Optional[int]:
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.description,
obj.group_id,
obj.loading_screen,
)
nid = await shortUrlDao.create(short_url)
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")
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)
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("group", lambda x, *_: x.group)
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,
description: Optional[str],
group_id: Optional[SerialId],
loading_screen: Optional[str] = None,
deleted: bool = False,
editor_id: Optional[SerialId] = None,
created: Optional[datetime] = None,
@ -26,6 +27,7 @@ class ShortUrl(DbModelABC):
self._target_url = target_url
self._description = description
self._group_id = group_id
self._loading_screen = loading_screen
@property
def short_url(self) -> str:
@ -74,8 +76,17 @@ class ShortUrl(DbModelABC):
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:
return {
"id": self.id,
"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.description, str)
self.attribute(ShortUrl.group_id, int)
self.attribute(ShortUrl.loading_screen, bool)
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>
<div class="app">
<aside *ngIf="showSidebar">
<aside>
<app-sidebar></app-sidebar>
</aside>
<section class="component">

View File

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

View File

@ -1,5 +1,5 @@
import { Component } from "@angular/core";
import { AuthService } from "src/app/service/auth.service";
import { KeycloakService } from "keycloak-angular";
@Component({
selector: "app-home",
@ -7,11 +7,9 @@ import { AuthService } from "src/app/service/auth.service";
styleUrl: "./home.component.scss",
})
export class HomeComponent {
constructor(private auth: AuthService) {
if (this.auth.user$.value !== undefined) {
return;
constructor(private keycloak: KeycloakService) {
if (!this.keycloak.isLoggedIn()) {
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",
})
export class RedirectComponent implements OnInit {
defaultSecs = 5;
secs = -1;
constructor(
private sidebar: SidebarService,
private router: Router,
@ -41,7 +44,19 @@ export class RedirectComponent implements OnInit {
return;
}
if (!response.loadingScreen) {
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 {
id: number;
targetUrl: string;
loadingScreen: boolean;
}
export interface ShortUrlCreateInput {