Update Authentication + Build Script

This commit is contained in:
Moritz Utcke
2023-05-18 14:01:54 +04:00
parent 5559f5ca4d
commit 492b790527
22 changed files with 276 additions and 163 deletions

View File

@@ -3,8 +3,6 @@ name: Node.js CD
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
@@ -19,9 +17,10 @@ jobs:
password: "!2Zc727cI1"
port: 22
script: |
cd /home/root/apps/online-energieausweis
cd ~/apps/online-energiausweis
git reset --hard HEAD
git clean -f -d
git pull origin main
git status
pnpm install --prod
pnpm run build
pm2 restart online-energieausweis
pnpm install
bash build.sh

View File

@@ -1,24 +0,0 @@
# This workflow will do a clean install of node dependencies,
# build the source code and run tests across different versions of node
# For more information see:
# https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Node.js CI
on:
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js 12
uses: actions/setup-node@v2
with:
node-version: 19.x
- run: npm i
- run: npm run build --if-present
- run: npm test

View File

@@ -1,18 +0,0 @@
name: Docker Image CI
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build the Docker image
run: docker build . --file Dockerfile --tag online-energieausweis:$(date +%s)

37
build.sh Normal file
View File

@@ -0,0 +1,37 @@
#!/bin/bash
# Define your environment variables here
APP_NAME="online-energiausweis"
APP_PORT=3000
DB_CONTAINER_NAME="database"
DB_NAME="main"
DB_USER="main"
DB_PASSWORD="hHMP8cd^N3SnzGRR"
DB_PORT=5432
DB_VOLUME="postgres_data"
git_pull_force() {
git reset --hard HEAD
git clean -f -d
git pull origin main
}
# Zuerst müssen wir neue Änderungen von GitHub pullen.
cd ~/apps/$APP_NAME
git_pull_force;
# Dann bauen wir das Docker Image unserer Application
cd ~/apps/$APP_NAME
docker stop $APP_NAME
docker rm $APP_NAME
docker build -t $APP_NAME .
# Danach machen wir ein Backup der Datenbank, falls bei der Migration etwas schiefgehen sollte.
cd ~/backups/
BACKUP_FILENAME="$(date +"%Y-%m-%d_%H-%M-%S").sql.gz"
docker exec -t $DB_CONTAINER_NAME pg_dumpall -c -U $DB_USER | gzip > $BACKUP_FILENAME
# Und starten unsere App wieder.
docker run -d --name $APP_NAME --link $DB_CONTAINER_NAME -p "$APP_PORT:80" -e DB_CONNECTION=postgresql://$DB_USER:$DB_PASSWORD@${DB_CONTAINER_NAME}:$DB_PORT/$DB_NAME -e DB_PORT=$DB_PORT $APP_NAME
# Das Backup lassen wir da, falls irgendwas schief gehen sollte.

View File

@@ -3,16 +3,16 @@
import { Gebaeude } from "src/lib/Gebaeude";
import HelpLabel from "~/components/HelpLabel.svelte";
import { auditHeizungGebaeudeBaujahr } from "../Verbrauchsausweis/audits/HeizungGebaeudeBaujahr";
import { addNotification, deleteNotification } from "../Notifications/shared";
import { addNotification, deleteNotification } from "@ibcornelsen/ui";
import TagInput from "../TagInput.svelte";
import { writable } from "svelte/store";
export let gebaeude: Gebaeude;
// TODO: Das ist scheise
let tags = writable([]);
$: ausweis = gebaeude.ausweis || new Verbrauchsausweis();
let baujahr = writable(gebaeude.baujahr);
let baujahrAnlage = writable(gebaeude.ausweis.baujahr_anlage);
</script>
<div class="GRB">
@@ -96,9 +96,7 @@
deleteNotification("HEIZUNG_BAUJAHR")
}}
className="{auditHeizungGebaeudeBaujahr(gebaeude) ? "linked" : ""}"
required
autocomplete="off"
bind:tags
bind:tags={baujahrAnlage}
/>
</div>
</div>
@@ -133,9 +131,7 @@
deleteNotification("GEBAEUDE_BAUJAHR")
}}
className="{auditHeizungGebaeudeBaujahr(gebaeude) ? "linked" : ""}"
required
autocomplete="off"
bind:tags
bind:tags={baujahr}
/>
</div>
</div>

