Commit 4a2e4617 authored by Dominik Hebeler's avatar Dominik Hebeler
Browse files

Merge branch '1122-implement-method-for-changing-the-member-key' into 'development'

Resolve "Implement Method for Changing the member key"

Closes #1122

See merge request !1856
parents c02ab818 d4842cd6
......@@ -6,37 +6,31 @@ use Cookie;
use Illuminate\Http\Request;
use LaravelLocalization;
use \App\Models\Key;
use \Carbon\Carbon;
use Validator;
class KeyController extends Controller
{
public function index(Request $request)
{
$redirUrl = $request->input('redirUrl', "");
$cookie = Cookie::get('key');
$key = $request->input('keyToSet', '');
if (empty($key) && empty($cookie)) {
$key = 'enter_key_here';
} elseif (empty($key) && !empty($cookie)) {
$key = $cookie;
} elseif (!empty($key)) {
$key = $request->input('key');
}
// How many Ad Free searches should a user get max when he creates a new key
const KEYCHANGE_ADFREE_SEARCHES = 150;
public function index(\App\Models\Key $key, Request $request)
{
$cookieLink = LaravelLocalization::getLocalizedURL(LaravelLocalization::getCurrentLocale(), route('loadSettings', Cookie::get()));
return view('key')
->with('title', trans('titles.key'))
->with('cookie', $key)
->with('keystatus', $key->getStatus())
->with('cookie', $key->key)
->with('cookieLink', $cookieLink);
}
public function setKey(Request $request)
{
$redirUrl = $request->input('redirUrl', "");
$keyToSet = $request->input('keyToSet');
$key = new Key($request->input('keyToSet', ''));
if ($key->getStatus()) {
$status = $key->getStatus();
if ($status !== null) {
# Valid Key
$host = $request->header("X_Forwarded_Host", "");
if (empty($host)) {
......@@ -46,10 +40,7 @@ class KeyController extends Controller
$settings = Cookie::get();
$settings['key'] = $keyToSet;
$cookieLink = LaravelLocalization::getLocalizedURL(LaravelLocalization::getCurrentLocale(), route('loadSettings', $settings));
return view('key')
->with('title', trans('titles.key'))
->with('cookie', $keyToSet)
->with('cookieLink', $cookieLink);
return redirect(LaravelLocalization::getLocalizedURL(LaravelLocalization::getCurrentLocale(), route('keyindex')));
} else {
$cookieLink = LaravelLocalization::getLocalizedURL(LaravelLocalization::getCurrentLocale(), route('loadSettings', Cookie::get()));
return view('key')
......@@ -74,4 +65,152 @@ class KeyController extends Controller
return redirect($url);
}
}
public function changeKeyIndex(\App\Models\Key $key, Request $request){
if(!$key->canChange()){
return redirect(LaravelLocalization::getLocalizedURL(LaravelLocalization::getCurrentLocale(), route('keyindex')));
}
return view('keychange', [
"title" => trans('titles.keychange'),
"key" => $key->key,
"css" => [mix('css/keychange/index.css')]
]);
}
public function removeCurrent(\App\Models\Key $key, Request $request){
if(!$key->canChange()){
return redirect(LaravelLocalization::getLocalizedURL(LaravelLocalization::getCurrentLocale(), route('keyindex')));
}
// Reduce Current Key
$res = $key->reduce(self::KEYCHANGE_ADFREE_SEARCHES);
if(empty($res) || empty($res->status) || $res->status !== "success"){
return redirect(LaravelLocalization::getLocalizedURL(LaravelLocalization::getCurrentLocale(), route('keyindex')));
}
// Redirect to Cookie Remove URL with redirect to step two
$validUntil = Carbon::now("Europe/London")->addDays(2);
$format = "Y-m-d H:i:s";
$data = [
"validUntil" => $validUntil->format($format),
"password" => hash_hmac("sha256", $validUntil->format($format), env("APP_KEY", "WEAK_KEY")),
];
$targetUrl = LaravelLocalization::getLocalizedURL(LaravelLocalization::getCurrentLocale(), route('changeKeyTwo', $data));
$redirUrl = LaravelLocalization::getLocalizedURL(LaravelLocalization::getCurrentLocale(), route('removeCookie', [
"ir" => $targetUrl
]));
return redirect($redirUrl);
}
public function generateNew(\App\Models\Key $key, Request $request){
// Validate Request Data
$validUntil = $request->input('validUntil', '');
$password = $request->input('password', '');
$format = "Y-m-d H:i:s";
// Check if Validuntil
$valid = true;
if(empty($validUntil)){
$valid = false;
}else{
$validUntil = Carbon::createFromFormat($format, $validUntil, "Europe/London");
if(!$validUntil){
$valid = false;
}
}
if($valid && Carbon::now()->diffInSeconds($validUntil) <= 0){
$valid = false;
}
if($valid){
// Check if hash matches
$expectedHash = hash_hmac("sha256", $validUntil->format($format), env("APP_KEY", "WEAK_KEY"));
if(!hash_equals($expectedHash, $password)){
$valid = false;
}
}
if(!$valid){
return redirect(LaravelLocalization::getLocalizedURL(LaravelLocalization::getCurrentLocale(), route('keyindex')));
}
// Check if the key already was generated
if (!$key->checkForChange("", $password)) {
return redirect(LaravelLocalization::getLocalizedURL(LaravelLocalization::getCurrentLocale(), route('keyindex')));
}
return view('keychangetwo', [
"title" => trans('titles.keychange'),
"validUntil" => $validUntil,
"css" => [mix('css/keychange/index.css')]
]);
}
public function generateNewPost(\App\Models\Key $key, Request $request){
// Validate Request Data
$validUntil = $request->input('validUntil', '');
$password = $request->input('password', '');
$format = "Y-m-d H:i:s";
// Check if Validuntil
$valid = true;
if(empty($validUntil)){
$valid = false;
}else{
$validUntil = Carbon::createFromFormat($format, $validUntil, "Europe/London");
if(!$validUntil){
$valid = false;
}
}
if($valid && Carbon::now()->diffInSeconds($validUntil) <= 0){
$valid = false;
}
if($valid){
// Check if hash matches
$expectedHash = hash_hmac("sha256", $validUntil->format($format), env("APP_KEY", "WEAK_KEY"));
if(!hash_equals($expectedHash, $password)){
$valid = false;
}
}
if(!$valid){
return redirect(LaravelLocalization::getLocalizedURL(LaravelLocalization::getCurrentLocale(), route('keyindex')));
}
$validator = Validator::make($request->all(), [
'newkey' => 'required|min:4|max:20',
]);
if($validator->fails()) {
$data = [
"validUntil" => $validUntil->format($format),
"password" => hash_hmac("sha256", $validUntil->format($format), env("APP_KEY", "WEAK_KEY")),
"newkey" => $request->input('newkey', ''),
];
$targetUrl = LaravelLocalization::getLocalizedURL(LaravelLocalization::getCurrentLocale(), route('changeKeyTwo', $data));
return redirect($targetUrl);
}
$newkey = $request->input('newkey', '');
$characters = '0123456789abcdefghijklmnopqrstuvwxyz';
$randomSuffix = "";
$suffixCount = 3;
for($i = 0; $i < $suffixCount; $i++){
$randomSuffix .= $characters[rand(0, strlen($characters)-1)];
}
$newkey = $newkey . $randomSuffix;
if($key->checkForChange($newkey, $password)){
$result = $key->generateKey(null, self::KEYCHANGE_ADFREE_SEARCHES, $newkey, "Schlüssel gewechselt. Hash $password");
if(!empty($result)){
Cookie::queue('key', $result, 525600, '/', null, false, false);
return redirect(LaravelLocalization::getLocalizedURL(LaravelLocalization::getCurrentLocale(), route('changeKeyThree', ["newkey" => $result])));
}
}
$data = [
"validUntil" => $validUntil->format($format),
"password" => hash_hmac("sha256", $validUntil->format($format), env("APP_KEY", "WEAK_KEY")),
];
$targetUrl = LaravelLocalization::getLocalizedURL(LaravelLocalization::getCurrentLocale(), route('changeKeyTwo', $data));
return redirect($targetUrl);
}
}
......@@ -220,7 +220,7 @@ class MailController extends Controller
$betrag = round($betrag, 2, PHP_ROUND_HALF_DOWN);
# Generating personalised key for donor
$key = app('App\Models\Key')->generateKey($betrag);
$key = app('App\Models\Key')->generateKey($betrag, null, null, 'Für ' . $betrag . '€ aufgeladen am '. date("d.m.Y"));
try {
$postdata = [
......
......@@ -17,7 +17,7 @@ class RemoveKey
public function handle($request, Closure $next)
{
// Check if a wrong Key Cookie is set and if so remove it
if(Cookie::has("key") && !app('App\Models\Key')->getStatus()){
if(Cookie::has("key") && app('App\Models\Key')->getStatus() === null){
return redirect(route("removeCookie", ["ir" => url()->full()]));
}
return $next($request);
......
......@@ -1502,7 +1502,6 @@ class MetaGer
$logEntry .= " time=" . round((microtime(true) - $this->starttime), 2) . " serv=" . $this->fokus;
$logEntry .= " interface=" . LaravelLocalization::getCurrentLocale();
$logEntry .= " sprachfilter=" . $this->lang;
$logEntry .= " key=" . $this->apiKey;
$logEntry .= " eingabe=" . $this->eingabe;
$logEntry = preg_replace("/\n+/", " ", $logEntry);
......
......@@ -4,12 +4,14 @@ namespace App\Models;
use Illuminate\Support\Facades\Redis;
use Request;
use \Carbon\Carbon;
class Key
{
public $key;
public $status; # valid key = true, invalid key = false, unidentified key = null
public $status; # Null If Key invalid | false if valid but has no adFreeSearches | true if valid and has adFreeSearches
private $keyserver = "https://key.metager.de/";
private $keyinfo;
public function __construct($key, $status = null)
{
......@@ -25,35 +27,29 @@ class Key
{
if ($this->key !== '' && $this->status === null) {
$this->updateStatus();
if(empty($this->status)){
if($this->status === null){
// The user provided an invalid key which we will log to fail2ban
$fail2banEnabled = config("metager.metager.fail2ban_enabled");
if(empty($fail2banEnabled) || !$fail2banEnabled || !env("fail2banurl", false) || !env("fail2banuser") || !env("fail2banpassword")){
return false;
if (!empty($fail2banEnabled) && $fail2banEnabled && !empty(env("fail2banurl", false)) && !empty(env("fail2banuser")) && !empty(env("fail2banpassword"))) {
// Submit fetch job to worker
$mission = [
"resulthash" => "captcha",
"url" => env("fail2banurl") . "/mgkeytry/",
"useragent" => "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:81.0) Gecko/20100101 Firefox/81.0",
"username" => env("fail2banuser"),
"password" => env("fail2banpassword"),
"headers" => [
"ip" => Request::ip()
],
"cacheDuration" => 0,
"name" => "Captcha",
];
$mission = json_encode($mission);
Redis::rpush(\App\MetaGer::FETCHQUEUE_KEY, $mission);
}
// Submit fetch job to worker
$mission = [
"resulthash" => "captcha",
"url" => env("fail2banurl") . "/mgkeytry/",
"useragent" => "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:81.0) Gecko/20100101 Firefox/81.0",
"username" => env("fail2banuser"),
"password" => env("fail2banpassword"),
"headers" => [
"ip" => Request::ip()
],
"cacheDuration" => 0,
"name" => "Captcha",
];
$mission = json_encode($mission);
Redis::rpush(\App\MetaGer::FETCHQUEUE_KEY, $mission);
}
}
if ($this->status === null || $this->status === false) {
return false;
} else {
return true;
}
return $this->status;
}
public function updateStatus()
......@@ -71,14 +67,15 @@ class Key
try {
$link = $this->keyserver . "v2/key/". urlencode($this->key);
$result = json_decode(file_get_contents($link, false, $context));
if ($result->{'apiAccess'} == 'unlimited') {
$this->status = true;
return true;
} else if ($result->{'apiAccess'} == 'normal' && $result->{'adFreeSearches'} > 0){
$this->status = true;
if(!empty($result)){
$this->keyinfo = $result;
if($this->keyinfo->adFreeSearches > 0 || $this->keyinfo->apiAccess === "unlimited"){
$this->status = true;
}else{
$this->status = false;
}
return true;
} else {
$this->status = false;
}else{
return false;
}
} catch (\ErrorException $e) {
......@@ -118,15 +115,26 @@ class Key
return false;
}
}
public function generateKey($payment)
public function generateKey($payment = null, $adFreeSearches = null, $key = null, $notes = "")
{
$authKey = base64_encode(env("KEY_USER", "test") . ':' . env("KEY_PASSWORD", "test"));
$postdata = http_build_query(array(
'payment' => $payment,
$postdata = array(
'apiAccess' => 'normal',
'notes' => 'Fuer ' . $payment . '€ aufgeladen am '. date("d.m.Y"),
'expiresAfterDays' => 365
));
'expiresAfterDays' => 365,
'notes' => $notes
);
if(!empty($key)){
$postdata["key"] = $key;
}
if(!empty($payment)){
$postdata["payment"] = $payment;
}else if(!empty($adFreeSearches)){
$postdata["adFreeSearches"] = $adFreeSearches;
}else{
return false;
}
$postdata = http_build_query($postdata, "", "&", PHP_QUERY_RFC3986);
$opts = array(
'http' => array(
'method' => 'POST',
......@@ -149,4 +157,89 @@ class Key
return false;
}
}
public function reduce($count){
$authKey = base64_encode(env("KEY_USER", "test") . ':' . env("KEY_PASSWORD", "test"));
$postdata = http_build_query(array(
'adFreeSearches' => $count,
));
$opts = array(
'http' => array(
'method' => 'POST',
'header' => [
'Content-type: application/x-www-form-urlencoded',
'Authorization: Basic ' . $authKey
],
'content' => $postdata,
'timeout' => 5
),
);
$context = stream_context_create($opts);
try {
$link = $this->keyserver . "v2/key/" . $this->key . "/reduce-searches";
$result = json_decode(file_get_contents($link, false, $context));
return $result;
} catch (\ErrorException $e) {
return false;
}
}
/**
* Tells if this key is liable to change to a custom key
* Currently only members are allowed to do so and only every 2 days
* Also only the original member key is allowed to be changed
*
* @return boolean
*/
public function canChange(){
if(empty($this->status) || !preg_match("/^Mitgliederschlüssel\./", $this->keyinfo->notes) || $this->keyinfo->adFreeSearches < \App\Http\Controllers\KeyController::KEYCHANGE_ADFREE_SEARCHES){
return false;
}
if(!empty($this->keyinfo->KeyChangedAt)){
// "2021-03-09T09:19:44.000Z"
$keyChangedAt = Carbon::createFromTimeString($this->keyinfo->KeyChangedAt, 'Europe/London');
if($keyChangedAt->diffInSeconds(Carbon::now()) > (2 * 24 * 60 * 60)){
return true;
}else{
return false;
}
}
return true;
}
public function checkForChange($newkey = "", $hash){
$authKey = base64_encode(env("KEY_USER", "test") . ':' . env("KEY_PASSWORD", "test"));
$postdata = http_build_query(array(
'hash' => $hash,
'key' => $newkey,
));
$opts = array(
'http' => array(
'method' => 'POST',
'header' => [
'Content-type: application/x-www-form-urlencoded',
'Authorization: Basic ' . $authKey
],
'content' => $postdata,
'timeout' => 5
),
);
$context = stream_context_create($opts);
try {
$link = $this->keyserver . "v2/key/can-change";
$result = json_decode(file_get_contents($link, false, $context));
if(!empty($result) && $result->status === "success" && empty($result->results)){
return true;
}else{
return false;
}
} catch (\ErrorException $e) {
return false;
}
}
}
......@@ -4,7 +4,7 @@ return [
'h1' => "Schlüssel für Ihre werbefreie Suche",
'p1' => 'MetaGer bietet <a href=":url1">SUMA-EV Mitgliedern</a> und großzügigen <a href=":url2">Spendern</a> einen Schlüssel an, mit dem sie Zugriff auf ein Kontingent an werbefreien Suchen haben.',
'p2' => 'Auf dieser Seite können Sie Ihren Schlüssel (sofern bekannt) eingeben. Wir speichern diesen mit Hilfe eines Cookies auf Ihrem PC. Auf diese Weise sendet Ihr Browser den Schlüssel automatisch bei jeder durchgeführten Suche an uns, sodass wir die Werbung für Sie entfernen können.',
'p3' => 'Wenn Sie sich den Cookie anschauen steht dort drin "key=xxxx". Wir verwenden diesen dementsprechend nicht für Tracking-Zwecke. Er wird auch zu keinem Zeitpunkt in irgendeiner Form von uns gespeichert oder geloggt.',
'p3' => 'Wenn Sie sich den Cookie anschauen steht dort drin "key=xxxx". Er wird zu keinem Zeitpunkt in irgendeiner Form von uns gespeichert oder geloggt. Wir verwenden diesen insbesondere auch nicht für Tracking-Zwecke',
'p4' => 'Wichtig: Um diese Funktion nutzen zu können, müssen Sie Cookies in Ihrem Browser zugelassen haben. Die Einstellung bleibt dann solange gespeichert, wie Ihr Browser Cookies speichert.',
'p5' => 'Um den Schlüssel darüber hinausgehend speichern zu können haben Sie folgende Möglichkeiten:',
'li1' => 'Richten Sie sich folgenden Link als Startseite/Lesezeichen ein:',
......@@ -12,5 +12,9 @@ return [
'placeholder1' => 'Schlüssel eingeben...',
'removeKey' => 'aktuellen Schlüssel entfernen',
'invalidKey' => 'Der eingegebene Schlüssel ist ungültig',
'empty' => 'Ihr Schlüssel ist zwar gültig, enthält aber keine werbefreien Suchen mehr.',
'backLink' => 'Zurück zur letzten Seite',
'custom.h3' => 'Wunsch-Schlüssel',
'custom.p1' => 'Mitglieder des SUMA-EV haben die Möglichkeit, sich einen eigenen Schlüssel auszusuchen.',
'custom.a1' => 'Wunsch Schlüssel einrichten'
];
<?php
return [
'h1' => 'Wunsch Schlüssel',
'p1' => 'Mit diesem Tool haben Sie die Möglichkeit Ihren aktuellen Mitgliederschlüssel ":key" zu wechseln. Mitgliederschlüssel sind an Ihre Mitgliedschaft im SUMA-EV gekoppelt. So kann dieser automatisch erneuert werden. Auch wenn wir das nicht tun, hätten wir dadurch natürlich theoretisch die Möglichkeit eine Verknüpfung zwischen Mitgliederschlüssel und durchgeführten Suchen zu schaffen. Mit dem Wechsel auf einen Wunsch Schlüssel wird eine solche Verknüpfung theoretisch und praktisch für uns unmöglich.',
'p2' => 'Außerdem können Sie mit diesem Tool einen Schlüssel erstellen, der leichter zu merken ist. Bitte heben Sie sich den ursprünglichen Schlüssel dennoch auf. Dadurch, dass der neue Schlüssel nicht mehr automatisch erneuert werden kann, wird dieser irgendwann ungültig. In dem Fall müssen Sie diesen Prozess wiederholen, indem Sie Ihren ursprünglichen Mitgliederschlüssel (:key) bei MetaGer eingeben und dieses Tool erneut aufrufen.',
'p3' => 'Heben Sie sich deshalb beide Schlüssel gut auf und beachten Sie insbesondere, dass wir nicht in der Lage sind Ihnen den neuen, durch diesen Wechsel entstandenen, Schlüssel bei Verlust mitzuteilen oder wiederherzustellen.',
'p4' => 'Dieses Tool führt Sie durch folgende Schritte um einen neuen anonymen Wunsch Schlüssel zu erstellen:',
'ol1.li1' => 'Entfernen aller Informationen über den aktuellen Mitgliederschlüssel aus Ihrem Browser',
'ol1.li2' => 'Erzeugen einer URL mit der ein neuer Schlüssel erstellt werden kann. Diese URL enthält keine Informationen mehr über den Mitgliederschlüssel.',
'ol1.li3' => 'Erzeugen eines neuen Wunsch Schlüssels',
'ol1.li4' => 'Speichern des neuen Wunsch Schlüssels im Browser',
'a1' => 'Gelesen - Wunsch Schlüssel erstellen',
'p5' => 'Ihr bisheriger Mitgliederschlüssel wurde nun aus dem Browser gelöscht. Außerdem haben wir eine URL erzeugt, mit der Sie sich Ihren Wunschschlüssel erstellen können. Dies ist innerhalb von :validUntil jederzeit und von jedem Gerät aus möglich indem Sie folgende URL im Browser aufrufen, oder direkt das Formular auf dieser Seite verwenden.',
'p6' => 'In das nachfolgende Textfeld können, Sie nun Ihren gewünschten Schlüssel eintragen. Um Überschneidungen zu vermeiden fügen wir noch ein paar Zeichen an den Schlüssel an. Mit dem Klick auf generieren wird Ihr neuer Schlüssel dann erzeugt und auf der folgenden Seite angezeigt, damit Sie Ihn sich abspeichern können.',
'input1label' => 'Wunsch Schlüssel eintragen (Mindestens 4 und maximal 20 Zeichen)',
'input1' => 'Wunsch Schlüssel eintragen',
'button1' => 'Wunsch Schlüssel generieren',
'p7' => ' Ihr neuer Wunsch Schlüssel wurde erfolgreich eingerichtet. Bitte merken Sie ihn sich gut. Er kann bei Verlust nämlich nicht wiederhergestellt werden. Er wird außerdem irgendwann ungültig werden. Wenn der Fall eintritt, tragen Sie bei MetaGer bitte Ihren regulären Mitgliederschlüssel ein und wiederholen Sie diesen Vorgang.',
'p8' => 'Ihr neuer Schlüssel lautet:',
];
\ No newline at end of file
......@@ -24,6 +24,7 @@ return [
'asso' => 'Assoziator - MetaGer',
'plugin' => 'Plugin - MetaGer',
'key' => 'Mitgliederschlüssel - MetaGer',
'keychange' => 'Mitgliederschlüssel wechseln',
'settings' => 'Sucheinstellungen (:fokus)',
'allSettings' => 'Alle Sucheinstellungen - MetaGer',
'transparency' => 'Transparenzerklärung - MetaGer',
......
<?php
return [
'h1' => 'Favorite Key',
'p1' => 'With this tool you have the possibility to change your current membership key ":key". Membership keys are linked to your membership in SUMA-EV. So it can be renewed automatically. Even if we don\'t do that, we would of course theoretically have the possibility of creating a link between the membership key and the searches carried out. With the change to a desired key, such a link is theoretically and practically impossible for us.',
'p2' => 'Also, you can use this tool to create a key that is easier to remember. Please keep the original key anyway. Because the new key can no longer be renewed automatically, it will eventually become invalid. In that case you will have to repeat this process by entering your original membership key (:key) into MetaGer and calling up this tool again.',
'p3' => 'You should therefore keep both keys in a safe place and pay particular attention to the fact that we are not able to restore the new one if it is lost.',
'p4' => 'This tool guides you through the following steps to create a new anonymous favorite key:',
'ol1.li1' => 'Remove all information about the current member key from your browser',
'ol1.li2' => 'Generating a URL with which a new key can be created. This URL no longer contains information about the member key.',
'ol1.li3' => 'Generate a new favorite key',
'ol1.li4' => 'Save the new favorite key in the browser',
'a1' => 'Read - create a favorite key',
'p5' => 'Your previous membership key has now been deleted from the browser. We have also created a URL that you can use to create your desired key. This is possible within :validUntil at any time and from any device by calling up the following URL in the browser, or directly using the form on this page.',
'p6' => 'You can now enter your desired key in the following text field. To avoid overlapping, we add a few more characters to the key. By clicking on generate, your new key will be generated and displayed on the following page so that you can save it.',
'input1label' => 'Enter the desired key (at least 4 and a maximum of 20 characters)',
'input1' => 'Enter the desired key',
'button1' => 'Generate favorite key',
'p7' => 'Your new desired key has been successfully set up. Please make a note of it. It cannot be restored if it is lost. It will also expire at some point. If this occurs, please enter your regular membership key at MetaGer and repeat this process.',
'p8' => 'Your new key is:',
];
\ No newline at end of file
......@@ -8,7 +8,7 @@
.card-medium;
margin: 8px 0;
.error {
color: red;
color: #fd5757;
text-align: center;
font-weight: bold;
}
......
......@@ -24,4 +24,14 @@
text-align: center;
margin-top: 16px;
}
#remove-key {
margin-top: 8px;
}
#enter-key-form {
display: flex;
justify-content: center;
gap: 4px;
}
}
\ No newline at end of file
#steps {
display: flex;
justify-content: space-between;
padding: 16px;
counter-reset: steps;
> div {
counter-increment: steps;
padding: 8px;
&::before {
content: counter(steps)". ";
}
&.active {
background-color: #2879025c;
}
}
}
\ No newline at end of file
......@@ -19,12 +19,13 @@
<p>{{ trans('key.p2') }}</p>
<p>{{ trans('key.p3') }}</p>
<p>{{ trans('key.p4') }}</p>
@if(isset($keystatus) && $keystatus !== null)
<p>{{ trans('key.p5') }}</p>
<ol>
<li>
@lang ('key.li1')
<div class="copyLink">