Compare commits
18 Commits
b1d39abd67
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| d4cf0b2c39 | |||
| 1dfa60703b | |||
| df1513387a | |||
| c32880ee3e | |||
| 5d8cf17e22 | |||
| 04ab5e3002 | |||
| 7927b515f2 | |||
| 7789ebc0f9 | |||
| 362f68108c | |||
| c5d4d82c42 | |||
| 43bdf9a746 | |||
| 7e8a6d6590 | |||
| ad8fc19750 | |||
| a6aab8f975 | |||
| 6f184e1083 | |||
| 7a1143ca9b | |||
| 249f949967 | |||
| acc391e8e6 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -6,3 +6,5 @@ xcuserdata
|
|||||||
DerivedData/
|
DerivedData/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
file:*
|
file:*
|
||||||
|
Resources/config.json
|
||||||
|
Resources/db.sqlite
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<Workspace
|
|
||||||
version = "1.0">
|
|
||||||
<FileRef
|
|
||||||
location = "self:">
|
|
||||||
</FileRef>
|
|
||||||
</Workspace>
|
|
||||||
19
LICENSE
Normal file
19
LICENSE
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
MIT License Copyright (c) 2020 Arsen Musayelyan
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is furnished
|
||||||
|
to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice (including the next
|
||||||
|
paragraph) shall be included in all copies or substantial portions of the
|
||||||
|
Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
|
||||||
|
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
|
||||||
|
OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
@@ -28,6 +28,33 @@
|
|||||||
"version": "4.2.1"
|
"version": "4.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"package": "fluent",
|
||||||
|
"repositoryURL": "https://github.com/vapor/fluent.git",
|
||||||
|
"state": {
|
||||||
|
"branch": null,
|
||||||
|
"revision": "e681c93df3201a2d8ceef15e8a9a0634578df233",
|
||||||
|
"version": "4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"package": "fluent-kit",
|
||||||
|
"repositoryURL": "https://github.com/vapor/fluent-kit.git",
|
||||||
|
"state": {
|
||||||
|
"branch": null,
|
||||||
|
"revision": "31d96b547cc1f869f2885d932a8a9a7ae2103fc6",
|
||||||
|
"version": "1.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"package": "fluent-sqlite-driver",
|
||||||
|
"repositoryURL": "https://github.com/vapor/fluent-sqlite-driver.git",
|
||||||
|
"state": {
|
||||||
|
"branch": null,
|
||||||
|
"revision": "6f29f6f182c812075f09c7575c18ac5535c26824",
|
||||||
|
"version": "4.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"package": "leaf",
|
"package": "leaf",
|
||||||
"repositoryURL": "https://github.com/vapor/leaf",
|
"repositoryURL": "https://github.com/vapor/leaf",
|
||||||
@@ -64,6 +91,33 @@
|
|||||||
"version": "4.2.0"
|
"version": "4.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"package": "sql-kit",
|
||||||
|
"repositoryURL": "https://github.com/vapor/sql-kit.git",
|
||||||
|
"state": {
|
||||||
|
"branch": null,
|
||||||
|
"revision": "ea9928b7f4a801b175a00b982034d9c54ecb6167",
|
||||||
|
"version": "3.7.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"package": "sqlite-kit",
|
||||||
|
"repositoryURL": "https://github.com/vapor/sqlite-kit.git",
|
||||||
|
"state": {
|
||||||
|
"branch": null,
|
||||||
|
"revision": "2ec279b9c845cec254646834b66338551a024561",
|
||||||
|
"version": "4.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"package": "sqlite-nio",
|
||||||
|
"repositoryURL": "https://github.com/vapor/sqlite-nio.git",
|
||||||
|
"state": {
|
||||||
|
"branch": null,
|
||||||
|
"revision": "6481dd0b01112d082dd7eb362782126e81964138",
|
||||||
|
"version": "1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"package": "swift-backtrace",
|
"package": "swift-backtrace",
|
||||||
"repositoryURL": "https://github.com/swift-server/swift-backtrace.git",
|
"repositoryURL": "https://github.com/swift-server/swift-backtrace.git",
|
||||||
@@ -78,8 +132,8 @@
|
|||||||
"repositoryURL": "https://github.com/apple/swift-crypto.git",
|
"repositoryURL": "https://github.com/apple/swift-crypto.git",
|
||||||
"state": {
|
"state": {
|
||||||
"branch": null,
|
"branch": null,
|
||||||
"revision": "9b9d1868601a199334da5d14f4ab2d37d4f8d0c5",
|
"revision": "9680b7251cd2be22caaed8f1468bd9e8915a62fb",
|
||||||
"version": "1.0.2"
|
"version": "1.1.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ let package = Package(
|
|||||||
.package(url: "https://github.com/vapor/leaf", .exact("4.0.0-tau.1")),
|
.package(url: "https://github.com/vapor/leaf", .exact("4.0.0-tau.1")),
|
||||||
.package(url: "https://github.com/vapor/leaf-kit", .exact("1.0.0-tau.1.1")),
|
.package(url: "https://github.com/vapor/leaf-kit", .exact("1.0.0-tau.1.1")),
|
||||||
// Leaf Error Middleware for custom error pages
|
// Leaf Error Middleware for custom error pages
|
||||||
.package(name: "LeafErrorMiddleware", url: "https://github.com/brokenhandsio/leaf-error-middleware.git", from: "2.0.0-beta")
|
.package(name: "LeafErrorMiddleware", url: "https://github.com/brokenhandsio/leaf-error-middleware.git", from: "2.0.0-beta"),
|
||||||
|
.package(url: "https://github.com/apple/swift-crypto.git", from: "1.1.2"),
|
||||||
|
.package(url: "https://github.com/vapor/fluent.git", from: "4.0.0"),
|
||||||
|
.package(url: "https://github.com/vapor/fluent-sqlite-driver.git", from: "4.0.0")
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
.target(
|
.target(
|
||||||
@@ -21,6 +24,9 @@ let package = Package(
|
|||||||
dependencies: [
|
dependencies: [
|
||||||
.product(name: "Vapor", package: "vapor"),
|
.product(name: "Vapor", package: "vapor"),
|
||||||
.product(name: "Leaf", package: "leaf"),
|
.product(name: "Leaf", package: "leaf"),
|
||||||
|
.product(name: "Crypto", package: "swift-crypto"),
|
||||||
|
.product(name: "Fluent", package: "fluent"),
|
||||||
|
.product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"),
|
||||||
"LeafErrorMiddleware"
|
"LeafErrorMiddleware"
|
||||||
],
|
],
|
||||||
swiftSettings: [
|
swiftSettings: [
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Statusboard
|
# Statusboard
|
||||||
## Status dashboard written in Swift using Vapor
|
## This repository is archived as [simpledash](https://gitea.arsenm.dev/Arsen6331/simpledash) is now able to replace it
|
||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
Statusboard can be configured using the JSON config at `/Resources/config.json`
|
Statusboard can be configured using the JSON config at `/Resources/config.json`
|
||||||
|
|||||||
24
Resources/Views/404.leaf
Normal file
24
Resources/Views/404.leaf
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Not Found</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="/tabler/css/tabler.css">
|
||||||
|
<link rel="stylesheet" type="text/css" href="/tabler/css/dashboard.css">
|
||||||
|
<script src="/tabler/js/core.js"></script>
|
||||||
|
<script src="/tabler/js/dashboard.js"></script>
|
||||||
|
</head>
|
||||||
|
<body class="">
|
||||||
|
<div class="page">
|
||||||
|
<div class="page-content">
|
||||||
|
<div class="container text-center">
|
||||||
|
<div class="display-1 text-muted mb-5"><i class="si si-exclamation"></i> 404</div>
|
||||||
|
<h1 class="h2 mb-3">This page wasn't found on our server</h1>
|
||||||
|
<p class="h4 text-muted font-weight-normal mb-7">Either the page was removed or you clicked on a bad link…</p>
|
||||||
|
<a class="btn btn-primary" href="javascript:history.back()">
|
||||||
|
<i class="fe fe-arrow-left mr-2"></i>Go back
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -16,7 +16,11 @@
|
|||||||
<a class="header-brand" href="/">#(config.title)</a>
|
<a class="header-brand" href="/">#(config.title)</a>
|
||||||
<div class="d-flex order-lg-2 ml-auto">
|
<div class="d-flex order-lg-2 ml-auto">
|
||||||
<div class="nav-item d-none d-md-flex">
|
<div class="nav-item d-none d-md-flex">
|
||||||
<a href="https://github.com/tabler/tabler" class="btn btn-sm btn-outline-primary" target="_blank">Source code</a>
|
#if(loggedIn):
|
||||||
|
<a href="/logout" class="btn btn-sm btn-outline-primary" target="_self">Log Out</a>
|
||||||
|
#else:
|
||||||
|
<a href="/login" class="btn btn-sm btn-outline-primary" target="_self">Log In</a>
|
||||||
|
#endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
43
Resources/Views/card.leaf
Normal file
43
Resources/Views/card.leaf
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<div class="col">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">#(service["name"])</h3>
|
||||||
|
<div class="card-options">
|
||||||
|
<span class="tag #if(service["url"]): btn-loading #endif">
|
||||||
|
Status
|
||||||
|
<span id="#(service["name"])Status" class="tag-addon">Unavailable</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
#(service["description"])
|
||||||
|
</div>
|
||||||
|
#if(service["url"]):
|
||||||
|
<div class="card-footer">
|
||||||
|
URL: <a href="#(service["url"])">#(service["url"])</a>
|
||||||
|
</div>
|
||||||
|
#endif
|
||||||
|
</div>
|
||||||
|
#if(service["url"]):
|
||||||
|
<script>
|
||||||
|
fullURL = '#(service["url"])'
|
||||||
|
var url = fullURL.replace("https://", "")
|
||||||
|
url = url.replace("http://", "")
|
||||||
|
var request = new XMLHttpRequest()
|
||||||
|
request.open('GET', "/status/" + url, true)
|
||||||
|
request.onload = function () {
|
||||||
|
var data = JSON.parse(this.response)
|
||||||
|
if (data.down === "true" || parseInt(data.code) > 500 && parseInt(data.code) < 600 ) {
|
||||||
|
document.getElementById('#(service["name"])Status').classList.add("tag-danger")
|
||||||
|
document.getElementById('#(service["name"])Status').parentElement.classList.remove("btn-loading")
|
||||||
|
document.getElementById('#(service["name"])Status').innerHTML = "Offline"
|
||||||
|
} else {
|
||||||
|
document.getElementById('#(service["name"])Status').classList.add("tag-success")
|
||||||
|
document.getElementById('#(service["name"])Status').parentElement.classList.remove("btn-loading")
|
||||||
|
document.getElementById('#(service["name"])Status').innerHTML = "Online"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
request.send()
|
||||||
|
</script>
|
||||||
|
#endif
|
||||||
|
</div>
|
||||||
@@ -7,47 +7,13 @@
|
|||||||
<h6>#(node)</h6>
|
<h6>#(node)</h6>
|
||||||
<div class="row row-cards row-deck">
|
<div class="row row-cards row-deck">
|
||||||
#for(service in services):
|
#for(service in services):
|
||||||
<div class="col">
|
#if(!service["private"] == "true"):
|
||||||
<div class="card">
|
#inline("card")
|
||||||
<div class="card-header">
|
#else:
|
||||||
<h3 class="card-title">#(service["name"])</h3>
|
#if(loggedIn):
|
||||||
<div class="card-options">
|
#inline("card")
|
||||||
<span class="tag">
|
|
||||||
Status
|
|
||||||
<span id="#(service["name"])Status" class="tag-addon">Unavailable</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
#(service["description"])
|
|
||||||
</div>
|
|
||||||
#if(service["url"]):
|
|
||||||
<div class="card-footer">
|
|
||||||
URL: <a href="#(service["url"])">#(service["url"])</a>
|
|
||||||
</div>
|
|
||||||
#endif
|
#endif
|
||||||
</div>
|
|
||||||
#if(service["url"]):
|
|
||||||
<script>
|
|
||||||
fullURL = '#(service["url"])'
|
|
||||||
var url = fullURL.replace("https://", "")
|
|
||||||
url = url.replace("http://", "")
|
|
||||||
var request = new XMLHttpRequest()
|
|
||||||
request.open('GET', "/status/" + url, true)
|
|
||||||
request.onload = function () {
|
|
||||||
var data = JSON.parse(this.response)
|
|
||||||
if (data.down === "true" || parseInt(data.code) > 500 && parseInt(data.code) < 600 ) {
|
|
||||||
document.getElementById('#(service["name"])Status').classList.add("tag-danger")
|
|
||||||
document.getElementById('#(service["name"])Status').innerHTML = "Offline"
|
|
||||||
} else {
|
|
||||||
document.getElementById('#(service["name"])Status').classList.add("tag-success")
|
|
||||||
document.getElementById('#(service["name"])Status').innerHTML = "Online"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
request.send()
|
|
||||||
</script>
|
|
||||||
#endif
|
#endif
|
||||||
</div>
|
|
||||||
#endfor
|
#endfor
|
||||||
</div>
|
</div>
|
||||||
#endfor
|
#endfor
|
||||||
|
|||||||
30
Resources/Views/login.leaf
Normal file
30
Resources/Views/login.leaf
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
#define(body):
|
||||||
|
<div class="page">
|
||||||
|
<div class="page-single">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-login mx-auto">
|
||||||
|
<div class="text-center mb-6">
|
||||||
|
<img src="" class="h-6" alt="">
|
||||||
|
</div>
|
||||||
|
<form class="card" action="/login" method="post">
|
||||||
|
<div class="card-body p-6">
|
||||||
|
<div class="card-title">Login to #(config.title)</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<input type="password" class="form-control" id="passwordInput" name="password" value="password" placeholder="Password">
|
||||||
|
</div>
|
||||||
|
<div class="form-footer">
|
||||||
|
<button type="submit" class="btn btn-primary btn-block">Sign in</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
#enddefine
|
||||||
|
#inline("base")
|
||||||
31
Resources/Views/serverError.leaf
Normal file
31
Resources/Views/serverError.leaf
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>#(statusMessage)</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="/tabler/css/tabler.css">
|
||||||
|
<link rel="stylesheet" type="text/css" href="/tabler/css/dashboard.css">
|
||||||
|
<script src="/tabler/js/core.js"></script>
|
||||||
|
<script src="/tabler/js/dashboard.js"></script>
|
||||||
|
</head>
|
||||||
|
<body class="">
|
||||||
|
<div class="page">
|
||||||
|
<div class="page-content">
|
||||||
|
<div class="container text-center">
|
||||||
|
<div class="display-1 text-muted mb-5"><i class="si si-exclamation"></i> #(status)</div>
|
||||||
|
<h1 class="h2 mb-3">
|
||||||
|
#if(reason ?? false):
|
||||||
|
#(reason)
|
||||||
|
#else:
|
||||||
|
#(statusMessage)
|
||||||
|
#endif
|
||||||
|
</h1>
|
||||||
|
<p class="h4 text-muted font-weight-normal mb-7">If you have done everything properly, please try again later…</p>
|
||||||
|
<a class="btn btn-primary" href="javascript:history.back()">
|
||||||
|
<i class="fe fe-arrow-left mr-2"></i>Go back
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"title": "Statusboard",
|
"title": "Statusboard",
|
||||||
|
"passwordHash": "ef92b778bafe771e89245b89ecbc08a44a4e166c06659911881f383d4473e94f",
|
||||||
"services": {
|
"services": {
|
||||||
"Node 1": [
|
"Node 1": [
|
||||||
{
|
{
|
||||||
@@ -7,6 +8,14 @@
|
|||||||
"url": "https://example.com",
|
"url": "https://example.com",
|
||||||
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
|
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"Secret Cluster 01": [
|
||||||
|
{
|
||||||
|
"name": "Secret Example",
|
||||||
|
"url": "https://example.net",
|
||||||
|
"private": "true",
|
||||||
|
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,5 +3,6 @@ import Vapor
|
|||||||
|
|
||||||
struct Config: Codable {
|
struct Config: Codable {
|
||||||
let title: String
|
let title: String
|
||||||
|
let passwordHash: String
|
||||||
let services: [String:[[String:String]]]
|
let services: [String:[[String:String]]]
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,24 @@
|
|||||||
|
import Fluent
|
||||||
import Vapor
|
import Vapor
|
||||||
import Leaf
|
import Leaf
|
||||||
import LeafErrorMiddleware
|
import LeafErrorMiddleware
|
||||||
|
import FluentSQLiteDriver
|
||||||
|
|
||||||
// configures your application
|
// configures your application
|
||||||
public func configure(_ app: Application) throws {
|
public func configure(_ app: Application) throws {
|
||||||
|
app.middleware.use(LeafErrorMiddleware())
|
||||||
// Serve files from /Public
|
// Serve files from /Public
|
||||||
app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))
|
app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))
|
||||||
app.middleware.use(LeafErrorMiddleware())
|
|
||||||
|
app.sessions.use(.fluent)
|
||||||
|
app.sessions.configuration.cookieName = "statusboard-session"
|
||||||
|
app.sessions.configuration.cookieFactory = { sessionID in
|
||||||
|
.init(string: sessionID.string, expires: Date(timeIntervalSinceNow: 60*60*24*365), isSecure: true, isHTTPOnly: true, sameSite: HTTPCookies.SameSitePolicy.none)
|
||||||
|
}
|
||||||
|
app.middleware.use(app.sessions.middleware)
|
||||||
|
|
||||||
|
app.databases.use(.sqlite(.file("\(app.directory.resourcesDirectory)/db.sqlite")), as: .sqlite)
|
||||||
|
app.migrations.add(SessionRecord.migration)
|
||||||
|
|
||||||
// Configure Leaf
|
// Configure Leaf
|
||||||
LeafOption.caching = app.environment.isRelease ? .default : .bypass
|
LeafOption.caching = app.environment.isRelease ? .default : .bypass
|
||||||
|
|||||||
7
Sources/App/context.swift
Normal file
7
Sources/App/context.swift
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import Foundation
|
||||||
|
import Vapor
|
||||||
|
|
||||||
|
struct LContext: Codable {
|
||||||
|
let config: Config
|
||||||
|
let loggedIn: Bool
|
||||||
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import Vapor
|
|
||||||
|
|
||||||
struct isItUp: Codable {
|
|
||||||
let isitdown: Bool
|
|
||||||
let response_code: Int
|
|
||||||
}
|
|
||||||
6
Sources/App/login.swift
Normal file
6
Sources/App/login.swift
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import Foundation
|
||||||
|
import Vapor
|
||||||
|
|
||||||
|
struct Login: Codable {
|
||||||
|
let password: String?
|
||||||
|
}
|
||||||
@@ -1,21 +1,104 @@
|
|||||||
import Vapor
|
import Vapor
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import FoundationNetworking
|
||||||
|
import Crypto
|
||||||
import Leaf
|
import Leaf
|
||||||
|
|
||||||
func routes(_ app: Application) throws {
|
func routes(_ app: Application) throws {
|
||||||
// Render home page when root domain is loaded
|
// Render home page when root domain is loaded
|
||||||
app.get { req -> EventLoopFuture<View> in
|
app.get { req -> EventLoopFuture<View> in
|
||||||
|
// Get data from config file at /Resources/config.json
|
||||||
let fileData = try String(contentsOfFile: "\(app.directory.resourcesDirectory)/config.json").data(using: .utf8)
|
let fileData = try String(contentsOfFile: "\(app.directory.resourcesDirectory)/config.json").data(using: .utf8)
|
||||||
|
// Decode JSON file to Config struct
|
||||||
let config: Config = try! JSONDecoder().decode(Config.self, from: fileData!)
|
let config: Config = try! JSONDecoder().decode(Config.self, from: fileData!)
|
||||||
return req.view.render("home", ["config": config])
|
// Check if user is logged in
|
||||||
|
let loginStatus = req.session.data["loggedIn"] ?? "false"
|
||||||
|
// Change loginStatus to a boolean
|
||||||
|
let loginBool = loginStatus == "true" ? true : false
|
||||||
|
// Render home.leaf with config and login status as context
|
||||||
|
return req.view.render("home", LContext(config: config, loggedIn: loginBool))
|
||||||
}
|
}
|
||||||
|
|
||||||
app.get("status", ":url") { req -> EventLoopFuture<[String: String]> in
|
// Return website status when /status/:url is loaded
|
||||||
let url = URL(string: req.parameters.get("url")!)
|
app.get("status", ":url") { req -> [String: String] in
|
||||||
return req.client.get("https://isitdown.site/api/v3/\(url!)").flatMapThrowing { res in
|
// Get URL from request parameters
|
||||||
try res.content.decode(isItUp.self)
|
let url = URL(string: "http://\(req.parameters.get("url")!)")
|
||||||
}.map { json in
|
// Configure URLSession
|
||||||
["down": String(json.isitdown), "code": String(json.response_code)]
|
let config = URLSessionConfiguration.default
|
||||||
|
// Set request timeouts to 5s, then assume offline
|
||||||
|
config.timeoutIntervalForRequest = 5
|
||||||
|
config.timeoutIntervalForResource = 5
|
||||||
|
// Declare statusDict for storing website status
|
||||||
|
var statusDict: [String:String] = [:]
|
||||||
|
// Declare URLSession request using URL from request parameters
|
||||||
|
var headReq = URLRequest(url: url!)
|
||||||
|
// Set HEAD request
|
||||||
|
headReq.httpMethod = "HEAD"
|
||||||
|
// Set request timeout to 5s, then assume offline
|
||||||
|
headReq.timeoutInterval = 5
|
||||||
|
// Create DispatchSemaphore to block thread until request is resolved and status stored
|
||||||
|
let semaphore = DispatchSemaphore(value: 0)
|
||||||
|
// Run Async URLSession dataTask
|
||||||
|
URLSession.shared.dataTask(with: headReq, completionHandler: { (_, response, error) in
|
||||||
|
// If the response is valid
|
||||||
|
if let httpURLResponse = response as? HTTPURLResponse {
|
||||||
|
// Store status code in statusDict
|
||||||
|
statusDict["code"] = String(httpURLResponse.statusCode)
|
||||||
|
// Store website status in statusDict
|
||||||
|
statusDict["down"] = "false"
|
||||||
|
}
|
||||||
|
// Signal DispatchSemaphore to stop blocking
|
||||||
|
semaphore.signal()
|
||||||
|
}).resume()
|
||||||
|
// Wait for semaphore signal
|
||||||
|
semaphore.wait()
|
||||||
|
// If statusDict is empty, return code 0, down true. Otherwise, return statusdict
|
||||||
|
return statusDict.count > 0 ? statusDict : ["code": "0", "down": "true"]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render login page on GET request to /login
|
||||||
|
app.get("login") { req -> EventLoopFuture<View> in
|
||||||
|
// Get data from config file at /Resources/config.json
|
||||||
|
let fileData = try String(contentsOfFile: "\(app.directory.resourcesDirectory)/config.json").data(using: .utf8)
|
||||||
|
// Decode JSON file to Config struct
|
||||||
|
let config: Config = try! JSONDecoder().decode(Config.self, from: fileData!)
|
||||||
|
// Render home.leaf with config as context
|
||||||
|
return req.view.render("login", LContext(config: config, loggedIn: false))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify credentials and log in on POST request to /login
|
||||||
|
app.post("login") { req -> Response in
|
||||||
|
// Decode POST request data into Login struct
|
||||||
|
let data = try req.content.decode(Login.self)
|
||||||
|
// Get data from config file at /Resources/config.json
|
||||||
|
let fileData = try String(contentsOfFile: "\(app.directory.resourcesDirectory)/config.json").data(using: .utf8)
|
||||||
|
// Decode JSON file to Config struct
|
||||||
|
let config: Config = try! JSONDecoder().decode(Config.self, from: fileData!)
|
||||||
|
// Get password from POST request data
|
||||||
|
let loginPassData = data.password?.data(using: .utf8)
|
||||||
|
// Hash password in POST data using SHA256.hash() from SwiftCrypto
|
||||||
|
let loginPassHash = SHA256.hash(data: loginPassData ?? "".data(using: .utf8)!)
|
||||||
|
// Convert hash to string
|
||||||
|
let stringHash = loginPassHash.map { String(format: "%02hhx", $0) }.joined()
|
||||||
|
// If hash in config matches provided hash
|
||||||
|
if stringHash == config.passwordHash {
|
||||||
|
// Set logged in to true in session
|
||||||
|
req.session.data["loggedIn"] = "true"
|
||||||
|
// Redirect back to /
|
||||||
|
return try req.redirect(to: "/")
|
||||||
|
} else {
|
||||||
|
// If hashes do not match, return unauthorized error
|
||||||
|
throw Abort(.unauthorized)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Destroy session on GET request to logout
|
||||||
|
app.get("logout") { req -> Response in
|
||||||
|
// Destroy session
|
||||||
|
req.session.destroy()
|
||||||
|
// Redirect back to /
|
||||||
|
return try req.redirect(to: "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
[[headers]]
|
|
||||||
# Define which paths this specific [[headers]] block will cover.
|
|
||||||
for = "/*"
|
|
||||||
[headers.values]
|
|
||||||
Access-Control-Allow-Origin = "*"
|
|
||||||
Reference in New Issue
Block a user