Compare commits
No commits in common. "021152bcf592ac3d9f8eb00d69eadb288cf6c75f" and "ff341ddcb5c22120959b677945733877c7d3749b" have entirely different histories.
021152bcf5
...
ff341ddcb5
@ -31,31 +31,6 @@ jobs:
|
||||
run: |
|
||||
docker push git.sh-edraft.de/sh-edraft.de/open-redirect-api-dev:$(cat version.txt)
|
||||
|
||||
build-redirector:
|
||||
runs-on: [runner]
|
||||
container: git.sh-edraft.de/sh-edraft.de/act-runner:latest
|
||||
steps:
|
||||
- name: Clone Repository
|
||||
uses: https://github.com/actions/checkout@v3
|
||||
with:
|
||||
token: ${{ secrets.CI_ACCESS_TOKEN }}
|
||||
|
||||
- name: Build docker
|
||||
run: |
|
||||
cd api
|
||||
docker build -f dockerfile_redirector -t git.sh-edraft.de/sh-edraft.de/open-redirect-redirector-dev:$(cat ../version.txt) .
|
||||
|
||||
- name: Login to registry git.sh-edraft.de
|
||||
uses: https://github.com/docker/login-action@v1
|
||||
with:
|
||||
registry: git.sh-edraft.de
|
||||
username: ${{ secrets.CI_USERNAME }}
|
||||
password: ${{ secrets.CI_ACCESS_TOKEN }}
|
||||
|
||||
- name: Push image
|
||||
run: |
|
||||
docker push git.sh-edraft.de/sh-edraft.de/open-redirect-redirector-dev:$(cat version.txt)
|
||||
|
||||
build-web:
|
||||
runs-on: [runner]
|
||||
container: git.sh-edraft.de/sh-edraft.de/act-runner:latest
|
||||
@ -78,7 +53,7 @@ jobs:
|
||||
- name: Build docker
|
||||
run: |
|
||||
cd web
|
||||
docker build -t git.sh-edraft.de/sh-edraft.de/open-redirect-web-dev:$(cat ../version.txt) .
|
||||
docker build -t git.sh-edraft.de/sh-edraft.de/open-redirect-web:$(cat ../version.txt) .
|
||||
|
||||
- name: Login to registry git.sh-edraft.de
|
||||
uses: https://github.com/docker/login-action@v1
|
||||
@ -89,4 +64,4 @@ jobs:
|
||||
|
||||
- name: Push image
|
||||
run: |
|
||||
docker push git.sh-edraft.de/sh-edraft.de/open-redirect-web-dev:$(cat version.txt)
|
||||
docker push git.sh-edraft.de/sh-edraft.de/open-redirect-web:$(cat version.txt)
|
||||
|
@ -3,12 +3,6 @@ FROM python:3.12.8-alpine
|
||||
|
||||
WORKDIR /app
|
||||
COPY ./src/ .
|
||||
|
||||
RUN rm redirector
|
||||
RUN rm redirector.py
|
||||
RUN rm -r static
|
||||
RUN rm -r templates
|
||||
|
||||
COPY ./requirements.txt .
|
||||
|
||||
RUN python -m pip install --upgrade pip
|
||||
|
@ -1,22 +0,0 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM python:3.12.8-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./src/core/ ./core
|
||||
COPY ./src/data/ ./data
|
||||
COPY ./src/static/ ./static
|
||||
COPY ./src/templates/ ./templates
|
||||
COPY ./src/redirector.py .
|
||||
COPY ./src/redirector .
|
||||
|
||||
COPY ./requirements.txt .
|
||||
|
||||
RUN python -m pip install --upgrade pip
|
||||
RUN python -m pip install -r requirements.txt
|
||||
|
||||
RUN apk update
|
||||
RUN apk add bash
|
||||
RUN apk add nano
|
||||
|
||||
CMD [ "bash", "/app/redirector"]
|
49
api/src/api/routes/redirect.py
Normal file
49
api/src/api/routes/redirect.py
Normal file
@ -0,0 +1,49 @@
|
||||
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/<path:path>")
|
||||
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/<path:path>")
|
||||
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)
|
@ -1,9 +1,4 @@
|
||||
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:
|
||||
@ -19,30 +14,3 @@ 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()
|
||||
|
@ -1,16 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [[ $1 == "-dev" ]]; then
|
||||
export ENVIRONMENT=development
|
||||
export NAME=Open-redirect-dev
|
||||
elif [[ $1 == "-stage" ]]; then
|
||||
export ENVIRONMENT=staging
|
||||
export NAME=Open-redirect-test
|
||||
elif [[ $1 == "-prod" ]]; then
|
||||
export ENVIRONMENT=production
|
||||
export NAME=Open-redirect
|
||||
fi
|
||||
|
||||
export PYTHONPATH=./:$PYTHONPATH
|
||||
|
||||
python3.12 redirector.py
|
@ -1,120 +0,0 @@
|
||||
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("/")
|
||||
def index():
|
||||
return render_template("404.html"), 404
|
||||
|
||||
@app.route("/<path:path>")
|
||||
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
|
||||
# ___// | /
|
||||
# --' \_~-,
|
@ -19,6 +19,30 @@ 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():
|
||||
@ -45,7 +69,7 @@ class Startup:
|
||||
|
||||
app.debug = Environment.get_environment() == "development"
|
||||
|
||||
await Database.startup_db()
|
||||
await cls._startup_db()
|
||||
await FileService.clean_files()
|
||||
|
||||
await cls._seed_data()
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 354 KiB |
Binary file not shown.
Before Width: | Height: | Size: 142 KiB |
Binary file not shown.
Before Width: | Height: | Size: 4.6 MiB |
@ -1,27 +0,0 @@
|
||||
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;
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>404 - Not found</title>
|
||||
<link rel="stylesheet" href="/static/styles.css">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="w-full h-full flex flex-col justify-center items-center">
|
||||
<div class="bg-2 tile">
|
||||
<h1 class="flex justify-center items-center">404 - Not found</h1>
|
||||
<img src="/static/not_found.gif" alt="">
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -1,39 +0,0 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Redirecting {{ key }}</title>
|
||||
<link rel="stylesheet" href="/static/styles.css">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="w-full h-full flex flex-col justify-center items-center">
|
||||
<div class="flex items-center justify-center">
|
||||
<div class="relative w-screen h-screen bg-cover bg-center"
|
||||
style="background-image: url('/static/custom/background.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="/static/custom/logo.png" alt="logo">
|
||||
</div>
|
||||
|
||||
<h1 class="relative text-3xl text-white">Redirecting...</h1>
|
||||
<p class="relative text-white">You will be redirected in <span id="countdown">5</span> seconds.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
let countdown = 5;
|
||||
const countdownElement = document.getElementById('countdown');
|
||||
const interval = setInterval(() => {
|
||||
countdown--;
|
||||
countdownElement.textContent = countdown;
|
||||
if (countdown <= 0) {
|
||||
clearInterval(interval);
|
||||
window.location.href = '{{ target_url }}';
|
||||
}
|
||||
}, 1000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -1 +1 @@
|
||||
1.0.0
|
||||
0.2.1
|
@ -1,22 +1,23 @@
|
||||
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 { 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";
|
||||
|
||||
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: '**', redirectTo: '/404', pathMatch: 'full' },
|
||||
{ path: "404", component: NotFoundComponent },
|
||||
{ path: "**", component: RedirectComponent },
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
@ -1,27 +1,28 @@
|
||||
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 { 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 { RedirectComponent } from "./components/redirect/redirect.component";
|
||||
import { SettingsService } from "src/app/service/settings.service";
|
||||
|
||||
if (environment.production) {
|
||||
Logger.enableProductionMode();
|
||||
@ -35,7 +36,7 @@ export function HttpLoaderFactory(http: HttpClient) {
|
||||
|
||||
export function appInitializerFactory(
|
||||
keycloak: KeycloakService,
|
||||
settings: SettingsService
|
||||
settings: SettingsService,
|
||||
): () => Promise<void> {
|
||||
return (): Promise<void> =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
@ -43,7 +44,7 @@ export function appInitializerFactory(
|
||||
.loadSettings()
|
||||
.then(() => initializeKeycloak(keycloak, settings))
|
||||
.then(() => resolve())
|
||||
.catch(error => reject(error));
|
||||
.catch((error) => reject(error));
|
||||
});
|
||||
}
|
||||
|
||||
@ -56,6 +57,7 @@ export function appInitializerFactory(
|
||||
SpinnerComponent,
|
||||
SidebarComponent,
|
||||
HomeComponent,
|
||||
RedirectComponent,
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
@ -63,7 +65,7 @@ export function appInitializerFactory(
|
||||
AppRoutingModule,
|
||||
SharedModule,
|
||||
TranslateModule.forRoot({
|
||||
defaultLanguage: 'en',
|
||||
defaultLanguage: "en",
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useFactory: HttpLoaderFactory,
|
||||
|
16
web/src/app/components/redirect/redirect.component.html
Normal file
16
web/src/app/components/redirect/redirect.component.html
Normal file
@ -0,0 +1,16 @@
|
||||
<div class="flex items-center justify-center">
|
||||
<div class="relative w-screen h-screen bg-cover bg-center"
|
||||
style="background-image: url({{settings.loadingScreen.background}})"></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]="settings.loadingScreen.logo"
|
||||
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>
|
5
web/src/app/components/redirect/redirect.component.scss
Normal file
5
web/src/app/components/redirect/redirect.component.scss
Normal file
@ -0,0 +1,5 @@
|
||||
.bg-image {
|
||||
background-position: top center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
}
|
22
web/src/app/components/redirect/redirect.component.spec.ts
Normal file
22
web/src/app/components/redirect/redirect.component.spec.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
|
||||
import { RedirectComponent } from "./redirect.component";
|
||||
|
||||
describe("RedirectComponent", () => {
|
||||
let component: RedirectComponent;
|
||||
let fixture: ComponentFixture<RedirectComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [RedirectComponent],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(RedirectComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("should create", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
69
web/src/app/components/redirect/redirect.component.ts
Normal file
69
web/src/app/components/redirect/redirect.component.ts
Normal file
@ -0,0 +1,69 @@
|
||||
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<ShortUrlDto | undefined>(`${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);
|
||||
});
|
||||
}
|
||||
}
|
@ -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,5 +23,4 @@ export interface KeycloakSettings {
|
||||
|
||||
export interface ApiSettings {
|
||||
url: string;
|
||||
redirector: string;
|
||||
}
|
||||
|
@ -1,34 +1,28 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { SharedModule } from 'src/app/modules/shared/shared.module';
|
||||
import { PermissionGuard } from 'src/app/core/guard/permission.guard';
|
||||
import { PermissionsEnum } from 'src/app/model/auth/permissionsEnum';
|
||||
import { NgModule } from "@angular/core";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { RouterModule, Routes } from "@angular/router";
|
||||
import { SharedModule } from "src/app/modules/shared/shared.module";
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: 'groups',
|
||||
path: "groups",
|
||||
loadChildren: () =>
|
||||
import('src/app/modules/admin/groups/groups.module').then(
|
||||
m => m.GroupsModule
|
||||
import("src/app/modules/admin/groups/groups.module").then(
|
||||
(m) => m.GroupsModule,
|
||||
),
|
||||
canActivate: [PermissionGuard],
|
||||
data: { permissions: [PermissionsEnum.groups] },
|
||||
},
|
||||
{
|
||||
path: 'urls',
|
||||
path: "urls",
|
||||
loadChildren: () =>
|
||||
import('src/app/modules/admin/short-urls/short-urls.module').then(
|
||||
m => m.ShortUrlsModule
|
||||
import("src/app/modules/admin/short-urls/short-urls.module").then(
|
||||
(m) => m.ShortUrlsModule,
|
||||
),
|
||||
canActivate: [PermissionGuard],
|
||||
data: { permissions: [PermissionsEnum.shortUrls] },
|
||||
},
|
||||
{
|
||||
path: 'administration',
|
||||
path: "administration",
|
||||
loadChildren: () =>
|
||||
import('src/app/modules/admin/administration/administration.module').then(
|
||||
m => m.AdministrationModule
|
||||
import("src/app/modules/admin/administration/administration.module").then(
|
||||
(m) => m.AdministrationModule,
|
||||
),
|
||||
},
|
||||
];
|
||||
|
@ -1,7 +1,7 @@
|
||||
<router-outlet></router-outlet>
|
||||
|
||||
<ng-template #shortUrl let-url>
|
||||
<div class="flex flex-col gap-2 bg rounded-lg p-3 w-full max-w-lg justify-between">
|
||||
<div class="flex flex-col gap-2 bg rounded-l p-3 w-full max-w-lg justify-between">
|
||||
<div class="flex flex-col gap-2">
|
||||
<h3>{{ url.shortUrl }}</h3>
|
||||
<div class="grid-container">
|
||||
|
@ -8,7 +8,6 @@ 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',
|
||||
@ -44,8 +43,7 @@ export class ShortUrlsPage
|
||||
constructor(
|
||||
private toast: ToastService,
|
||||
private confirmation: ConfirmationDialogService,
|
||||
private auth: AuthService,
|
||||
private settings: SettingsService
|
||||
private auth: AuthService
|
||||
) {
|
||||
super(true, {
|
||||
read: [PermissionsEnum.shortUrls],
|
||||
@ -139,15 +137,13 @@ export class ShortUrlsPage
|
||||
}
|
||||
|
||||
open(url: string) {
|
||||
window.open(`${this.settings.settings.api.redirector}/${url}`, '_blank');
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
|
||||
copy(val: string) {
|
||||
navigator.clipboard
|
||||
.writeText(`${this.settings.settings.api.redirector}/${val}`)
|
||||
.then(() => {
|
||||
this.toast.info('common.copied', 'common.copied_to_clipboard');
|
||||
});
|
||||
navigator.clipboard.writeText(`${window.origin}/${val}`).then(() => {
|
||||
this.toast.info('common.copied', 'common.copied_to_clipboard');
|
||||
});
|
||||
}
|
||||
|
||||
getShortUrlsWithoutGroup(): ShortUrl[] {
|
||||
|
@ -1,18 +1,18 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { MenuElement } from 'src/app/model/view/menu-element';
|
||||
import { AuthService } from 'src/app/service/auth.service';
|
||||
import { PermissionsEnum } from 'src/app/model/auth/permissionsEnum';
|
||||
import { Injectable } from "@angular/core";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { MenuElement } from "src/app/model/view/menu-element";
|
||||
import { AuthService } from "src/app/service/auth.service";
|
||||
import { PermissionsEnum } from "src/app/model/auth/permissionsEnum";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
providedIn: "root",
|
||||
})
|
||||
export class SidebarService {
|
||||
visible$ = new BehaviorSubject<boolean>(true);
|
||||
elements$ = new BehaviorSubject<MenuElement[]>([]);
|
||||
|
||||
constructor(private auth: AuthService) {
|
||||
this.auth.user$.subscribe(user => {
|
||||
this.auth.user$.subscribe((user) => {
|
||||
if (user) {
|
||||
this.setElements().then();
|
||||
}
|
||||
@ -31,18 +31,14 @@ export class SidebarService {
|
||||
async setElements() {
|
||||
const elements: MenuElement[] = [
|
||||
{
|
||||
label: 'common.groups',
|
||||
icon: 'pi pi-tags',
|
||||
routerLink: ['/admin/groups'],
|
||||
visible: await this.auth.hasAnyPermissionLazy([PermissionsEnum.groups]),
|
||||
label: "common.groups",
|
||||
icon: "pi pi-tags",
|
||||
routerLink: ["/admin/groups"],
|
||||
},
|
||||
{
|
||||
label: 'common.urls',
|
||||
icon: 'pi pi-tag',
|
||||
routerLink: ['/admin/urls'],
|
||||
visible: await this.auth.hasAnyPermissionLazy([
|
||||
PermissionsEnum.shortUrls,
|
||||
]),
|
||||
label: "common.urls",
|
||||
icon: "pi pi-tag",
|
||||
routerLink: ["/admin/urls"],
|
||||
},
|
||||
await this.groupAdministration(),
|
||||
];
|
||||
@ -51,35 +47,30 @@ export class SidebarService {
|
||||
|
||||
async groupAdministration() {
|
||||
return {
|
||||
label: 'sidebar.administration',
|
||||
icon: 'pi pi-wrench',
|
||||
label: "sidebar.administration",
|
||||
icon: "pi pi-wrench",
|
||||
expanded: true,
|
||||
visible: await this.auth.hasAnyPermissionLazy([
|
||||
PermissionsEnum.users,
|
||||
PermissionsEnum.roles,
|
||||
PermissionsEnum.apiKeys,
|
||||
]),
|
||||
items: [
|
||||
{
|
||||
label: 'sidebar.users',
|
||||
icon: 'pi pi-user',
|
||||
routerLink: ['/admin/administration/users'],
|
||||
label: "sidebar.users",
|
||||
icon: "pi pi-user",
|
||||
routerLink: ["/admin/administration/users"],
|
||||
visible: await this.auth.hasAnyPermissionLazy([
|
||||
PermissionsEnum.users,
|
||||
]),
|
||||
},
|
||||
{
|
||||
label: 'sidebar.roles',
|
||||
icon: 'pi pi-user-edit',
|
||||
routerLink: ['/admin/administration/roles'],
|
||||
label: "sidebar.roles",
|
||||
icon: "pi pi-user-edit",
|
||||
routerLink: ["/admin/administration/roles"],
|
||||
visible: await this.auth.hasAnyPermissionLazy([
|
||||
PermissionsEnum.roles,
|
||||
]),
|
||||
},
|
||||
{
|
||||
label: 'sidebar.api_keys',
|
||||
icon: 'pi pi-key',
|
||||
routerLink: ['/admin/administration/api-keys'],
|
||||
label: "sidebar.api_keys",
|
||||
icon: "pi pi-key",
|
||||
routerLink: ["/admin/administration/api-keys"],
|
||||
visible: await this.auth.hasAnyPermissionLazy([
|
||||
PermissionsEnum.apiKeys,
|
||||
]),
|
||||
|
@ -9,12 +9,15 @@
|
||||
}
|
||||
],
|
||||
"api": {
|
||||
"url": "",
|
||||
"redirector": ""
|
||||
"url": ""
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
@ -138,8 +138,6 @@
|
||||
},
|
||||
"user": {
|
||||
"count_header": "Benutzer",
|
||||
"email": "E-Mail",
|
||||
"user": "Benutzer",
|
||||
"username": "Benutzername"
|
||||
"user": "Benutzer"
|
||||
}
|
||||
}
|
@ -138,8 +138,6 @@
|
||||
},
|
||||
"user": {
|
||||
"count_header": "User(s)",
|
||||
"email": "E-Mail",
|
||||
"user": "User",
|
||||
"username": "Username"
|
||||
"user": "User"
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user