View File

@@ -13,6 +13,7 @@
/>
<div class="tooltip">
<slot></slot>
<div id="arrow" class="invisible absolute h-3 w-3 bg-inherit before:visible before:absolute before:h-3 before:w-3 right-2 before:rotate-45 before:bg-inherit before:content-['']"></div>
</div>
</div>
</div>
@@ -23,7 +24,7 @@
}
.tooltip {
@apply absolute left-0 translate-x-[-50%] max-w-[350px] w-max break-words invisible bg-white rounded-lg p-2 shadow-lg top-0 translate-y-[calc(-100%-8px)] transition-all duration-300 opacity-0;
@apply absolute -right-1 max-w-[350px] w-max break-words invisible bg-white rounded-lg p-2 shadow-lg top-0 translate-y-[calc(-100%-8px)] transition-all duration-300 opacity-0;
}
.tooltip-opener:hover .tooltip {

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import cookie from "cookiejs";
import { addNotification } from "./Notifications/shared";
import { addNotification } from "@ibcornelsen/ui";
let email: string;
let password: string;

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { addNotification } from "./Notifications/shared";
import { addNotification } from "@ibcornelsen/ui";
let passwort: string;
let email: string;

View File

@@ -3,9 +3,9 @@
let tag = "";
export let tags: Writable<string[]> = writable([]);
export let addKeys: number[] = [13];
export let maxTags: number;
export let tags: Writable<any[]> = writable([]);
export let addKeys: number[] = [13, 32];
export let maxTags: number = Infinity;
export let onlyUnique: boolean = false;
export let removeKeys: number[] = [8];
export let placeholder: string = "";
@@ -13,26 +13,26 @@
export let allowDrop: boolean = false;
export let splitWith: string = ",";
export let name: string = "";
export let id: string = "";
export let allowBlur: boolean = true;
export let disable: boolean = false;
export let minChars: number = 0;
export let labelText;
export let labelShow;
export let readonly: boolean = false;
export let onTagClick: Function;
export let onFocusIn: () => any;
export let onFocusOut: () => any;
export let className: string;
export let onTagClick: Function = ()=>{};
export let onFocusIn: () => any = () => {};
export let onFocusOut: () => any = () => {};
export let className: string = "";
function addTag(tag: string) {
if (onlyUnique && $tags.indexOf(tag) > -1) {
if ((onlyUnique && $tags.indexOf(tag) > -1) || maxTags == $tags.length) {
tag = "";
return;
}
if (minChars > tag.length) {
return;
}
tags.update(value => {
return [...value, tag];
return [...value, tag].sort((a,b) => a-b);
})
}
@@ -82,7 +82,8 @@
on:focusin={onFocusIn}
on:focusout={onFocusOut}
class="border-none h-full w-full {className}"
disabled={disable || readonly}
disabled={disable}
readonly={readonly}
autocomplete="off"
{...$$restProps}
/>

View File

@@ -11,10 +11,8 @@
import moment from "moment";
import BilderZusatzsysteme from "../Ausweis/BilderZusatzsysteme.svelte";
import { Gebaeude } from "src/lib/Gebaeude";
import { hideLinkedElement, notifications } from "../Notifications/shared";
import { gebaeude } from "./shared";
import RawNotificationWrapper from "../Notifications/RawNotificationWrapper.svelte";
import RawNotification from "../Notifications/RawNotification.svelte";
import { RawNotificationWrapper, RawNotification, notifications } from "@ibcornelsen/ui";
import { auditHeizungGebaeudeBaujahr } from "./audits/HeizungGebaeudeBaujahr";
import { AuditType, hidden } from "./audits/hidden";
import { auditBedarfsausweisBenoetigt } from "./audits/BedarfsausweisBenoetigt";
@@ -26,9 +24,9 @@
$gebaeude.ausweis = new Verbrauchsausweis();
$gebaeude.ausweis.gebaeude = $gebaeude;
if (uid) {
(async () => {
async () => {
const result = await fetch(`/api/verbrauchsausweis?uid=${uid}`, {
method: "GET"
method: "GET",
});
const json = await result.json();
@@ -38,14 +36,14 @@
$gebaeude.ausweis = new Verbrauchsausweis(json.data.ausweis);
$gebaeude.ausweis.gebaeude = $gebaeude;
}
})
};
}
$: ausweis = $gebaeude.ausweis || new Verbrauchsausweis();
function automatischAusfüllen() {
$gebaeude.baujahr = 1962;
ausweis.baujahr_anlage = 1952;
$gebaeude.baujahr = [1962];
ausweis.baujahr_anlage = [1952];
$gebaeude.saniert = true;
$gebaeude.einheiten = 1;
ausweis.ausstellgrund = "Vermietung";
@@ -224,7 +222,8 @@
type="checkbox"
class="IGwwbool"
name="IGwwbool"
bind:checked={ausweis.kennwerte.warmwasser_enthalten}
bind:checked={ausweis.kennwerte
.warmwasser_enthalten}
/>Warmwasser im Verbrauch enthalten</label
>
</div>
@@ -451,7 +450,6 @@
</form>
<RawNotificationWrapper>
{#each Object.entries($notifications) as [uid, notification] (uid)}
<RawNotification notification={{ ...notification, uid }}>
{@html notification.subtext}
@@ -459,49 +457,61 @@
{/each}
{#if auditBedarfsausweisBenoetigt($gebaeude)}
<RawNotification notification={{
<RawNotification
notification={{
message: "Bedarfsausweis benötigt!",
timeout: 0,
uid: "BEDARFSAUSWEIS",
dismissable: false,
type: "info"
}}>
Sie benötigen einen Bedarfsausweis. <a href='/bedarfsausweis'>Bitte führen Sie hier Ihre Eingabe für den Bedarfsausweis fort</a>.
type: "info",
}}
>
Sie benötigen einen Bedarfsausweis. <a href="/bedarfsausweis"
>Bitte führen Sie hier Ihre Eingabe für den Bedarfsausweis fort</a
>.
</RawNotification>
{/if}
{#if auditHeizungGebaeudeBaujahr($gebaeude)}
<RawNotification notification={{
<RawNotification
notification={{
message: "Plausibilitätsprüfung",
timeout: 0,
uid: "HEIZUNG_VOR_GEBAEUDE",
dismissable: true,
onUserDismiss: () => {
hidden.add(AuditType.HEIZUNG_GEBAEUDE_BAUJAHR)
hidden.add(AuditType.HEIZUNG_GEBAEUDE_BAUJAHR);
$gebaeude = $gebaeude;
},
type: "warning"
}}>
Sie haben angegeben, dass ihre Heizung vor ihrem Gebäude konstruiert wurde. Sind sie sich sicher, dass das stimmt?
type: "warning",
}}
>
Sie haben angegeben, dass ihre Heizung vor ihrem Gebäude konstruiert
wurde. Sind sie sich sicher, dass das stimmt?
</RawNotification>
{/if}
{#if auditVerbrauchAbweichung($gebaeude).length > 0}
<RawNotification notification={{
<RawNotification
notification={{
message: "Plausibilitätsprüfung",
timeout: 0,
uid: "VERBRAUCH_ABWEICHUNG",
dismissable: true,
onUserDismiss: () => {
hidden.add(AuditType.VERBRAUCH_ABWEICHUNG)
hidden.add(AuditType.VERBRAUCH_ABWEICHUNG);
$gebaeude = $gebaeude;
},
type: "warning"
}}>
Die Abweichung der Verbräuche zwischen Zeitraum {auditVerbrauchAbweichung($gebaeude)[0]} und {auditVerbrauchAbweichung($gebaeude)[1]} beträgt mehr als 25% und sie haben keinen Leerstand angegeben. Sind sie sich sicher, dass das stimmt?
type: "warning",
}}
>
Die Abweichung der Verbräuche zwischen Zeitraum {auditVerbrauchAbweichung(
$gebaeude
)[0]} und {auditVerbrauchAbweichung($gebaeude)[1]} beträgt mehr als 25%
und sie haben keinen Leerstand angegeben. Sind sie sich sicher, dass
das stimmt?
</RawNotification>
{/if}
</RawNotificationWrapper>
<style>

View File

@@ -7,8 +7,8 @@ export function auditHeizungGebaeudeBaujahr(gebaeude: Gebaeude): boolean {
return false;
}
return ausweis.baujahr_anlage > 1500 &&
gebaeude.baujahr > 1500 &&
ausweis.baujahr_anlage < gebaeude.baujahr
return ausweis.baujahr_anlage[0] > 1500 &&
gebaeude.baujahr[0] > 1500 &&
ausweis.baujahr_anlage[0] < gebaeude.baujahr[0]
&& !hidden.has(AuditType.HEIZUNG_GEBAEUDE_BAUJAHR)
}

View File

@@ -13,7 +13,10 @@
async function fetchZipCodeInformation() {
const result = await fetch(`/api/zip?zip=${zip}`, {
method: "GET"
method: "GET",
headers: {
"authorization": "Basic " + btoa("user@ib-cornelsen.de:test")
}
}).then(r => r.json());
if (result.success === true) {

View File

@@ -3,7 +3,6 @@ import "../style/global.scss"
import Footer from '../components/Footer.astro';
import Header from '../components/Header.astro';
import SidebarLeft from '../components/SidebarLeft.astro';
import NotificationWrapper from "~/components/Notifications/NotificationWrapper.svelte";
export interface Props {
title: string;

View File

@@ -1,10 +1,10 @@
---
import "../style/global.scss"
import Footer from '../components/Footer.astro';
import Header from '../components/Header.astro';
import SidebarLeft from '../components/SidebarLeft.astro';
import SidebarRight from '../components/SidebarRight.astro';
import NotificationWrapper from "~/components/Notifications/NotificationWrapper.svelte";
import "../style/global.scss";
import Footer from "../components/Footer.astro";
import Header from "../components/Header.astro";
import SidebarLeft from "../components/SidebarLeft.astro";
import SidebarRight from "../components/SidebarRight.astro";
import { NotificationWrapper } from "@ibcornelsen/ui";
export interface Props {
title: string;
@@ -13,27 +13,27 @@ export interface Props {
const { title } = Astro.props;
const schema = JSON.stringify({
'@context': 'http://schema.org',
'@type': 'Corporation',
name: 'IB Cornelsen',
alternateName: 'online-energieausweis.org',
url: 'https://online-energieausweis.org',
logo: 'https://online-energieausweis.org/ib-cornelsen.png',
"@context": "http://schema.org",
"@type": "Corporation",
name: "IB Cornelsen",
alternateName: "online-energieausweis.org",
url: "https://online-energieausweis.org",
logo: "https://online-energieausweis.org/ib-cornelsen.png",
address: {
'@type': 'PostalAddress',
streetAddress: 'Katendeich 5A',
addressLocality: 'Hamburg',
postalCode: '21035',
addressCountry: 'Deutschland',
email: 'info@online-energieausweis.org',
"@type": "PostalAddress",
streetAddress: "Katendeich 5A",
addressLocality: "Hamburg",
postalCode: "21035",
addressCountry: "Deutschland",
email: "info@online-energieausweis.org",
},
contactPoint: {
'@type': 'ContactPoint',
telephone: '+49-040-209339850',
faxNumber: '+49-040-209339859',
contactType: 'customer service',
areaServed: 'DE',
availableLanguage: 'German',
"@type": "ContactPoint",
telephone: "+49-040-209339850",
faxNumber: "+49-040-209339859",
contactType: "customer service",
areaServed: "DE",
availableLanguage: "German",
},
});
---
@@ -65,7 +65,10 @@ const schema = JSON.stringify({
content="✅ Jetzt Ihren Energieausweis online erstellen. Erhalten Sie Ihren online Energieausweis rechtssicher und nach aktueller GEG (vormals EnEV) vom Diplom Ingenieur geprüft."
/>
<meta property="og:url" content="https://online-energieausweis.org/" />
<meta property="og:site_name" content="Energieausweis online erstellen" />
<meta
property="og:site_name"
content="Energieausweis online erstellen"
/>
<meta name="twitter:card" content="summary_large_image" />
<meta
@@ -81,21 +84,23 @@ const schema = JSON.stringify({
content="https://online-energieausweis.org/images/energieausweis-online-erstellen.jpg"
/>
<title>
{title || 'Energieausweis online erstellen - Online Energieausweis'}
{title || "Energieausweis online erstellen - Online Energieausweis"}
</title>
</head>
<body>
<Header />
<main class="grid gap-6 p-6 grid-cols-[2fr,6fr,2fr] max-w-[1920px] w-full">
<SidebarLeft></SidebarLeft>
<main
class="grid gap-6 p-6 grid-cols-[2fr,6fr,2fr] max-w-[1920px] w-full"
>
<SidebarLeft />
<article class="mainContent">
<slot />
</article>
<SidebarRight></SidebarRight>
<SidebarRight />
</main>
<Footer />
<NotificationWrapper client:load></NotificationWrapper>
<NotificationWrapper client:load />
</body>
</html>
@@ -117,7 +122,8 @@ const schema = JSON.stringify({
@apply py-1.5 px-4 w-full rounded-lg outline-none text-lg text-slate-700 border bg-gray-50 transition-colors;
}
input:hover, input:focus {
input:hover,
input:focus {
@apply bg-gray-100;
}

View File

@@ -20,7 +20,7 @@ export class Bedarfsausweis {
public objekt_gebaeudeteil: string = "";
public objekt_saniert: boolean = false;
public baujahr_gebaeude: number = 0;
public baujahr_anlage: number = 0;
public baujahr_anlage: number[] = [];
public anzahl_einheiten: number = 0;
public erstellungsdatum: Date = new Date();

View File

@@ -21,7 +21,7 @@ export class VerbrauchsausweisGewerbe {
public objekt_gebaeudeteil: string = "";
public objekt_saniert: boolean = false;
public baujahr_gebaeude: number = 0;
public baujahr_anlage: number = 0;
public baujahr_anlage: number[] = [];
public anzahl_einheiten: number = 0;
public erstellungsdatum: Date = new Date();

View File

@@ -1,6 +1,5 @@
import { Ausweis } from "./Ausweis/Ausweis";
import { Verbrauchsausweis } from "./Ausweis/Verbrauchsausweis";
import { Dachgeschoss, Lueftungskonzept } from "./Ausweis/types";
import { BitChecker } from "./BitChecker";
@@ -11,7 +10,7 @@ export class Gebaeude {
public strasse: string = "";
public gebaeudeteil: string = "";
public saniert: boolean = false;
public baujahr: number = 0;
public baujahr: number[] = [];
public einheiten: number = 0;
public wohnflaeche: number = 0;
public keller_beheizt: boolean = false;

View File

@@ -5,6 +5,6 @@ export function encodeToken(data: Record<string, any>) {
return token;
}
export function decodeToken(token: string) {
export function decodeToken<T>(token: string): Partial<T> {
return jwt.decode(token, "yIvbgS$k7Bfc+mpV%TWDZAhje9#uJad4");
}

View File

@@ -0,0 +1 @@
export { validateAuthorizationHeader } from "./validate";

View File

@@ -0,0 +1,62 @@
import { decodeToken } from "src/lib/JsonWebToken";
import { validatePassword } from "src/lib/Password";
import { User } from "src/lib/User";
export async function validateAuthorizationHeader(
request: Request,
validAuthenticationMethods: ("Bearer" | "Basic")[]
): Promise<User | null> {
if (!request.headers.has("authorization")) {
return null;
}
const header = request.headers.get("authorization") as string;
const [authorizationType, value] = header.split(" ");
if (authorizationType == "Basic" && validAuthenticationMethods.indexOf("Basic") > -1) {
// Basic user validation;
try {
const [email, password] = Buffer.from(value, "base64")
.toString()
.split(":");
const user = await User.fromEmail(email);
if (!user) {
return null;
}
if (!validatePassword(user.passwort, password)) {
return null;
}
return user;
} catch (e) {
return null;
}
} else if (authorizationType == "Bearer" && validAuthenticationMethods.indexOf("Bearer") > -1) {
const stringToken = Buffer.from(value, "base64").toString();
try {
const token = decodeToken<{ id: number; uid: string; exp: string }>(
stringToken
);
const id = token.id;
if (!id) {
return null;
}
const user = User.fromPrivateId(id);
if (!user) {
return null;
}
return user;
} catch (e) {
return null;
}
} else {
return null;
}
}

34
src/pages/api/token.ts Normal file
View File

@@ -0,0 +1,34 @@
import type { APIRoute } from "astro";
import { success, MissingPropertyError, error } from "../../lib/APIResponse";
import { validatePassword, hashPassword } from "../../lib/Password";
import { User } from "../../lib/User";
import moment from "moment";
import { encodeToken } from "../../lib/JsonWebToken";
/**
* Ruft einen Nutzer anhand seiner uid aus der Datenbank ab.
* @param param0 Die Request mit dem request body. Dieser enthält entweder eine uid mit der der Benutzer identifiziert werden kann.
*/
export const post: APIRoute = async ({ request }) => {
const body = await request.json();
if (!body.hasOwnProperty("email") || !body.hasOwnProperty("password")) {
return MissingPropertyError(["email", "password"]);
}
const user = await User.fromEmail(body.email);
if (!user) {
return error(["Invalid email or password."]);
}
// Validate Password
if (!validatePassword(user.passwort, body.password)) {
return error(["Invalid email or password."]);
}
const expiry = moment().add(2, "days").unix();
const token = encodeToken({ id: user.id, uid: user.uid, exp: expiry })
return success({ token, expires: expiry });
}

View File

@@ -1,12 +1,19 @@
import type { APIRoute } from "astro";
import { success, MissingPropertyError, MissingEntityError, InvalidDataError } from "../../lib/APIResponse";
import { success, MissingPropertyError, MissingEntityError, InvalidDataError, error } from "../../lib/APIResponse";
import { ZIPInformation } from "src/lib/ZIPInformation";
import { validateAuthorizationHeader } from "src/lib/server/Authorization";
/**
* Ruft einen Nutzer anhand seiner uid aus der Datenbank ab.
* @param param0 Die Request mit dem request body. Dieser enthält entweder eine uid mit der der Benutzer identifiziert werden kann.
*/
export const get: APIRoute = async ({ request }) => {
const user = await validateAuthorizationHeader(request, ["Bearer", "Basic"]);
if (!user) {
return error(["Invalid authentication credentials!"]);
}
const body = Object.fromEntries(new URLSearchParams(request.url.split("?")[1]))
let result;