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