Compare commits
3 Commits
873eaad79c
...
b1b3d21d1a
Author | SHA1 | Date |
---|---|---|
Joe Bellus | b1b3d21d1a | |
Joe Bellus | 6d9d863039 | |
Joe Bellus | 04c2f37b60 |
|
@ -0,0 +1,3 @@
|
|||
|
||||
ALTER TABLE application ADD COLUMN favicon Boolean DEFAULT false;
|
||||
ALTER TABLE bookmark ADD COLUMN favicon Boolean DEFAULT false;
|
|
@ -15,6 +15,7 @@ struct ApiApplication {
|
|||
pub application_category_id: Option<i32>,
|
||||
pub enable_healthcheck: bool,
|
||||
pub healthcheck_status: Option<bool>,
|
||||
pub favicon: bool,
|
||||
}
|
||||
|
||||
impl From<application::Model> for ApiApplication {
|
||||
|
@ -29,6 +30,7 @@ impl From<application::Model> for ApiApplication {
|
|||
application_category_id: model.application_category_id,
|
||||
enable_healthcheck: model.enable_healthcheck,
|
||||
healthcheck_status: None,
|
||||
favicon: model.favicon,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -65,6 +67,7 @@ pub async fn new_application(
|
|||
glyph: Set(data.0.glyph),
|
||||
application_category_id: Set(data.0.application_category_id),
|
||||
enable_healthcheck: Set(data.0.enable_healthcheck),
|
||||
favicon: Set(data.0.favicon),
|
||||
};
|
||||
let app = model.insert(&state.db).await?;
|
||||
Ok(HttpResponse::Ok().json(app))
|
||||
|
@ -106,6 +109,7 @@ pub async fn update_applications(
|
|||
application_category_id: Set(data.application_category_id),
|
||||
glyph: Set(data.glyph),
|
||||
enable_healthcheck: Set(data.enable_healthcheck),
|
||||
favicon: Set(data.favicon),
|
||||
};
|
||||
let model = ret.update(&state.db).await?;
|
||||
Ok(HttpResponse::Ok().json(model))
|
||||
|
@ -210,6 +214,7 @@ mod tests {
|
|||
active: true,
|
||||
application_category_id: None,
|
||||
enable_healthcheck: false,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let state = setup_state().await?;
|
||||
|
|
|
@ -49,7 +49,7 @@ pub struct UpdatePasswordRequest {
|
|||
}
|
||||
|
||||
#[instrument]
|
||||
#[post("password")]
|
||||
#[put("password")]
|
||||
pub async fn update_password(
|
||||
state: web::Data<AppState>,
|
||||
req: web::Json<UpdatePasswordRequest>,
|
||||
|
|
|
@ -15,6 +15,7 @@ pub struct Model {
|
|||
pub glyph: Option<String>,
|
||||
pub application_category_id: Option<i32>,
|
||||
pub enable_healthcheck: bool,
|
||||
pub favicon: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter)]
|
||||
|
@ -52,6 +53,7 @@ impl Default for Model {
|
|||
glyph: Default::default(),
|
||||
application_category_id: Default::default(),
|
||||
enable_healthcheck: false,
|
||||
favicon: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -73,7 +73,6 @@ impl Handler<Event> for EventBroker {
|
|||
type Result = ();
|
||||
|
||||
fn handle(&mut self, msg: Event, _ctx: &mut Self::Context) -> Self::Result {
|
||||
tracing::info!("Event received");
|
||||
for (_, ses) in self.sessions.iter() {
|
||||
let _ = ses.addr.do_send(msg.clone());
|
||||
}
|
||||
|
|
17
src/main.rs
17
src/main.rs
|
@ -6,7 +6,6 @@ use actix_web::{
|
|||
web::{self, Data},
|
||||
App, HttpResponse, HttpServer,
|
||||
};
|
||||
use clap::crate_version;
|
||||
use rust_embed::RustEmbed;
|
||||
use sea_orm::{prelude::*, Database};
|
||||
use tokio::sync::Mutex;
|
||||
|
@ -100,6 +99,13 @@ async fn main() {
|
|||
.await
|
||||
{
|
||||
Ok(res) if res.status() == 200 => {
|
||||
if !st
|
||||
.healthcheck_status
|
||||
.lock()
|
||||
.await
|
||||
.get(&app.id)
|
||||
.unwrap_or(&false)
|
||||
{
|
||||
st.healthcheck_status.lock().await.insert(app.id, true);
|
||||
let _ = events::EventBroker::from_registry()
|
||||
.send(events::Event::HealthcheckChange {
|
||||
|
@ -108,6 +114,7 @@ async fn main() {
|
|||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("Error performing healthcheck: {}", e);
|
||||
st.healthcheck_status.lock().await.insert(app.id, false);
|
||||
|
@ -119,6 +126,13 @@ async fn main() {
|
|||
.await;
|
||||
}
|
||||
Ok(res) => {
|
||||
if *st
|
||||
.healthcheck_status
|
||||
.lock()
|
||||
.await
|
||||
.get(&app.id)
|
||||
.unwrap_or(&true)
|
||||
{
|
||||
tracing::warn!("Non 200 status code: {}", res.status());
|
||||
st.healthcheck_status.lock().await.insert(app.id, false);
|
||||
let _ = events::EventBroker::from_registry()
|
||||
|
@ -130,6 +144,7 @@ async fn main() {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(60)).await;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -12,7 +12,8 @@
|
|||
<text-field v-model="app.appName" label="Name" required />
|
||||
<text-field v-model="app.description" label="Description" />
|
||||
<text-field v-model="app.url" label="URL" required />
|
||||
<icon-picker v-model="app.glyph" />
|
||||
<switch-field v-model="app.favicon" label="Use favicon" />
|
||||
<icon-picker v-model="app.glyph" v-if="!app.favicon" />
|
||||
<switch-field v-model="app.enableHealthcheck" label="Health check" />
|
||||
</form>
|
||||
</template>
|
||||
|
@ -84,6 +85,7 @@ export default {
|
|||
let resp = await axios.post("/api/applications", {
|
||||
active: true,
|
||||
enableHealthcheck: !!this.app.enableHealthcheck,
|
||||
favicon: !!this.app.favicon,
|
||||
...this.app,
|
||||
});
|
||||
if (resp.status == 200) {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div :class="{tile: true, alive: appData.healthcheckStatus === true, dead: appData.healthcheckStatus === false}" @click="click">
|
||||
<font-awesome-icon v-if="appData.glyph" :icon="appData.glyph" size="2x" />
|
||||
<font-awesome-icon v-if="!appData.glyph && appData.healthcheckStatus === false" icon="ban" size="2x" />
|
||||
<font-awesome-icon v-if="appData.glyph && !appData.favicon" :icon="appData.glyph" size="2x" />
|
||||
<img :src="favicon" v-if="appData.favicon" width="30" height="30"/>
|
||||
<div class="label">
|
||||
<div class="title">{{appData.appName}}</div>
|
||||
<div class="description">{{appData.description}}</div>
|
||||
|
@ -12,6 +12,16 @@
|
|||
<script>
|
||||
export default {
|
||||
props: ["appData"],
|
||||
computed: {
|
||||
favicon() {
|
||||
try {
|
||||
const url = new URL(this.appData.url);
|
||||
return `${url.protocol}${url.hostname}/favicon.ico`;
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
click(e) {
|
||||
this.$emit("clicked", e, this.appData);
|
||||
|
@ -19,7 +29,7 @@ export default {
|
|||
},
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
<style scoped>
|
||||
.tile {
|
||||
transition: all 0.5s;
|
||||
padding: 16px 25px;
|
||||
|
@ -40,7 +50,7 @@ export default {
|
|||
flex: 1;
|
||||
text-align: left;
|
||||
}
|
||||
svg {
|
||||
svg, img {
|
||||
color: #fff;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
@ -52,7 +62,7 @@ svg {
|
|||
.tile.alive svg {
|
||||
color: #009900;
|
||||
}
|
||||
.tile.dead svg {
|
||||
.tile.dead svg {
|
||||
color: #900;
|
||||
}
|
||||
.title {
|
||||
|
@ -65,6 +75,7 @@ svg {
|
|||
color: #ccc;
|
||||
font-size: 0.8em;
|
||||
line-height: 1;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
|
||||
|
@ -78,4 +89,7 @@ svg {
|
|||
.lightMode .tile:hover {
|
||||
background-color: rgba(200,200,200, 0.5);
|
||||
}
|
||||
.tile.dead {
|
||||
background: rgba(50,0,0,0.4);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<template>
|
||||
<div class="bookmark-tile" @click="click">
|
||||
<font-awesome-icon icon="external-link-alt"/>
|
||||
<font-awesome-icon v-if="!showFavIcon" icon="external-link-alt" size="2x"/>
|
||||
<img v-if="showFavIcon" :src="favicon" @error="faviconError"/>
|
||||
{{bookmark.bookmarkName}}
|
||||
</div>
|
||||
</template>
|
||||
|
@ -8,19 +9,45 @@
|
|||
<script>
|
||||
export default {
|
||||
props: ["bookmark"],
|
||||
data() {
|
||||
return {
|
||||
showFavIcon: true,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
favicon() {
|
||||
try {
|
||||
const url = new URL(this.bookmark.url);
|
||||
return `${url.protocol}${url.hostname}/favicon.ico`;
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
click() {
|
||||
this.$emit("clicked", this.bookmark);
|
||||
},
|
||||
faviconError() {
|
||||
this.showFavIcon = false;
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
<style scoped>
|
||||
.bookmark-tile {
|
||||
padding: 5px 25px;
|
||||
border-radius: 5px;
|
||||
transition: all 0.2s;
|
||||
line-height: 20px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.bookmark-tile img, .bookmark-tile svg {
|
||||
margin-right: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.bookmark-tile, .bookmark-tile svg {
|
||||
|
@ -42,6 +69,4 @@ export default {
|
|||
background: rgba(255,255,255,0.25);
|
||||
color: #222;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
|
|
@ -14,9 +14,34 @@ export default {
|
|||
</script>
|
||||
|
||||
<style scoped>
|
||||
svg {
|
||||
color: #fff;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.tile.new {
|
||||
transition: all 0.5s;
|
||||
padding: 16px 25px;
|
||||
border-radius: 5px;
|
||||
margin: 5px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
background: rgba(0,0,0,0.8);
|
||||
}
|
||||
.label {
|
||||
font-weight: 900;
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
}
|
||||
.title {
|
||||
font-weight: bold;
|
||||
}
|
||||
.tile:hover {
|
||||
transition: all 0.1s;
|
||||
box-shadow: rgba(0, 0, 0, 0.5) 0px 20px 30px -10px;
|
||||
}
|
||||
.lightMode .tile.new {
|
||||
background: rgba(255,255,255,0.8);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<form-field>
|
||||
<label v-if="label" :class="{error: isError}">{{label}}</label>
|
||||
<input :value="modelValue" ref="field" @input="handleInput" :type="inputType" :class="{error: isError}" />
|
||||
<input :placeholder="placeholder" :value="modelValue" ref="field" @input="handleInput" :type="inputType" :class="{error: isError}" />
|
||||
</form-field>
|
||||
</template>
|
||||
|
||||
|
@ -9,6 +9,7 @@
|
|||
import FormField from "./FormField.vue";
|
||||
export default {
|
||||
props: {
|
||||
placeholder: String,
|
||||
required: Boolean,
|
||||
label: String,
|
||||
modelValue: String,
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
<div class="login-wrapper">
|
||||
<form @submit.prevent="goLogin">
|
||||
<panel>
|
||||
<input v-model="password" type="password" autofocus />
|
||||
<div class="error" v-if="error">{{error}}</div>
|
||||
<input v-model="password" type="password" autofocus @input="resetError"/>
|
||||
<button @clicked="goLogin" type="submit">Authenticate</button>
|
||||
</panel>
|
||||
</form>
|
||||
|
@ -21,15 +22,23 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
password: "",
|
||||
error: ""
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
resetError() {
|
||||
this.error = "";
|
||||
},
|
||||
async goLogin() {
|
||||
const response = await axios.post("/api/authorize", {
|
||||
password: this.password,
|
||||
});
|
||||
if (response.status == 200) {
|
||||
localStorage.setItem("token", response.data.token);
|
||||
this.$router.push("/");
|
||||
} else {
|
||||
this.error = "Could not authenticate";
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -71,4 +80,13 @@ button:hover {
|
|||
box-shadow: rgba(0, 0, 0, 0.25) 0px 14px 28px,
|
||||
rgba(0, 0, 0, 0.22) 0px 10px 10px;
|
||||
}
|
||||
.error {
|
||||
color: #f33;
|
||||
background: #311;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 10px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
</style>>
|
||||
|
|
|
@ -16,7 +16,17 @@
|
|||
<btn label="Update" @click="updateMode" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<h1>Password</h1>
|
||||
<h2>Change password</h2>
|
||||
<text-field v-model="password" password placeholder="password" />
|
||||
<text-field v-model="confirmPassword" password placeholder="confirm password" />
|
||||
<div class="actions">
|
||||
<btn :disabled="password !== confirmPassword || !password.length" label="Change Password" @click="updatePassword" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-buttons">
|
||||
<btn danger label="Logout" @click="logout" />
|
||||
<btn primary label="Back to Dashboard" @click="backClicked" />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -26,13 +36,16 @@
|
|||
import Btn from "../components/Button.vue";
|
||||
import SwitchField from "../components/Switch.vue";
|
||||
import axios from "axios";
|
||||
import TextField from '../components/TextField.vue';
|
||||
|
||||
export default {
|
||||
components: { Btn, SwitchField },
|
||||
components: { Btn, SwitchField, TextField },
|
||||
data() {
|
||||
return {
|
||||
file: "",
|
||||
darkMode: true,
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
|
@ -51,8 +64,20 @@ export default {
|
|||
mode: this.darkMode ? "dark" : "light"
|
||||
});
|
||||
},
|
||||
async updatePassword() {
|
||||
const res = await axios.put("/api/password", {
|
||||
password: this.password,
|
||||
});
|
||||
if (res.status === 200) {
|
||||
this.$router.push("/login");
|
||||
}
|
||||
},
|
||||
backClicked() {
|
||||
this.$router.push("/");
|
||||
},
|
||||
logout() {
|
||||
localStorage.removeItem("token");
|
||||
this.$router.push("/login");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -96,4 +121,7 @@ export default {
|
|||
margin-top: 15px;
|
||||
text-align: right;
|
||||
}
|
||||
.control-buttons button + button {
|
||||
margin-left: 15px;
|
||||
}
|
||||
</style>
|
||||
|
|
Loading…
Reference in New Issue