Compare commits
5 Commits
main
...
production
| Author | SHA1 | Date | |
|---|---|---|---|
| 008205422d | |||
| 9a90ccd909 | |||
| 64f3c29e21 | |||
| 6ff7e75360 | |||
| 4a8072e61b |
@ -5,51 +5,17 @@ namespace App\Http\Controllers;
|
|||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Http\Response;
|
use Illuminate\Http\Response;
|
||||||
use Illuminate\Support\Facades\RateLimiter;
|
|
||||||
|
|
||||||
class AuthController extends Controller
|
class AuthController extends Controller
|
||||||
{
|
{
|
||||||
private int $captchaTtlSeconds = 120;
|
|
||||||
private int $loginDecaySeconds = 60;
|
private int $loginDecaySeconds = 60;
|
||||||
private int $maxLoginAttempts = 10;
|
private int $maxLoginAttempts = 10;
|
||||||
|
|
||||||
private function generateCaptchaCode(int $length = 6): string
|
|
||||||
{
|
|
||||||
// Avoid ambiguous chars: 0,O,1,I,l
|
|
||||||
$chars = '23456789ABCDEFGHJKLMNPQRSTUVWXYZ';
|
|
||||||
$out = '';
|
|
||||||
$max = strlen($chars) - 1;
|
|
||||||
for ($i = 0; $i < $length; $i++) {
|
|
||||||
$out .= $chars[random_int(0, $max)];
|
|
||||||
}
|
|
||||||
return $out;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function refreshCaptcha(): string
|
|
||||||
{
|
|
||||||
$captcha = $this->generateCaptchaCode(6);
|
|
||||||
session([
|
|
||||||
'login_captcha' => $captcha,
|
|
||||||
'login_captcha_created_at' => now()->getTimestamp(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return $captcha;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function ensureCaptchaValid(): void
|
|
||||||
{
|
|
||||||
$createdAt = (int) session('login_captcha_created_at', 0);
|
|
||||||
$expired = $createdAt <= 0 || (now()->getTimestamp() - $createdAt) > $this->captchaTtlSeconds;
|
|
||||||
|
|
||||||
if ($expired || (string) session('login_captcha', '') === '') {
|
|
||||||
$this->refreshCaptcha();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function index(){
|
public function index(){
|
||||||
$this->refreshCaptcha();
|
$captcha = $this->generateCaptchaCode(6);
|
||||||
|
session(['login_captcha' => $captcha]);
|
||||||
$data = [
|
$data = [
|
||||||
'title' => 'Login Admin | Order Gizi'
|
'title' => 'Login Admin | Order Gizi',
|
||||||
];
|
];
|
||||||
return view('auth.index', $data);
|
return view('auth.index', $data);
|
||||||
}
|
}
|
||||||
@ -69,25 +35,30 @@ class AuthController extends Controller
|
|||||||
|
|
||||||
$now = time();
|
$now = time();
|
||||||
$rateKey = 'login:' . $request->ip() . ':' . strtolower((string) $request->input('username'));
|
$rateKey = 'login:' . $request->ip() . ':' . strtolower((string) $request->input('username'));
|
||||||
if (RateLimiter::tooManyAttempts($rateKey, $this->maxLoginAttempts)) {
|
$windowSeconds = 60;
|
||||||
return back()
|
$maxAttempts = 10;
|
||||||
->withInput($request->only('username'))
|
$attempts = (array) $request->session()->get($rateKey, []);
|
||||||
->with(['alertError' => 'rate']);
|
$attempts = array_values(array_filter($attempts, fn($ts) => is_int($ts) && $ts > ($now - $windowSeconds)));
|
||||||
|
if(count($attempts) >= $maxAttempts){
|
||||||
|
return back()->withInput($request->only('username'))
|
||||||
|
->with(['alertError' => 'rate']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$backoffKey = $rateKey . ':backoff_until';
|
||||||
|
$until = (int) $request->session()->get($backoffKey, 0);
|
||||||
|
if($until > $now){
|
||||||
|
return back()->withInput($request->only('username'))
|
||||||
|
->with(['alertError' => 'backoff']);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->ensureCaptchaValid();
|
|
||||||
$expectedCaptcha = (string) session('login_captcha', '');
|
$expectedCaptcha = (string) session('login_captcha', '');
|
||||||
$givenCaptcha = strtoupper(preg_replace('/\s+/', '', (string) $request->input('captcha', '')));
|
$givenCaptcha = strtoupper(preg_replace('/\s+/', '', (string) $request->input('captcha', '')));
|
||||||
if ($expectedCaptcha === '' || !hash_equals(strtoupper($expectedCaptcha), (string) $givenCaptcha)) {
|
if($expectedCaptcha === '' || !hash_equals(strtoupper($expectedCaptcha), (string) $givenCaptcha)){
|
||||||
RateLimiter::hit($rateKey, $this->loginDecaySeconds);
|
return back()->withInput($request->only('username'))
|
||||||
$this->refreshCaptcha();
|
->with(['alertError' => 'captcha']);
|
||||||
return back()
|
|
||||||
->withInput($request->only('username'))
|
|
||||||
->with(['alertError' => 'captcha']);
|
|
||||||
}
|
}
|
||||||
// One-time use
|
|
||||||
$request->session()->forget('login_captcha');
|
$request->session()->forget('login_captcha');
|
||||||
$request->session()->forget('login_captcha_created_at');
|
|
||||||
|
|
||||||
// IMPORTANT: only pass auth credentials to Auth::attempt
|
// IMPORTANT: only pass auth credentials to Auth::attempt
|
||||||
// (do not include captcha / honeypot fields, otherwise Laravel will query non-existent columns)
|
// (do not include captcha / honeypot fields, otherwise Laravel will query non-existent columns)
|
||||||
@ -98,13 +69,18 @@ class AuthController extends Controller
|
|||||||
|
|
||||||
if(Auth::attempt($credentials)){
|
if(Auth::attempt($credentials)){
|
||||||
$request->session()->regenerate();
|
$request->session()->regenerate();
|
||||||
RateLimiter::clear($rateKey);
|
$request->session()->forget($rateKey);
|
||||||
|
$request->session()->forget($backoffKey);
|
||||||
return redirect()->intended('/dashboard');
|
return redirect()->intended('/dashboard');
|
||||||
}
|
}
|
||||||
|
// record failed attempt
|
||||||
|
$attempts[] = $now;
|
||||||
|
$request->session()->put($rateKey, $attempts);
|
||||||
|
|
||||||
RateLimiter::hit($rateKey, $this->loginDecaySeconds);
|
// set exponential backoff (1,2,4,8,16,30 seconds max) based on failures in window
|
||||||
$this->refreshCaptcha();
|
$failCount = count($attempts);
|
||||||
|
$delay = min(30, (int) pow(2, max(0, $failCount - 1)));
|
||||||
|
$request->session()->put($backoffKey, $now + $delay);
|
||||||
return back()->with(['alertError' => 'Gagal Login!']);
|
return back()->with(['alertError' => 'Gagal Login!']);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,27 +92,26 @@ class AuthController extends Controller
|
|||||||
return redirect('/login');
|
return redirect('/login');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function captcha(Request $request){
|
public function captcha(){
|
||||||
$this->ensureCaptchaValid();
|
|
||||||
$captcha = (string) session('login_captcha', '');
|
$captcha = (string) session('login_captcha', '');
|
||||||
|
if($captcha === ''){
|
||||||
if (!function_exists('imagecreatetruecolor')) {
|
$captcha = $this->generateCaptchaCode(6);
|
||||||
return response('GD extension is not available', Response::HTTP_INTERNAL_SERVER_ERROR)
|
session(['login_captcha' => $captcha]);
|
||||||
->header('Content-Type', 'text/plain');
|
}
|
||||||
|
if(!function_exists('imagecreatetruecolor')){
|
||||||
|
return response('GD extension is not available', Response::HTTP_INTERNAL_SERVER_ERROR)->header('Content-Type', 'text/plain');
|
||||||
}
|
}
|
||||||
|
|
||||||
$width = 140;
|
$width=140;
|
||||||
$height = 44;
|
$height=44;
|
||||||
$img = imagecreatetruecolor($width, $height);
|
$img = imagecreatetruecolor($width, $height);
|
||||||
|
|
||||||
$bg = imagecolorallocate($img, 245, 247, 250);
|
$bg = imagecolorallocate($img, 245, 247, 250);
|
||||||
$fg = imagecolorallocate($img, 35, 45, 70);
|
$fg = imagecolorallocate($img, 35, 45, 70);
|
||||||
$noise = imagecolorallocate($img, 120, 130, 150);
|
$noise = imagecolorallocate($img, 120, 130, 150);
|
||||||
|
|
||||||
imagefilledrectangle($img, 0, 0, $width, $height, $bg);
|
imagefilledrectangle($img, 0, 0, $width, $height, $bg);
|
||||||
|
|
||||||
// noise lines
|
for($i = 0; $i < 6; $i++){
|
||||||
for ($i = 0; $i < 6; $i++) {
|
|
||||||
imageline(
|
imageline(
|
||||||
$img,
|
$img,
|
||||||
random_int(0, $width),
|
random_int(0, $width),
|
||||||
@ -147,15 +122,13 @@ class AuthController extends Controller
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// noise dots
|
for($i = 0; $i < 180; $i++){
|
||||||
for ($i = 0; $i < 180; $i++) {
|
|
||||||
imagesetpixel($img, random_int(0, $width - 1), random_int(0, $height - 1), $noise);
|
imagesetpixel($img, random_int(0, $width - 1), random_int(0, $height - 1), $noise);
|
||||||
}
|
}
|
||||||
|
|
||||||
// draw text (built-in font to avoid font dependency)
|
|
||||||
$font = 5;
|
$font = 5;
|
||||||
$textWidth = imagefontwidth($font) * strlen($captcha);
|
$textWidth = imagefontwidth($font) * strlen($captcha);
|
||||||
$textHeight = imagefontheight($font);
|
$textHeight = imagefontwidth($font);
|
||||||
$x = (int) (($width - $textWidth) / 2);
|
$x = (int) (($width - $textWidth) / 2);
|
||||||
$y = (int) (($height - $textHeight) / 2);
|
$y = (int) (($height - $textHeight) / 2);
|
||||||
imagestring($img, $font, $x, $y, $captcha, $fg);
|
imagestring($img, $font, $x, $y, $captcha, $fg);
|
||||||
@ -164,9 +137,17 @@ class AuthController extends Controller
|
|||||||
imagepng($img);
|
imagepng($img);
|
||||||
$png = ob_get_clean();
|
$png = ob_get_clean();
|
||||||
imagedestroy($img);
|
imagedestroy($img);
|
||||||
|
return response($png, 200)->header('Content-Type', 'image/png')->header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
|
||||||
|
}
|
||||||
|
|
||||||
return response($png, 200)
|
public function generateCaptchaCode(int $length = 6): string
|
||||||
->header('Content-Type', 'image/png')
|
{
|
||||||
->header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
|
$chars = '23456789ABCDEFGHJKLMNPQRSTUVWXYZ';
|
||||||
|
$out = '';
|
||||||
|
$max = strlen($chars) - 1;
|
||||||
|
for ($i = 0; $i < $length; $i++){
|
||||||
|
$out .= $chars[random_int(0, $max)];
|
||||||
|
}
|
||||||
|
return $out;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
public/assets/juknis.pptx
Normal file
BIN
public/assets/juknis.pptx
Normal file
Binary file not shown.
@ -47,81 +47,209 @@
|
|||||||
<!--? Config: Mandatory theme config file contain global vars & default theme options, Set your preferred theme option in this file. -->
|
<!--? Config: Mandatory theme config file contain global vars & default theme options, Set your preferred theme option in this file. -->
|
||||||
<script src="{{ ver('/assets/js/config.js') }}"></script>
|
<script src="{{ ver('/assets/js/config.js') }}"></script>
|
||||||
</head>
|
</head>
|
||||||
|
<style>
|
||||||
|
body{
|
||||||
|
background-color: #E2E7E0;
|
||||||
|
}
|
||||||
|
.login-wrapper{
|
||||||
|
min-height: 650px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-inner{
|
||||||
|
height: 490px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-item{
|
||||||
|
height: 490px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-item img{
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
.login-card{
|
||||||
|
min-height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card .card-body{
|
||||||
|
display:flex;
|
||||||
|
flex-direction:column;
|
||||||
|
justify-content:center;
|
||||||
|
}
|
||||||
|
.carousel-indicators{
|
||||||
|
bottom:-35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-indicators button{
|
||||||
|
width:10px !important;
|
||||||
|
height:10px !important;
|
||||||
|
border-radius:50% !important;
|
||||||
|
border:none !important;
|
||||||
|
margin:0 4px !important;
|
||||||
|
background:#adb5bd !important;
|
||||||
|
opacity:1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-indicators .active{
|
||||||
|
background:#212529 !important;
|
||||||
|
transform:scale(1.3);
|
||||||
|
}
|
||||||
|
.carousel-control-prev,
|
||||||
|
.carousel-control-next{
|
||||||
|
width:45px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-control-prev-icon,
|
||||||
|
.carousel-control-next-icon{
|
||||||
|
background-color:#fff;
|
||||||
|
border-radius:50%;
|
||||||
|
padding:18px;
|
||||||
|
box-shadow:0 2px 8px rgba(0,0,0,.15);
|
||||||
|
filter: invert(1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
<body>
|
<body>
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
|
|
||||||
<div class="container-xxl">
|
|
||||||
<div class="authentication-wrapper authentication-basic container-p-y">
|
<div class="container">
|
||||||
<div class="authentication-inner">
|
<div class="row min-vh-100">
|
||||||
<!-- Register -->
|
<div class="col-lg-8 d-none d-lg-block">
|
||||||
<div class="card">
|
|
||||||
<div class="card-body">
|
<div class="text-center pt-5">
|
||||||
<p class="mb-4">Please sign-in to your account</p>
|
<img src="/logo/logo_rsabhk.png"
|
||||||
@if (session()->has('alertError'))
|
class="img-fluid"
|
||||||
<div class="alert alert-danger fw-bold" role="alert">
|
style="max-height:90px;">
|
||||||
@if(session('alertError') === 'captcha')
|
|
||||||
Captcha salah!
|
<h5 class="fw-bold mt-3 mb-2">
|
||||||
@elseif(session('alertError') === 'rate')
|
RUMAH SAKIT ANAK DAN BUNDA HARAPAN KITA
|
||||||
Terlalu banyak percobaan login. Coba lagi dalam 1 menit.
|
</h5>
|
||||||
@elseif(session('alertError') === 'backoff')
|
|
||||||
Mohon tunggu beberapa detik sebelum mencoba lagi.
|
|
||||||
@else
|
|
||||||
Username atau password salah!
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
<form action="/login" class="mb-3" method="POST">
|
|
||||||
@csrf
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="email" class="form-label">Username</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-control"
|
|
||||||
id="username"
|
|
||||||
name="username"
|
|
||||||
placeholder="Enter your username"
|
|
||||||
autofocus
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3 form-password-toggle">
|
|
||||||
<div class="d-flex justify-content-between">
|
<div id="loginCarousel"
|
||||||
<label class="form-label" for="password">Password</label>
|
class="carousel slide"
|
||||||
</div>
|
data-bs-ride="carousel"
|
||||||
<div class="input-group input-group-merge">
|
data-bs-interval="3000">
|
||||||
<input
|
|
||||||
type="password"
|
<!-- Indicator -->
|
||||||
id="password"
|
<div class="carousel-indicators">
|
||||||
class="form-control"
|
|
||||||
name="password"
|
<button type="button"
|
||||||
placeholder="············"
|
data-bs-target="#loginCarousel"
|
||||||
aria-describedby="password"
|
data-bs-slide-to="0"
|
||||||
/>
|
class="active"
|
||||||
<span class="input-group-text cursor-pointer"><i class="bx bx-hide"></i></span>
|
aria-current="true"></button>
|
||||||
</div>
|
|
||||||
</div>
|
<button type="button"
|
||||||
<div class="mb-4">
|
data-bs-target="#loginCarousel"
|
||||||
<label class="form-label">Captcha</label>
|
data-bs-slide-to="1"></button>
|
||||||
<div class="d-flex align-items-center gap-2">
|
|
||||||
<img
|
<button type="button"
|
||||||
src="{{ route('captcha.login') }}?t={{ time() }}"
|
data-bs-target="#loginCarousel"
|
||||||
alt="captcha"
|
data-bs-slide-to="2"></button>
|
||||||
class="border rounded"
|
|
||||||
style="height: 44px; width: 140px; object-fit: cover;"
|
</div>
|
||||||
>
|
|
||||||
<input type="text" name="captcha" class="form-control text-uppercase" placeholder="Masukkan kode di gambar" autocomplete="off" required>
|
<div class="carousel-inner">
|
||||||
<a href="/login" class="btn btn-outline-secondary" title="Refresh captcha">Refresh</a>
|
|
||||||
|
<div class="carousel-item active text-center">
|
||||||
|
<img src="https://iki-sdm.rsabhk.co.id/metronic/assets/media/illustrations/sigma-1/17.png"
|
||||||
|
class="carousel-image">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="carousel-item text-center">
|
||||||
|
<img src="https://iki-sdm.rsabhk.co.id/metronic/assets/media/illustrations/dozzy-1/6.png"
|
||||||
|
class="carousel-image">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="carousel-item text-center">
|
||||||
|
<img src="https://iki-sdm.rsabhk.co.id/metronic/assets/media/illustrations/unitedpalms-1/12.png"
|
||||||
|
class="carousel-image">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4 d-flex flex-column justify-content-center mb-5">
|
||||||
|
<div class="card d-flex flex-row justify-content-center align-items-center p-3 fw-bold mb-3 fs-4 text-primary">
|
||||||
|
<i class="menu-icon tf-icons bx bx-user-circle me-2 fw-bold rounded fs-2"></i>
|
||||||
|
<span>Dashboard Admin GIZI</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card login-card">
|
||||||
|
<div class="card-body">
|
||||||
|
@if (session()->has('alertError'))
|
||||||
|
<div class="alert alert-danger fw-bold" role="alert">
|
||||||
|
@if(session('alertError') === 'rate')
|
||||||
|
Terlalu banyak percobaan login. Coba lagi dalam 1 menit.
|
||||||
|
@elseif(session('alertError') === 'backoff')
|
||||||
|
Mohon tunggu beberapa detik sebelum mencoba lagi.
|
||||||
|
@elseif(session('alertError') === 'captcha')
|
||||||
|
Captcha tidak sesuai
|
||||||
|
@else
|
||||||
|
Username atau password salah!
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
<form action="/login" class="mb-3" method="POST">
|
||||||
|
@csrf
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="email" class="form-label">Username</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="username"
|
||||||
|
name="username"
|
||||||
|
placeholder="Enter your username"
|
||||||
|
autofocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3 form-password-toggle">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<label class="form-label" for="password">Password</label>
|
||||||
|
</div>
|
||||||
|
<div class="input-group input-group-merge">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="password"
|
||||||
|
class="form-control"
|
||||||
|
name="password"
|
||||||
|
placeholder="············"
|
||||||
|
aria-describedby="password"
|
||||||
|
/>
|
||||||
|
<span class="input-group-text cursor-pointer"><i class="bx bx-hide"></i></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label">Captcha</label>
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<img
|
||||||
|
src="{{ route('captcha.login') }}?t={{ time() }}"
|
||||||
|
alt="captcha"
|
||||||
|
class="border rounded"
|
||||||
|
style="height: 44px; width: 140px; object-fit: cover;"
|
||||||
|
>
|
||||||
|
<input type="text" name="captcha" class="form-control text-uppercase" placeholder="Masukkan kode di gambar" autocomplete="off" required>
|
||||||
|
<a href="/login" class="btn btn-outline-secondary" title="Refresh captcha">Refresh</a>
|
||||||
|
</div>
|
||||||
|
<div class="form-text text-muted">Masukkan kode sesuai yang ditampilkan (huruf tidak membedakan kapital/kecil).</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<button class="btn btn-primary d-grid w-100" type="submit">Login</button>
|
||||||
|
</div>
|
||||||
|
<div class="alert alert-info" role="alert">
|
||||||
|
<b>Info Juknis</b> <br/>
|
||||||
|
<a href="/assets/juknis.pptx" class="fw-semibold text-primary"><u>Silahkan klik ini untuk download juknis</u></a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-text text-muted">Masukkan kode sesuai yang ditampilkan (huruf tidak membedakan kapital/kecil).</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<button class="btn btn-primary d-grid w-100" type="submit">Login</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Core JS -->
|
<!-- Core JS -->
|
||||||
@ -133,14 +261,9 @@
|
|||||||
|
|
||||||
<script src="{{ ver('/assets/vendor/js/menu.js') }}"></script>
|
<script src="{{ ver('/assets/vendor/js/menu.js') }}"></script>
|
||||||
<!-- endbuild -->
|
<!-- endbuild -->
|
||||||
|
|
||||||
<!-- Vendors JS -->
|
|
||||||
|
|
||||||
<!-- Main JS -->
|
<!-- Main JS -->
|
||||||
<script src="{{ ver('/assets/js/main.js') }}"></script>
|
<script src="{{ ver('/assets/js/main.js') }}"></script>
|
||||||
|
|
||||||
<!-- Page JS -->
|
|
||||||
|
|
||||||
<!-- Place this tag in your head or just before your close body tag. -->
|
<!-- Place this tag in your head or just before your close body tag. -->
|
||||||
<script async defer src="https://buttons.github.io/buttons.js"></script>
|
<script async defer src="https://buttons.github.io/buttons.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user