Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
The information contained in this website is for general information purposes only. The information is provided by The Adversary and while we endeavour to keep the information up to date and correct, we make no representations or warranties of any kind, express or implied, about the completeness, accuracy, reliability, suitability or availability with respect to the website or the information, products, services, or related graphics contained on the website for any purpose. Any reliance you place on such information is therefore strictly at your own risk.
+
Through this website you are able to link to other websites which are not under the control of The Adversary. We have no control over the nature, content and availability of those sites. The inclusion of any links does not necessarily imply a recommendation or endorse the views expressed within them.
+
Every effort is made to keep the website up and running smoothly. However, The Adversary takes no responsibility for, and will not be liable for, the website being temporarily unavailable due to technical issues beyond our control.
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/img/logodark.svg b/frontend/img/logodark.svg
new file mode 100644
index 0000000..13f4e04
--- /dev/null
+++ b/frontend/img/logodark.svg
@@ -0,0 +1,98 @@
+
+
diff --git a/frontend/img/logolight.svg b/frontend/img/logolight.svg
new file mode 100644
index 0000000..58262b6
--- /dev/null
+++ b/frontend/img/logolight.svg
@@ -0,0 +1,98 @@
+
+
diff --git a/frontend/js/api.js b/frontend/js/api.js
new file mode 100644
index 0000000..7d7b788
--- /dev/null
+++ b/frontend/js/api.js
@@ -0,0 +1,198 @@
+const prefix = document.getElementsByTagName("base")[0].href.replace(/(?=.*)\/$/gm, "");
+async function unknownResponse(resp) {
+ let text = await resp.text();
+ try {
+ let json = JSON.parse(text);
+ return new Error(`${json["message"]} (${resp.status})`);
+ }
+ catch (error) {
+ return new Error(`Server sent unknown error: ${text} (${resp.status})`);
+ }
+}
+function buildQuery(options) {
+ let query = [];
+ options.forEach(element => {
+ if (element[1] !== undefined) {
+ if (element[1] instanceof Date) {
+ element[1] = element[1].getSeconds();
+ }
+ else if (element[1] instanceof Array) {
+ element[1] = JSON.stringify(element[1]);
+ }
+ query.push(`${element[0]}=${element[1]}`);
+ }
+ });
+ return query.join("&");
+}
+async function login(username, password) {
+ let result = await fetch(`${prefix}/api/login`, {
+ method: "POST",
+ body: JSON.stringify({ "username": username, "password": password }),
+ credentials: "same-origin"
+ });
+ switch (result.status) {
+ case 200:
+ return;
+ case 401:
+ throw new Error("Wrong username and/or password");
+ default:
+ throw await unknownResponse(result);
+ }
+}
+async function authors(name, limit) {
+ let query = [["name", name], ["limit", limit]];
+ let result = await fetch(`${prefix}/api/authors?${buildQuery(query)}`);
+ if (result.status == 200) {
+ return await result.json();
+ }
+ else {
+ throw await unknownResponse(result);
+ }
+}
+async function tags(name, limit) {
+ let query = [["name", name], ["limit", limit]];
+ let result = await fetch(`${prefix}/api/tags?${buildQuery(query)}`);
+ if (result.status == 200) {
+ return await result.json();
+ }
+ else {
+ throw await unknownResponse(result);
+ }
+}
+async function recent(limit = 20) {
+ let query = [["limit", limit]];
+ let result = await fetch(`${prefix}/api/recent?${buildQuery(query)}`);
+ if (result.status == 200) {
+ return await result.json();
+ }
+ else {
+ throw await unknownResponse(result);
+ }
+}
+async function search(q) {
+ let query = [["q", q.query], ["from", q.from], ["to", q.to], ["authors", q.authors], ["tags", q.tags], ["limit", q.limit]];
+ let result = await fetch(`${prefix}/api/search?${buildQuery(query)}`);
+ if (result.status == 200) {
+ return await result.json();
+ }
+ else {
+ throw unknownResponse(result);
+ }
+}
+async function getArticle(id) {
+ let query = [["id", id]];
+ let result = await fetch(`${prefix}/api/article?${buildQuery(query)}`, {
+ method: "GET"
+ });
+ switch (result.status) {
+ case 200:
+ return await result.json();
+ case 401:
+ throw new Error("Not authorized");
+ case 404:
+ throw new Error("Article not found");
+ default:
+ throw await unknownResponse(result);
+ }
+}
+async function uploadArticle(payload) {
+ let result = await fetch(`${prefix}/api/article`, {
+ method: "POST",
+ body: JSON.stringify(payload)
+ });
+ switch (result.status) {
+ case 201:
+ return await result.json();
+ case 401:
+ throw new Error("Not authorized");
+ case 409:
+ throw new Error("An article with the same title already exists");
+ default:
+ throw await unknownResponse(result);
+ }
+}
+async function editArticle(payload) {
+ let result = await fetch(`${prefix}/api/article`, {
+ method: "PATCH",
+ body: JSON.stringify(payload)
+ });
+ let json = await result.json();
+ switch (result.status) {
+ case 201:
+ return json;
+ case 401:
+ throw new Error("Not authorized");
+ case 404:
+ throw new Error("Could not find article");
+ case 409:
+ throw new Error("An article with the same title already exists");
+ default:
+ throw await unknownResponse(result);
+ }
+}
+async function deleteArticle(id) {
+ let result = await fetch(`${prefix}/api/article`, {
+ method: "DELETE",
+ body: JSON.stringify({ "id": id })
+ });
+ switch (result.status) {
+ case 200:
+ return;
+ case 401:
+ throw new Error("Not authorized");
+ case 404:
+ throw new Error("Could not find article");
+ default:
+ throw await unknownResponse(result);
+ }
+}
+async function getAssets(name, limit = 20) {
+ let query = [["q", name], ["limit", limit]];
+ let result = await fetch(`${prefix}/api/assets?${buildQuery(query)}`, {
+ method: "GET",
+ });
+ switch (result.status) {
+ case 200:
+ return await result.json();
+ case 401:
+ throw new Error("Not authorized");
+ default:
+ throw await unknownResponse(result);
+ }
+}
+async function addAsset(name, content) {
+ let result = await fetch(`${prefix}/api/assets`, {
+ method: "POST",
+ body: JSON.stringify({
+ "name": name,
+ "content": content,
+ })
+ });
+ switch (result.status) {
+ case 201:
+ return await result.json();
+ case 401:
+ throw new Error("Not authorized");
+ case 409:
+ throw new Error("An asset with the same name already exists");
+ default:
+ throw await unknownResponse(result);
+ }
+}
+async function deleteAsset(id) {
+ let result = await fetch(`${prefix}/api/assets`, {
+ method: "DELETE",
+ body: JSON.stringify({ "id": id })
+ });
+ switch (result.status) {
+ case 200:
+ return;
+ case 401:
+ throw new Error("Not authorized");
+ case 404:
+ throw new Error("An asset with this id does not exist");
+ default:
+ throw await unknownResponse(result);
+ }
+}
+//# sourceMappingURL=api.js.map
\ No newline at end of file
diff --git a/frontend/js/api.js.map b/frontend/js/api.js.map
new file mode 100644
index 0000000..b81e994
--- /dev/null
+++ b/frontend/js/api.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"api.js","sourceRoot":"","sources":["api.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
diff --git a/frontend/js/api.ts b/frontend/js/api.ts
new file mode 100644
index 0000000..46a573e
--- /dev/null
+++ b/frontend/js/api.ts
@@ -0,0 +1,277 @@
+const prefix = document.getElementsByTagName("base")[0].href.replace(/(?=.*)\/$/gm, "");
+
+
+async function unknownResponse(resp: Response): Promise {
+ let text = await resp.text()
+ try {
+ let json = JSON.parse(text)
+ return new Error(`${json["message"]} (${resp.status})`)
+ } catch (error) {
+ return new Error(`Server sent unknown error: ${text} (${resp.status})`)
+ }
+}
+
+function buildQuery(options: any[][]): string {
+ let query: string[] = []
+ options.forEach(element => {
+ if (element[1] !== undefined) {
+ if (element[1] instanceof Date) {
+ element[1] = element[1].getSeconds()
+ } else if (element[1] instanceof Array) {
+ element[1] = JSON.stringify(element[1])
+ }
+ query.push(`${element[0]}=${element[1]}`)
+ }
+ });
+ return query.join("&")
+}
+
+interface Author {
+ id: number,
+ name: string,
+ information: string
+}
+
+interface ArticleSummary {
+ id: number,
+ title: string,
+ summary: string
+ authors: Author[],
+ image?: string,
+ tags: string[],
+ created: Date,
+ modified?: Date,
+ link: string
+}
+
+interface Asset {
+ id: number,
+ name: string,
+ link: string
+}
+
+async function login(username: string, password: string): Promise {
+ let result = await fetch(`${prefix}/api/login`, {
+ method: "POST",
+ body: JSON.stringify({"username": username, "password": password}),
+ credentials: "same-origin"
+ })
+ switch (result.status) {
+ case 200:
+ return
+ case 401:
+ throw new Error("Wrong username and/or password")
+ default:
+ throw await unknownResponse(result)
+ }
+}
+
+async function authors(name?: string, limit?: number): Promise {
+ let query = [["name", name], ["limit", limit]]
+
+ let result = await fetch(`${prefix}/api/authors?${buildQuery(query)}`)
+ if (result.status == 200) {
+ return await result.json()
+ } else {
+ throw await unknownResponse(result)
+ }
+}
+
+async function tags(name?: string, limit?: number): Promise {
+ let query = [["name", name], ["limit", limit]]
+
+ let result = await fetch(`${prefix}/api/tags?${buildQuery(query)}`)
+ if (result.status == 200) {
+ return await result.json()
+ } else {
+ throw await unknownResponse(result)
+ }
+}
+
+async function recent(limit: number = 20): Promise {
+ let query = [["limit", limit]]
+
+ let result = await fetch(`${prefix}/api/recent?${buildQuery(query)}`)
+ if (result.status == 200) {
+ return await result.json()
+ } else {
+ throw await unknownResponse(result)
+ }
+}
+
+interface SearchQuery {
+ query?: string,
+ from?: Date,
+ to?: Date,
+ authors?: number[],
+ tags?: string[],
+ limit?: number
+}
+
+async function search(q: SearchQuery): Promise {
+ let query = [["q", q.query], ["from", q.from], ["to", q.to], ["authors", q.authors], ["tags", q.tags], ["limit", q.limit]]
+
+ let result = await fetch(`${prefix}/api/search?${buildQuery(query)}`)
+ if (result.status == 200) {
+ return await result.json()
+ } else {
+ throw unknownResponse(result)
+ }
+}
+
+interface ArticleGetPayload {
+ title: string,
+ summary: string,
+ authors: number[],
+ image: string,
+ tags: string[],
+ link: string
+ content: string
+}
+
+async function getArticle(id: number): Promise {
+ let query = [["id", id]]
+
+ let result = await fetch(`${prefix}/api/article?${buildQuery(query)}`, {
+ method: "GET"
+ })
+ switch (result.status) {
+ case 200:
+ return await result.json()
+ case 401:
+ throw new Error("Not authorized")
+ case 404:
+ throw new Error("Article not found")
+ default:
+ throw await unknownResponse(result)
+ }
+}
+
+interface ArticleUploadPayload {
+ title: string,
+ summary: string,
+ authors: number[],
+ image?: string,
+ tags: string[],
+ link?: string
+ content: string
+}
+
+async function uploadArticle(payload: ArticleUploadPayload): Promise {
+ let result = await fetch(`${prefix}/api/article`, {
+ method: "POST",
+ body: JSON.stringify(payload)
+ })
+ switch (result.status) {
+ case 201:
+ return await result.json()
+ case 401:
+ throw new Error("Not authorized")
+ case 409:
+ throw new Error("An article with the same title already exists")
+ default:
+ throw await unknownResponse(result)
+ }
+}
+
+interface ArticleEditPayload {
+ id: number,
+ title?: string,
+ summary?: string,
+ authors?: number[],
+ image?: string,
+ tags?: string[],
+ link?: string
+ content?: string
+}
+
+async function editArticle(payload: ArticleEditPayload): Promise {
+ let result = await fetch(`${prefix}/api/article`, {
+ method: "PATCH",
+ body: JSON.stringify(payload)
+ })
+ let json = await result.json()
+
+ switch (result.status) {
+ case 201:
+ return json
+ case 401:
+ throw new Error("Not authorized")
+ case 404:
+ throw new Error("Could not find article")
+ case 409:
+ throw new Error("An article with the same title already exists")
+ default:
+ throw await unknownResponse(result)
+ }
+}
+
+async function deleteArticle(id: number): Promise {
+ let result = await fetch(`${prefix}/api/article`, {
+ method: "DELETE",
+ body: JSON.stringify({"id": id})
+ })
+ switch (result.status) {
+ case 200:
+ return
+ case 401:
+ throw new Error("Not authorized")
+ case 404:
+ throw new Error("Could not find article")
+ default:
+ throw await unknownResponse(result)
+ }
+}
+
+async function getAssets(name?: string, limit: number = 20): Promise {
+ let query = [["q", name], ["limit", limit]]
+
+ let result = await fetch(`${prefix}/api/assets?${buildQuery(query)}`, {
+ method: "GET",
+ })
+ switch (result.status) {
+ case 200:
+ return await result.json()
+ case 401:
+ throw new Error("Not authorized")
+ default:
+ throw await unknownResponse(result)
+ }
+}
+
+async function addAsset(name: string, content: string): Promise {
+ let result = await fetch(`${prefix}/api/assets`, {
+ method: "POST",
+ body: JSON.stringify({
+ "name": name,
+ "content": content,
+ })
+ })
+ switch (result.status) {
+ case 201:
+ return await result.json()
+ case 401:
+ throw new Error("Not authorized")
+ case 409:
+ throw new Error("An asset with the same name already exists")
+ default:
+ throw await unknownResponse(result)
+ }
+}
+
+async function deleteAsset(id: number): Promise {
+ let result = await fetch(`${prefix}/api/assets`, {
+ method: "DELETE",
+ body: JSON.stringify({"id": id})
+ })
+ switch (result.status) {
+ case 200:
+ return
+ case 401:
+ throw new Error("Not authorized")
+ case 404:
+ throw new Error("An asset with this id does not exist")
+ default:
+ throw await unknownResponse(result)
+ }
+}
diff --git a/frontend/js/main.js b/frontend/js/main.js
new file mode 100644
index 0000000..4bd9e4f
--- /dev/null
+++ b/frontend/js/main.js
@@ -0,0 +1,69 @@
+let articleParent = document.getElementById("articles");
+function updateSeach(value) {
+ if (value == "") {
+ addRecent();
+ return;
+ }
+ let query = {
+ query: value,
+ limit: 5
+ };
+ clearArticles();
+ search(query).then(function (data) {
+ data.forEach(function (article) {
+ addArticle(article);
+ });
+ });
+}
+function clearArticles() {
+ articleParent.innerHTML = "";
+}
+window.onload = function () {
+ addRecent();
+};
+function addRecent() {
+ clearArticles();
+ recent(5).then(function (data) {
+ data.forEach(function (article) {
+ addArticle(article);
+ });
+ });
+}
+function addArticle(article) {
+ let articleA = document.createElement("a");
+ articleA.setAttribute("href", article.link);
+ let articleDiv = document.createElement("div");
+ articleDiv.setAttribute("class", "article");
+ let articleHeader = document.createElement("div");
+ articleHeader.setAttribute("class", "article-header");
+ let articleHeaderTitle = document.createElement("h3");
+ articleHeaderTitle.innerHTML = article.title;
+ articleHeader.appendChild(articleHeaderTitle);
+ articleDiv.appendChild(articleHeader);
+ let articleDescription = document.createElement("div");
+ articleDescription.setAttribute("class", "article-description");
+ let articleDescriptionP = document.createElement("p");
+ let articleDescriptionTopics = document.createElement("i");
+ articleDescriptionTopics.innerHTML = article.tags.join(", ");
+ let articleDescriptionAuthors = document.createElement("i");
+ articleDescriptionAuthors.innerHTML = article.authors[0].name;
+ let articleDescriptionDate = document.createElement("i");
+ articleDescriptionDate.innerHTML = article.modified.toString();
+ articleDescriptionP.appendChild(articleDescriptionTopics);
+ articleDescriptionP.appendChild(articleDescriptionAuthors);
+ articleDescriptionP.appendChild(articleDescriptionDate);
+ articleDescription.appendChild(articleDescriptionP);
+ articleDiv.appendChild(articleDescription);
+ let articleBody = document.createElement("div");
+ articleBody.setAttribute("class", "article-body");
+ let articleBodyP = document.createElement("p");
+ articleBodyP.innerHTML = article.summary;
+ articleBody.appendChild(articleBodyP);
+ articleDiv.appendChild(articleBody);
+ articleA.appendChild(articleDiv);
+ articleParent.appendChild(articleA);
+ let divider = document.createElement("div");
+ divider.setAttribute("class", "divider");
+ articleParent.appendChild(divider);
+}
+//# sourceMappingURL=main.js.map
\ No newline at end of file
diff --git a/frontend/js/main.js.map b/frontend/js/main.js.map
new file mode 100644
index 0000000..db17ddf
--- /dev/null
+++ b/frontend/js/main.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"main.js","sourceRoot":"","sources":["main.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
diff --git a/frontend/js/main.ts b/frontend/js/main.ts
new file mode 100644
index 0000000..5f8790a
--- /dev/null
+++ b/frontend/js/main.ts
@@ -0,0 +1,93 @@
+let articleParent = document.getElementById("articles");
+
+function updateSeach(value: string) {
+ if(value == "") {
+ addRecent()
+ return
+ }
+
+ let query: SearchQuery = {
+ query: value,
+ limit: 5
+ }
+
+
+ clearArticles()
+ search(query).then(function(data) {
+ data.forEach(function(article) {
+ addArticle(article)
+ })
+ })
+}
+
+function clearArticles() {
+ articleParent.innerHTML = ""
+}
+
+window.onload = function() {
+ addRecent()
+}
+
+function addRecent () {
+ clearArticles()
+ recent(5).then(function(data) {
+ data.forEach(function(article) {
+ addArticle(article)
+ })
+ })
+}
+
+function addArticle(article: ArticleSummary) {
+ let articleA = document.createElement("a")
+ articleA.setAttribute("href", article.link)
+
+ let articleDiv = document.createElement("div")
+ articleDiv.setAttribute("class", "article")
+
+ let articleHeader = document.createElement("div")
+ articleHeader.setAttribute("class", "article-header")
+
+ let articleHeaderTitle = document.createElement("h3")
+ articleHeaderTitle.innerHTML = article.title
+
+ articleHeader.appendChild(articleHeaderTitle)
+ articleDiv.appendChild(articleHeader)
+
+ let articleDescription = document.createElement("div")
+ articleDescription.setAttribute("class", "article-description")
+
+ let articleDescriptionP = document.createElement("p")
+
+ let articleDescriptionTopics = document.createElement("i")
+ articleDescriptionTopics.innerHTML = article.tags.join(", ")
+
+ let articleDescriptionAuthors = document.createElement("i")
+ articleDescriptionAuthors.innerHTML = article.authors[0].name //TODO join ALL Auhtors
+
+ let articleDescriptionDate = document.createElement("i")
+ articleDescriptionDate.innerHTML = article.modified.toString()
+
+ articleDescriptionP.appendChild(articleDescriptionTopics)
+ articleDescriptionP.appendChild(articleDescriptionAuthors)
+ articleDescriptionP.appendChild(articleDescriptionDate)
+
+ articleDescription.appendChild(articleDescriptionP)
+ articleDiv.appendChild(articleDescription)
+
+ let articleBody = document.createElement("div")
+ articleBody.setAttribute("class", "article-body")
+
+ let articleBodyP = document.createElement("p")
+ articleBodyP.innerHTML = article.summary
+
+ articleBody.appendChild(articleBodyP)
+ articleDiv.appendChild(articleBody)
+
+ articleA.appendChild(articleDiv)
+ articleParent.appendChild(articleA)
+
+ let divider = document.createElement("div")
+ divider.setAttribute("class", "divider")
+
+ articleParent.appendChild(divider)
+}
diff --git a/frontend/js/tsconfig.json b/frontend/js/tsconfig.json
new file mode 100644
index 0000000..90e7322
--- /dev/null
+++ b/frontend/js/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "compileOnSave": true,
+ "compilerOptions": {
+ "module": "commonjs",
+ "target": "es2018",
+ "removeComments": true,
+ "sourceMap": true,
+ "lib": ["es2018", "dom"]
+ }
+}