Compare commits
2 Commits
4713807f94
...
a4c5b441f6
| Author | SHA1 | Date | |
|---|---|---|---|
| a4c5b441f6 | |||
| 3de0e95569 |
@ -1,7 +1,8 @@
|
||||
import { AuthenticationPage, AuthenticationRules } from "../pages/auth/auth";
|
||||
import { NewsData, NewsFeed } from "../pages/feed/news";
|
||||
import { NewsItem } from "../pages/feed/news-item";
|
||||
|
||||
type EndpointURL = "" | `http${'s' | ''}://${string}`;
|
||||
type EndpointURL = "/api" | `http${'s' | ''}://${string}`;
|
||||
|
||||
interface ErrorDetails {
|
||||
code: string,
|
||||
@ -14,17 +15,27 @@ interface APIResponse<T> {
|
||||
}
|
||||
|
||||
let newsFeed : NewsFeed | null = null;
|
||||
let authPage : AuthenticationPage | null = null;
|
||||
|
||||
class ErrorAPI extends Error {}
|
||||
|
||||
export class ControllerAPI {
|
||||
protected endpoint: EndpointURL;
|
||||
constructor(endpoint: EndpointURL = "") {
|
||||
constructor(endpoint: EndpointURL = "/api") {
|
||||
this.endpoint = endpoint;
|
||||
}
|
||||
|
||||
protected async request<T>(path: string, options: RequestInit = {}): Promise<T> {
|
||||
const url = new URL(path, this.endpoint || window.location.origin).href;
|
||||
let url : string;
|
||||
if (this.endpoint === "/api") {
|
||||
url = new URL("/api", window.location.origin).href + path;
|
||||
}
|
||||
else if (/\/$/i.test(this.endpoint)) {
|
||||
url = this.endpoint + path.slice(1);
|
||||
}
|
||||
else {
|
||||
url = this.endpoint + path;
|
||||
}
|
||||
|
||||
const response = await fetch(url, options);
|
||||
const result: APIResponse<T> = await response.json();
|
||||
@ -50,21 +61,21 @@ export class ControllerAPI {
|
||||
async isAuthenticated(): Promise<boolean> {
|
||||
if (!this.checkFirstAuth()) return false;
|
||||
|
||||
const result = await this.request<boolean>('/api/isAuthenticated', {
|
||||
const result = await this.request<boolean>('/isAuthenticated', {
|
||||
method: 'GET',
|
||||
credentials: 'include' // Include cookies for authentication
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
async getNews(returnElement: boolean = false) : Promise<NewsFeed | NewsData[]> {
|
||||
async getNews(returnPage: boolean = false) : Promise<NewsFeed | NewsData[]> {
|
||||
this.throwIfUnauthed();
|
||||
|
||||
const result = await this.request<NewsData[]>('/api/news', {
|
||||
const result = await this.request<NewsData[]>('/news', {
|
||||
method: 'GET',
|
||||
credentials: 'include' // Include cookies for authentication
|
||||
});
|
||||
if (!returnElement)
|
||||
if (!returnPage)
|
||||
return result;
|
||||
if (!newsFeed)
|
||||
newsFeed = new NewsFeed(result.map(x => new NewsItem(
|
||||
@ -76,4 +87,15 @@ export class ControllerAPI {
|
||||
)));
|
||||
return newsFeed;
|
||||
}
|
||||
|
||||
async getAuthRules(returnPage: boolean = false) : Promise<AuthenticationRules | AuthenticationPage> {
|
||||
const result = await this.request<AuthenticationRules>('/authenticationRules', {
|
||||
method: 'GET',
|
||||
credentials: 'include' // Include cookies for authentication
|
||||
});
|
||||
if (!returnPage) return result;
|
||||
if (!authPage)
|
||||
authPage = new AuthenticationPage(result);
|
||||
return authPage;
|
||||
}
|
||||
}
|
||||
@ -59,3 +59,10 @@
|
||||
.nav-item {
|
||||
cursor: pointer; padding: 12px 24px; font-size: 1.5em;
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
margin-left: 30vw;
|
||||
margin-right: 30vw;
|
||||
margin-top: 30vh;
|
||||
margin-bottom: 30vh;
|
||||
}
|
||||
@ -1,11 +1,15 @@
|
||||
import { GefestEngine } from "./modules/Gefest/engine";
|
||||
import { Navbar } from "./pages/navbar";
|
||||
import { DarkStyle, LightStyle, defaultStyle } from "./styles";
|
||||
import { Navbar, themeToggle } from "./pages/navbar";
|
||||
import { DarkStyle, LightStyle, SupportedStyles } from "./styles";
|
||||
import { ControllerAPI } from "./api/RESTful";
|
||||
import { NewsFeed } from "./pages/feed/news";
|
||||
import { AuthenticationPage } from "./pages/auth/auth";
|
||||
|
||||
// I'll add a dark mode toggle later, but for now let's just use the light style by default
|
||||
let currentStyle = defaultStyle;
|
||||
const defaultStyle = new LightStyle();
|
||||
const styleMemLink = {
|
||||
currentStyle: () : SupportedStyles => defaultStyle
|
||||
};
|
||||
|
||||
{ // I hate trash in global scope, so I wrap it in a block
|
||||
const appElement = document.getElementById("app");
|
||||
@ -19,7 +23,7 @@ const api : ControllerAPI = new ControllerAPI();
|
||||
async function authInit() {
|
||||
console.log("User is authenticated");
|
||||
const newsFeed = await api.getNews(true) as NewsFeed;
|
||||
newsFeed.style = currentStyle;
|
||||
newsFeed.style = styleMemLink.currentStyle;
|
||||
const appElement = document.getElementById("app");
|
||||
if (appElement)
|
||||
appElement.innerHTML = newsFeed.build();
|
||||
@ -27,13 +31,35 @@ async function authInit() {
|
||||
|
||||
async function logInInit () {
|
||||
console.log("User is not authenticated");
|
||||
// Load the auth page
|
||||
const authPage = await api.getAuthRules(true) as AuthenticationPage;
|
||||
authPage.style = styleMemLink.currentStyle;
|
||||
const appElement = document.getElementById("app");
|
||||
if (appElement)
|
||||
appElement.innerHTML = authPage.build();
|
||||
}
|
||||
|
||||
async function changeStyleLogic () {
|
||||
const themes : SupportedStyles[] = [
|
||||
defaultStyle,
|
||||
new DarkStyle()
|
||||
];
|
||||
let choosed = 0;
|
||||
|
||||
themeToggle.onClick = () => {
|
||||
choosed++;
|
||||
if (choosed >= themes.length)
|
||||
choosed = 0;
|
||||
|
||||
styleMemLink.currentStyle = () => themes[choosed] as SupportedStyles;
|
||||
GefestEngine.render();
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await changeStyleLogic();
|
||||
// init navbar
|
||||
const navbar = new Navbar();
|
||||
navbar.style = currentStyle;
|
||||
navbar.style = styleMemLink.currentStyle;
|
||||
document.body.innerHTML = "<div id='app'></div>" + navbar.build();
|
||||
|
||||
if (await api.isAuthenticated()) {
|
||||
|
||||
@ -4,8 +4,9 @@ import { GefestStyle } from './style';
|
||||
// The general class of Gefest
|
||||
type GefestElementEvents = "onClick";
|
||||
export abstract class GefestElement {
|
||||
style: GefestStyle | null = null;
|
||||
style: GefestStyle | (() => GefestStyle) | null = null;
|
||||
isHidden: boolean = false;
|
||||
id : string | null = null;
|
||||
//onClick: (() => void) | null = null;
|
||||
readonly gefestId: string;
|
||||
protected content: (GefestElement | string)[] | string;
|
||||
@ -64,6 +65,8 @@ export abstract class GefestElement {
|
||||
* @returns The created GefestElement.
|
||||
*/
|
||||
static fromHTML(html: string): GefestElement {
|
||||
console.warn("Legacy and experiemental function!");
|
||||
|
||||
const htmlElement = new DOMParser().parseFromString(html, 'text/html').body.firstChild as HTMLElement;
|
||||
const element = new (class extends GefestElement {
|
||||
protected wrapHTML(content: string): string {
|
||||
@ -98,7 +101,7 @@ export abstract class GefestElement {
|
||||
* Call to GefestEngine for rerender element
|
||||
*/
|
||||
update (): void {
|
||||
GefestEngine.reRenderByGI(this.gefestId);
|
||||
GefestEngine.render(this.gefestId);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -112,6 +115,11 @@ export abstract class GefestElement {
|
||||
else
|
||||
this.removeAttribute('hidden');
|
||||
|
||||
if (this.id !== null)
|
||||
this.setAttribute('id', this.id);
|
||||
else
|
||||
this.removeAttribute('id');
|
||||
|
||||
if (!isFromStyle)
|
||||
return this.applyStyle(this.render(), this);
|
||||
return this.render();
|
||||
@ -200,8 +208,11 @@ export abstract class GefestElement {
|
||||
* @returns The styled HTML.
|
||||
*/
|
||||
protected applyStyle(html: string, element?: GefestElement): string {
|
||||
if (this.style)
|
||||
if (this.style) {
|
||||
if (typeof this.style === "function")
|
||||
return this.style().applyStyle(html, element);
|
||||
return this.style.applyStyle(html, element);
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
@ -222,8 +233,9 @@ export abstract class GefestElement {
|
||||
*/
|
||||
protected wrapHTML (content: string): string {
|
||||
const attrString = this.attributesToString();
|
||||
return `<div ${attrString}>${content}</div>`;
|
||||
return `<${this.htmlTag} ${attrString}>${content}</${this.htmlTag}>`;
|
||||
}
|
||||
protected htmlTag: string = "div";
|
||||
|
||||
/**
|
||||
* Renders the element and returns it as a string.
|
||||
@ -234,14 +246,20 @@ export abstract class GefestElement {
|
||||
return this.wrapHTML(this.content);
|
||||
}
|
||||
const rendered : string[] = [];
|
||||
if (!this.isHidden)
|
||||
for (const item of this.content) {
|
||||
if (typeof item === 'string') {
|
||||
rendered.push(item);
|
||||
}
|
||||
else if (item instanceof GefestElement) {
|
||||
if (!item.style && this.style) {
|
||||
if (!item.style && this.style !== null) {
|
||||
if (typeof this.style === "function") {
|
||||
item.style = this.style;
|
||||
}
|
||||
else if (this.style instanceof GefestStyle) {
|
||||
item.style = () => (this.style as GefestStyle);
|
||||
}
|
||||
}
|
||||
rendered.push(item.build());
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@ import { GefestElement } from "./element";
|
||||
|
||||
// For protecting the set of elements from direct access
|
||||
const elements: Set<GefestElement> = new Set();
|
||||
const elementsMapping: Map<string, GefestElement> = new Map<string, GefestElement> ();
|
||||
const elementsMapping: Record<string, GefestElement> = {};
|
||||
export class GefestEngine {
|
||||
static register (element: GefestElement) : string {
|
||||
elements.add(element);
|
||||
@ -10,8 +10,8 @@ export class GefestEngine {
|
||||
let gefestId : string;
|
||||
do {
|
||||
gefestId = `gefest-${Math.random().toString(36).substr(2, 9)}`;
|
||||
} while(!!elementsMapping.get(gefestId));
|
||||
elementsMapping.set(gefestId, element);
|
||||
} while(!!elementsMapping[gefestId]);
|
||||
elementsMapping[gefestId] = element;
|
||||
|
||||
return gefestId;
|
||||
}
|
||||
@ -31,14 +31,28 @@ export class GefestEngine {
|
||||
await GefestEngine.activateOnClick();
|
||||
}
|
||||
|
||||
static reRenderByGI (gefestId: string) {
|
||||
const gefestElement = elementsMapping.get(gefestId);
|
||||
if (!gefestElement) return;
|
||||
const elementHTML = document.querySelectorAll(`[data-gefest-id="${gefestId}"]`);
|
||||
static render (gefestId: string | string[] | null = null) {
|
||||
let gefestElements : string[] | null = null;
|
||||
if (typeof gefestId === "string")
|
||||
gefestId = [gefestId];
|
||||
else if (gefestId === null) {
|
||||
gefestId = Object.keys(elementsMapping);
|
||||
gefestElements = gefestId;
|
||||
}
|
||||
//const gefestElement = elementsMapping.get(gefestId);
|
||||
if (!gefestElements)
|
||||
gefestElements = gefestId.filter(gid => !!elementsMapping[gid]);
|
||||
if (gefestElements.length === 0) return;
|
||||
const elementHTML = document.querySelectorAll('[data-gefest-id]');
|
||||
|
||||
if (elementHTML.length > 0) {
|
||||
const htmlElement = elementHTML[0] as HTMLElement;
|
||||
htmlElement.outerHTML = gefestElement.build();
|
||||
for (let htmlElement of elementHTML) {
|
||||
const gid = htmlElement.getAttribute("data-gefest-id");
|
||||
if (!gid || !elementsMapping[gid])
|
||||
continue;
|
||||
|
||||
htmlElement.outerHTML = elementsMapping[gid].build();
|
||||
}
|
||||
GefestEngine.activateOnClick();
|
||||
}
|
||||
}
|
||||
|
||||
134
frontend/src/pages/auth/auth.ts
Normal file
134
frontend/src/pages/auth/auth.ts
Normal file
@ -0,0 +1,134 @@
|
||||
import { GefestElement } from "../../modules/Gefest/element";
|
||||
import { GefestA } from "../../modules/Gefest/primitives/a";
|
||||
import { GefestCenter } from "../../modules/Gefest/primitives/center";
|
||||
import { GefestHeader } from "../../modules/Gefest/primitives/header";
|
||||
import { DefaultElement } from "../default-element";
|
||||
|
||||
export interface AuthenticationRules {
|
||||
type: "password"
|
||||
};
|
||||
|
||||
type formType = "text" | "email" | "password" | "textarea";
|
||||
function getFormItem (elementHtmlId: string, title: string, type: formType = "text", placeholder : string | null = null): DefaultElement {
|
||||
const label: DefaultElement = new (class extends DefaultElement {
|
||||
constructor() {
|
||||
super([
|
||||
title
|
||||
]);
|
||||
this.addClass("form-label");
|
||||
this.setAttribute("for", elementHtmlId);
|
||||
}
|
||||
protected htmlTag = "label";
|
||||
});
|
||||
const input: DefaultElement = new (class extends DefaultElement {
|
||||
constructor() {
|
||||
super([]);
|
||||
this.id = elementHtmlId;
|
||||
this.addClass("form-control");
|
||||
if (!{
|
||||
text: true,
|
||||
email: true,
|
||||
password: true,
|
||||
textarea: false
|
||||
}[type]) {
|
||||
this.setAttribute("type", type);
|
||||
}
|
||||
if (placeholder !== null) {
|
||||
this.setAttribute("placeholder", placeholder);
|
||||
}
|
||||
}
|
||||
protected htmlTag = {
|
||||
text: "input",
|
||||
email: "input",
|
||||
password: "input",
|
||||
textarea: "textarea"
|
||||
}[type];
|
||||
});
|
||||
|
||||
return new (class extends DefaultElement { constructor() { super([
|
||||
label, input
|
||||
]); this.addClass("mb-3"); } });
|
||||
}
|
||||
class RegisterForm extends DefaultElement {
|
||||
link: GefestA;
|
||||
constructor(authRules : AuthenticationRules) {
|
||||
const header = new GefestHeader("Registration", 2);
|
||||
header.addClass("card-title");
|
||||
const link = new GefestA("Login to account");
|
||||
link.addClass("link-secondary");
|
||||
link.addClass("noselect");
|
||||
link.setPersonalStyle("cursor: pointer;");
|
||||
|
||||
const args = [
|
||||
getFormItem("username", "Username"),
|
||||
getFormItem("password", "Password", "password"),
|
||||
];
|
||||
switch (authRules.type) {
|
||||
case "password":
|
||||
args.push(getFormItem(
|
||||
"serverPassword",
|
||||
"Server Password",
|
||||
"password"
|
||||
));
|
||||
break;
|
||||
}
|
||||
|
||||
super([
|
||||
new GefestCenter([header, link]),
|
||||
...args
|
||||
]);
|
||||
this.link = link;
|
||||
this.isHidden = true;
|
||||
}
|
||||
}
|
||||
class LoginForm extends DefaultElement {
|
||||
link: GefestA;
|
||||
constructor() {
|
||||
const header = new GefestHeader("Login", 2);
|
||||
header.addClass("card-title");
|
||||
const link = new GefestA("Create a new account");
|
||||
link.addClass("link-secondary");
|
||||
link.addClass("noselect");
|
||||
link.setPersonalStyle("cursor: pointer;");
|
||||
|
||||
super([
|
||||
new GefestCenter([
|
||||
header, link
|
||||
]),
|
||||
getFormItem("username", "Username"),
|
||||
getFormItem("password", "Password", "password"),
|
||||
]);
|
||||
this.link = link;
|
||||
}
|
||||
}
|
||||
export class AuthenticationPage extends DefaultElement {
|
||||
constructor(authRules : AuthenticationRules) {
|
||||
const login = new LoginForm;
|
||||
const register = new RegisterForm(authRules);
|
||||
|
||||
const update = () => {
|
||||
login.update();
|
||||
register.update();
|
||||
};
|
||||
login.link.onClick = () => {
|
||||
login.isHidden = true;
|
||||
register.isHidden = false;
|
||||
update();
|
||||
};
|
||||
register.link.onClick = () => {
|
||||
login.isHidden = false;
|
||||
register.isHidden = true;
|
||||
update();
|
||||
};
|
||||
|
||||
super([
|
||||
new (class extends DefaultElement { constructor() { super([
|
||||
login,
|
||||
register
|
||||
]); this.addClass("card-body"); } })
|
||||
]);
|
||||
this.addClass("card");
|
||||
this.addClass("border");
|
||||
this.addClass("auth-card");
|
||||
}
|
||||
}
|
||||
@ -2,7 +2,7 @@ import { GefestElement } from "../modules/Gefest/element";
|
||||
import { SupportedStyles } from "../styles";
|
||||
|
||||
export abstract class DefaultElement extends GefestElement {
|
||||
style: SupportedStyles | null = null;
|
||||
style: SupportedStyles | (() => SupportedStyles) | null = null;
|
||||
constructor(content: (GefestElement | string)[] | string) {
|
||||
super(content);
|
||||
}
|
||||
@ -12,3 +12,13 @@ export abstract class DefaultElement extends GefestElement {
|
||||
return super.build(isFromStyle);
|
||||
}
|
||||
}
|
||||
|
||||
export class DefaultRoundedButton extends DefaultElement {
|
||||
constructor(btnContent : GefestElement | string) {
|
||||
super([ btnContent ]);
|
||||
this.setAttribute("type", "button");
|
||||
this.addClass("btn");
|
||||
this.addClass("rounded-circle");
|
||||
this.addClass("border");
|
||||
}
|
||||
}
|
||||
@ -28,7 +28,7 @@ class NewsUserInterfaceCard extends DefaultElement {
|
||||
}
|
||||
}
|
||||
// ^^ Card
|
||||
class NewsUserInterfaceButton extends DefaultElement {
|
||||
class NewsUserInterfaceButton extends DefaultElement { // <-- TODO: Fix it to DefaultRoundedButton
|
||||
constructor(userType : UserType, card : NewsUserInterfaceCard) {
|
||||
const uiIcon = new GefestI("");
|
||||
uiIcon.addClass({
|
||||
|
||||
@ -25,6 +25,7 @@ class NavbarButton extends DefaultElement {
|
||||
return `<li ${attrString}>${content}</li>`;
|
||||
}
|
||||
}
|
||||
export const themeToggle = new NavbarButton("fas fa-paint-roller");
|
||||
|
||||
class NavbarItemContainerUL extends DefaultElement {
|
||||
constructor() {
|
||||
@ -37,7 +38,9 @@ class NavbarItemContainerUL extends DefaultElement {
|
||||
|
||||
super([
|
||||
newsButton,
|
||||
chatButton
|
||||
chatButton,
|
||||
|
||||
themeToggle
|
||||
]);
|
||||
this.addClass("navbar-nav");
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ import { GefestElement } from "./modules/Gefest/element";
|
||||
import { Navbar } from "./pages/navbar";
|
||||
import { isNewsUI, NewsUserInterface } from "./pages/feed/news";
|
||||
|
||||
export abstract class SkeletonStyle extends GefestStyle {
|
||||
abstract class SkeletonStyle extends GefestStyle {
|
||||
protected abstract navbar(element: GefestElement): void;
|
||||
protected abstract newsui(element: GefestElement): void;
|
||||
protected abstract styleHandle(element: GefestElement): void;
|
||||
@ -65,5 +65,3 @@ export class LightStyle extends SkeletonStyle {
|
||||
}
|
||||
|
||||
export type SupportedStyles = SkeletonStyle;
|
||||
|
||||
export const defaultStyle = new LightStyle();
|
||||
@ -1,10 +1,16 @@
|
||||
import { Router } from "express";
|
||||
import { ApiResponse } from '../utils/rest-api-answer';
|
||||
import { NewsItem, getTestNews } from "../utils/news";
|
||||
import { getTestNews } from "../utils/news";
|
||||
import authRequired from "../utils/auth-required";
|
||||
import { NewsItem } from "./models/news";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/authenticationRules', (req, res) => {
|
||||
const response = new ApiResponse(req.authenticationRules);
|
||||
return res.json(response.getAnswer());
|
||||
});
|
||||
|
||||
router.get('/isAuthenticated', (req, res) => {
|
||||
const response = new ApiResponse(req.isAuthenticated);
|
||||
return res.json(response.getAnswer());
|
||||
|
||||
3
src/api/models/AuthenticationRules.ts
Normal file
3
src/api/models/AuthenticationRules.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export interface AuthenticationRulesTypes {
|
||||
type: "password";
|
||||
}
|
||||
14
src/api/models/news.ts
Normal file
14
src/api/models/news.ts
Normal file
@ -0,0 +1,14 @@
|
||||
export interface Source {
|
||||
name: string,
|
||||
icon?: string,
|
||||
sourceURL?: string,
|
||||
itemURL?: string,
|
||||
}
|
||||
|
||||
export interface NewsItem {
|
||||
id : number;
|
||||
title : string;
|
||||
content : string;
|
||||
source: Source;
|
||||
publishedAt : Date;
|
||||
}
|
||||
@ -1,19 +1,22 @@
|
||||
import express, { Request, Response } from 'express';
|
||||
import api from './api';
|
||||
import path from 'path';
|
||||
import { AuthenticationRulesTypes } from './api/models/AuthenticationRules';
|
||||
|
||||
const app = express();
|
||||
const port = 3000;
|
||||
|
||||
declare module 'express-serve-static-core' {
|
||||
interface Request {
|
||||
isAuthenticated?: boolean;
|
||||
isAuthenticated: boolean;
|
||||
authenticationRules: AuthenticationRulesTypes;
|
||||
}
|
||||
}
|
||||
|
||||
// Checking auth
|
||||
app.use((req, res, next) => {
|
||||
req.isAuthenticated = true; // Mock authentication, replace with real logic
|
||||
req.authenticationRules = { type: "password" };
|
||||
req.isAuthenticated = false; // Mock authentication, replace with real logic
|
||||
// Add your authentication logic here
|
||||
next();
|
||||
});
|
||||
|
||||
@ -1,17 +1,4 @@
|
||||
export interface Source {
|
||||
name: string,
|
||||
icon?: string,
|
||||
sourceURL?: string,
|
||||
itemURL?: string,
|
||||
}
|
||||
|
||||
export interface NewsItem {
|
||||
id : number;
|
||||
title : string;
|
||||
content : string;
|
||||
source: Source;
|
||||
publishedAt : Date;
|
||||
}
|
||||
import { NewsItem } from "../api/models/news";
|
||||
|
||||
export function getTestNews (): NewsItem[] {
|
||||
console.warn("Remove getTestNews from production! Only for tests!");
|
||||
|
||||
Loading…
Reference in New Issue
Block a user