mirror of
https://github.com/mCaptcha/mCaptcha.git
synced 2025-06-07 06:36:42 +00:00
sitekey list
This commit is contained in:
parent
3ac95e1005
commit
98719670df
@ -2,6 +2,6 @@ CREATE TABLE IF NOT EXISTS mcaptcha_config (
|
|||||||
config_id SERIAL PRIMARY KEY NOT NULL,
|
config_id SERIAL PRIMARY KEY NOT NULL,
|
||||||
user_id INTEGER NOT NULL references mcaptcha_users(ID) ON DELETE CASCADE,
|
user_id INTEGER NOT NULL references mcaptcha_users(ID) ON DELETE CASCADE,
|
||||||
key varchar(100) NOT NULL UNIQUE,
|
key varchar(100) NOT NULL UNIQUE,
|
||||||
name varchar(100) DEFAULT NULL,
|
name varchar(100) NOT NULL,
|
||||||
duration integer NOT NULL DEFAULT 30
|
duration integer NOT NULL DEFAULT 30
|
||||||
);
|
);
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
use actix_identity::Identity;
|
use actix_identity::Identity;
|
||||||
use actix_web::{web, HttpResponse, Responder};
|
use actix_web::{web, HttpResponse, Responder};
|
||||||
|
use log::debug;
|
||||||
use m_captcha::{defense::Level, DefenseBuilder};
|
use m_captcha::{defense::Level, DefenseBuilder};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
@ -54,6 +55,7 @@ pub mod routes {
|
|||||||
pub struct AddLevels {
|
pub struct AddLevels {
|
||||||
pub levels: Vec<Level>,
|
pub levels: Vec<Level>,
|
||||||
pub duration: u32,
|
pub duration: u32,
|
||||||
|
pub description: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn services(cfg: &mut web::ServiceConfig) {
|
pub fn services(cfg: &mut web::ServiceConfig) {
|
||||||
@ -104,7 +106,11 @@ async fn add_levels(
|
|||||||
|
|
||||||
defense.build()?;
|
defense.build()?;
|
||||||
|
|
||||||
let mcaptcha_config = add_mcaptcha_util(payload.duration, &data, &id).await?;
|
debug!("creating config");
|
||||||
|
let mcaptcha_config =
|
||||||
|
add_mcaptcha_util(payload.duration, &payload.description, &data, &id).await?;
|
||||||
|
|
||||||
|
debug!("config created");
|
||||||
|
|
||||||
for level in payload.levels.iter() {
|
for level in payload.levels.iter() {
|
||||||
let difficulty_factor = level.difficulty_factor as i32;
|
let difficulty_factor = level.difficulty_factor as i32;
|
||||||
|
@ -83,13 +83,15 @@ pub struct MCaptchaID {
|
|||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
pub struct MCaptchaDetails {
|
pub struct MCaptchaDetails {
|
||||||
pub name: Option<String>,
|
pub name: String,
|
||||||
pub key: String,
|
pub key: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
// this should be called from within add levels
|
// this should be called from within add levels
|
||||||
|
#[inline]
|
||||||
pub async fn add_mcaptcha_util(
|
pub async fn add_mcaptcha_util(
|
||||||
duration: u32,
|
duration: u32,
|
||||||
|
description: &str,
|
||||||
data: &Data,
|
data: &Data,
|
||||||
id: &Identity,
|
id: &Identity,
|
||||||
) -> ServiceResult<MCaptchaDetails> {
|
) -> ServiceResult<MCaptchaDetails> {
|
||||||
@ -102,12 +104,13 @@ pub async fn add_mcaptcha_util(
|
|||||||
key = get_random(32);
|
key = get_random(32);
|
||||||
|
|
||||||
let res = sqlx::query!(
|
let res = sqlx::query!(
|
||||||
"INSERT INTO mcaptcha_config
|
"INSERT INTO mcaptcha_config
|
||||||
(key, user_id, duration)
|
(key, user_id, duration, name)
|
||||||
VALUES ($1, (SELECT ID FROM mcaptcha_users WHERE name = $2), $3)",
|
VALUES ($1, (SELECT ID FROM mcaptcha_users WHERE name = $2), $3, $4)",
|
||||||
&key,
|
&key,
|
||||||
&username,
|
&username,
|
||||||
duration as i32
|
duration as i32,
|
||||||
|
description,
|
||||||
)
|
)
|
||||||
.execute(&data.db)
|
.execute(&data.db)
|
||||||
.await;
|
.await;
|
||||||
@ -125,7 +128,10 @@ pub async fn add_mcaptcha_util(
|
|||||||
Err(e) => Err(e)?,
|
Err(e) => Err(e)?,
|
||||||
|
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
resp = MCaptchaDetails { key, name: None };
|
resp = MCaptchaDetails {
|
||||||
|
key,
|
||||||
|
name: description.to_owned(),
|
||||||
|
};
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -133,10 +139,12 @@ pub async fn add_mcaptcha_util(
|
|||||||
Ok(resp)
|
Ok(resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// this should be called from within add levels
|
// TODO deprecate this
|
||||||
async fn add_mcaptcha(data: web::Data<Data>, id: Identity) -> ServiceResult<impl Responder> {
|
async fn add_mcaptcha(data: web::Data<Data>, id: Identity) -> ServiceResult<impl Responder> {
|
||||||
let duration = 30;
|
let duration = 30;
|
||||||
let resp = add_mcaptcha_util(duration, &data, &id).await?;
|
let description = "dummy";
|
||||||
|
|
||||||
|
let resp = add_mcaptcha_util(duration, description, &data, &id).await?;
|
||||||
Ok(HttpResponse::Ok().json(resp))
|
Ok(HttpResponse::Ok().json(resp))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,9 +24,7 @@ use actix_web::{
|
|||||||
HttpResponse,
|
HttpResponse,
|
||||||
};
|
};
|
||||||
use argon2_creds::errors::CredsError;
|
use argon2_creds::errors::CredsError;
|
||||||
//use awc::error::SendRequestError;
|
|
||||||
use derive_more::{Display, Error};
|
use derive_more::{Display, Error};
|
||||||
use lazy_static::lazy_static;
|
|
||||||
use m_captcha::errors::CaptchaError;
|
use m_captcha::errors::CaptchaError;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use url::ParseError;
|
use url::ParseError;
|
||||||
|
@ -19,25 +19,92 @@ use actix_identity::Identity;
|
|||||||
use actix_web::{web, HttpResponse, Responder};
|
use actix_web::{web, HttpResponse, Responder};
|
||||||
use sailfish::TemplateOnce;
|
use sailfish::TemplateOnce;
|
||||||
|
|
||||||
//use crate::api::v1::mcaptcha::mcaptcha::MCaptchaDetails;
|
use crate::api::v1::mcaptcha::mcaptcha::MCaptchaDetails;
|
||||||
use crate::errors::*;
|
use crate::errors::*;
|
||||||
use crate::Data;
|
use crate::Data;
|
||||||
|
|
||||||
#[derive(TemplateOnce, Clone)]
|
#[derive(TemplateOnce, Clone)]
|
||||||
#[template(path = "panel/site-keys/index.html")]
|
#[template(path = "panel/site-keys/index.html")]
|
||||||
pub struct IndexPage;
|
pub struct IndexPage {
|
||||||
|
sitekeys: SiteKeys,
|
||||||
|
}
|
||||||
|
|
||||||
const PAGE: &str = "SiteKeys";
|
const PAGE: &str = "SiteKeys";
|
||||||
|
|
||||||
impl Default for IndexPage {
|
impl IndexPage {
|
||||||
fn default() -> Self {
|
fn new(sitekeys: SiteKeys) -> Self {
|
||||||
IndexPage
|
IndexPage { sitekeys }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_sitekeys(data: web::Data<Data>, id: Identity) -> PageResult<impl Responder> {
|
pub async fn list_sitekeys(data: web::Data<Data>, id: Identity) -> PageResult<impl Responder> {
|
||||||
let body = IndexPage::default().render_once().unwrap();
|
let username = id.identity().unwrap();
|
||||||
|
let res = sqlx::query_as!(
|
||||||
|
MCaptchaDetails,
|
||||||
|
"SELECT key, name from mcaptcha_config WHERE
|
||||||
|
user_id = (SELECT ID FROM mcaptcha_users WHERE name = $1) ",
|
||||||
|
&username,
|
||||||
|
)
|
||||||
|
.fetch_all(&data.db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let body = IndexPage::new(res).render_once().unwrap();
|
||||||
Ok(HttpResponse::Ok()
|
Ok(HttpResponse::Ok()
|
||||||
.content_type("text/html; charset=utf-8")
|
.content_type("text/html; charset=utf-8")
|
||||||
.body(body))
|
.body(body))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SiteKeys = Vec<MCaptchaDetails>;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use actix_web::http::StatusCode;
|
||||||
|
use actix_web::test;
|
||||||
|
use actix_web::web::Bytes;
|
||||||
|
|
||||||
|
use crate::tests::*;
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn list_sitekeys_work() {
|
||||||
|
const NAME: &str = "listsitekeyuser";
|
||||||
|
const PASSWORD: &str = "longpassworddomain";
|
||||||
|
const EMAIL: &str = "listsitekeyuser@a.com";
|
||||||
|
|
||||||
|
{
|
||||||
|
let data = Data::new().await;
|
||||||
|
delete_user(NAME, &data).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
register_and_signin(NAME, EMAIL, PASSWORD).await;
|
||||||
|
let (data, _, signin_resp, key) = add_levels_util(NAME, PASSWORD).await;
|
||||||
|
let cookies = get_cookie!(signin_resp);
|
||||||
|
|
||||||
|
let mut app = get_app!(data).await;
|
||||||
|
|
||||||
|
let list_sitekey_resp = test::call_service(
|
||||||
|
&mut app,
|
||||||
|
test::TestRequest::get()
|
||||||
|
.uri(PAGES.panel.sitekey.list)
|
||||||
|
.cookie(cookies.clone())
|
||||||
|
.to_request(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(list_sitekey_resp.status(), StatusCode::OK);
|
||||||
|
|
||||||
|
let body: Bytes = test::read_body(list_sitekey_resp).await;
|
||||||
|
let body = String::from_utf8(body.to_vec()).unwrap();
|
||||||
|
|
||||||
|
// Bytes::from(key.key)
|
||||||
|
// .iter()
|
||||||
|
// .for_each(|e| assert!(body.contains(e)));
|
||||||
|
//
|
||||||
|
// Bytes::from(key.name)
|
||||||
|
// .iter()
|
||||||
|
// .for_each(|e| assert!(body.contains(e)));
|
||||||
|
|
||||||
|
assert!(body.contains(&key.key));
|
||||||
|
assert!(body.contains(&key.name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -22,13 +22,15 @@ pub mod routes {
|
|||||||
pub struct Sitekey {
|
pub struct Sitekey {
|
||||||
pub list: &'static str,
|
pub list: &'static str,
|
||||||
pub add: &'static str,
|
pub add: &'static str,
|
||||||
|
pub view: &'static str,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Sitekey {
|
impl Sitekey {
|
||||||
pub const fn new() -> Self {
|
pub const fn new() -> Self {
|
||||||
Sitekey {
|
Sitekey {
|
||||||
list: "/sitekey",
|
list: "/sitekey/list",
|
||||||
add: "/sitekey/add",
|
add: "/sitekey/add",
|
||||||
|
view: "/sitekey/{key}/view",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,6 +57,7 @@ macro_rules! get_app {
|
|||||||
))
|
))
|
||||||
.configure(crate::api::v1::pow::services)
|
.configure(crate::api::v1::pow::services)
|
||||||
.configure(crate::api::v1::services)
|
.configure(crate::api::v1::services)
|
||||||
|
.configure(crate::pages::services)
|
||||||
.data($data.clone()),
|
.data($data.clone()),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
@ -189,6 +190,7 @@ pub async fn add_levels_util(
|
|||||||
let add_level = AddLevels {
|
let add_level = AddLevels {
|
||||||
levels: levels.clone(),
|
levels: levels.clone(),
|
||||||
duration: 30,
|
duration: 30,
|
||||||
|
description: "dummy".into(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// 1. add level
|
// 1. add level
|
||||||
|
@ -24,3 +24,4 @@ $secondary-backdrop: #2b2c30;
|
|||||||
$light-grey: rgba(0, 0, 0, 0.125);
|
$light-grey: rgba(0, 0, 0, 0.125);
|
||||||
$white: #fff;
|
$white: #fff;
|
||||||
$form-content-width: 90%;
|
$form-content-width: 90%;
|
||||||
|
$black-text: #000;
|
||||||
|
40
templates/components/_box.scss
Normal file
40
templates/components/_box.scss
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@import '../vars';
|
||||||
|
|
||||||
|
@mixin box-title {
|
||||||
|
padding-left: 10px;
|
||||||
|
font-size: 1rem;
|
||||||
|
padding: 0.75rem 1.25rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
text-align: left;
|
||||||
|
width: 100%;
|
||||||
|
border-bottom: 0.1px solid $light-grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin box {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 90%;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
box-sizing: content-box;
|
||||||
|
background-color: $white;
|
||||||
|
margin: auto;
|
||||||
|
padding-bottom: 30px;
|
||||||
|
}
|
@ -1,12 +1,9 @@
|
|||||||
<. include!("../components/headers.html"); .>
|
<. include!("../components/headers.html"); .>
|
||||||
<main>
|
<div class="inner-container">
|
||||||
<div class="inner-container">
|
<div class="error-box">
|
||||||
<div class="error-box">
|
<h1 class="error-title"><.= title .></h1>
|
||||||
<h1 class="error-title"><.= title .></h1>
|
<p class="error-message"><.= message .></p>
|
||||||
<p class="error-message"><.= message .></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- end of container -->
|
</div>
|
||||||
</main>
|
<!-- end of container -->
|
||||||
|
|
||||||
<. include!("../components/footers.html"); .>
|
<. include!("../components/footers.html"); .>
|
||||||
|
@ -17,27 +17,14 @@
|
|||||||
|
|
||||||
@import '../reset';
|
@import '../reset';
|
||||||
@import '../vars';
|
@import '../vars';
|
||||||
|
@import '../components/box';
|
||||||
|
|
||||||
.error-box {
|
.error-box {
|
||||||
display: flex;
|
@include box;
|
||||||
flex-direction: column;
|
|
||||||
width: 90%;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
box-sizing: content-box;
|
|
||||||
background-color: $white;
|
|
||||||
margin: auto;
|
|
||||||
padding-bottom: 30px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-title {
|
.error-title {
|
||||||
padding-left: 10px;
|
@include box-title;
|
||||||
font-size: 1rem;
|
|
||||||
padding: 0.75rem 1.25rem;
|
|
||||||
box-sizing: border-box;
|
|
||||||
text-align: left;
|
|
||||||
width: 100%;
|
|
||||||
border-bottom: 0.1px solid $light-grey;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-message {
|
.error-message {
|
||||||
|
@ -24,14 +24,14 @@ import * as addSiteKey from './panel/add-site-key/';
|
|||||||
|
|
||||||
import VIEWS from './views/v1/routes';
|
import VIEWS from './views/v1/routes';
|
||||||
|
|
||||||
|
|
||||||
import './auth/forms.scss';
|
import './auth/forms.scss';
|
||||||
import './panel/main.scss';
|
import './panel/main.scss';
|
||||||
import './panel/header/sidebar/main.scss';
|
import './panel/header/sidebar/main.scss';
|
||||||
import './panel/taskbar/main.scss';
|
import './panel/taskbar/main.scss';
|
||||||
import './panel/help-banner/main.scss';
|
import './panel/help-banner/main.scss';
|
||||||
import './panel/add-site-key/main.scss';
|
import './panel/add-site-key/main.scss';
|
||||||
|
import './panel/site-keys/main.scss';
|
||||||
|
import './errors/main.scss';
|
||||||
|
|
||||||
const router = new Router();
|
const router = new Router();
|
||||||
|
|
||||||
|
@ -53,6 +53,7 @@ const validateDescription = (e: Event) => {
|
|||||||
const val = inputElement.value;
|
const val = inputElement.value;
|
||||||
const filed = 'Description';
|
const filed = 'Description';
|
||||||
isBlankString(val, filed, e);
|
isBlankString(val, filed, e);
|
||||||
|
return val;
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateDuration = (e: Event) => {
|
const validateDuration = (e: Event) => {
|
||||||
@ -71,7 +72,7 @@ const validateDuration = (e: Event) => {
|
|||||||
const submit = async (e: Event) => {
|
const submit = async (e: Event) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
validateDescription(e);
|
const description = validateDescription(e);
|
||||||
const duration = validateDuration(e);
|
const duration = validateDuration(e);
|
||||||
|
|
||||||
const formUrl = getFormUrl(FORM);
|
const formUrl = getFormUrl(FORM);
|
||||||
@ -82,6 +83,7 @@ const submit = async (e: Event) => {
|
|||||||
const payload = {
|
const payload = {
|
||||||
levels: levels,
|
levels: levels,
|
||||||
duration,
|
duration,
|
||||||
|
description,
|
||||||
};
|
};
|
||||||
|
|
||||||
console.debug(`[form submition] json payload: ${JSON.stringify(payload)}`);
|
console.debug(`[form submition] json payload: ${JSON.stringify(payload)}`);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<. include!("../../components/headers.html"); .> <. include!("../header/index.html");
|
<. include!("../../components/headers.html"); .> <.
|
||||||
.>
|
include!("../header/index.html"); .>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<. include!("../taskbar/index.html"); .> <.
|
<. include!("../taskbar/index.html"); .> <.
|
||||||
@ -7,7 +7,23 @@
|
|||||||
<!-- Main content container -->
|
<!-- Main content container -->
|
||||||
<div class="inner-container">
|
<div class="inner-container">
|
||||||
<!-- Main menu/ important actions roaster -->
|
<!-- Main menu/ important actions roaster -->
|
||||||
|
<ul class="sitekey-list__box">
|
||||||
|
<h1 class="sitekey-list__title">Your Sitekeys</h1>
|
||||||
|
<. for sitekey in sitekeys.iter() { .>
|
||||||
|
<a href="/sitekey/<.= sitekey.key .>/view" class="sitekey-list__item-container">
|
||||||
|
<li class="sitekey-list__item">
|
||||||
|
|
||||||
|
<span class="sitekey-list__name">
|
||||||
|
<.= sitekey.name .>
|
||||||
|
</span>
|
||||||
|
<span class="sitekey-list__key">
|
||||||
|
<.= sitekey.key .>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<. } .>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<!-- end of container -->
|
<!-- end of container -->
|
||||||
</main>
|
</main>
|
||||||
|
55
templates/panel/site-keys/main.scss
Normal file
55
templates/panel/site-keys/main.scss
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@import '../../reset';
|
||||||
|
@import '../../vars';
|
||||||
|
@import '../../components/box';
|
||||||
|
|
||||||
|
.sitekey-list__box {
|
||||||
|
@include box;
|
||||||
|
padding-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sitekey-list__title {
|
||||||
|
@include box-title;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sitekey-list__item-container {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-bottom: 0.1px solid $light-grey;
|
||||||
|
padding: 20px;
|
||||||
|
color: $black-text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sitekey-list__item {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sitekey-list__item-container:hover {
|
||||||
|
background-color: $light-grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sitekey-list__name {
|
||||||
|
flex: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sitekey-list__key {
|
||||||
|
flex: 1;
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user