initial commit

This commit is contained in:
wt committed 2025-07-13 15:24:13 +07:00
commit 9644786dd4
15 files changed
+595

No files matched your search

+1
View File
@@ -0,0 +1 @@
certs
+57
View File
@@ -0,0 +1,57 @@
package api
import (
"certs/utils"
"encoding/json"
"fmt"
"net/http"
"os"
"github.com/google/uuid"
)
func Certs(w http.ResponseWriter, r *http.Request) {
var certs_info []map[string]any
entires, err := os.ReadDir(os.Getenv("CERTS_PATH"))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
for _, entry := range entires {
if entry.IsDir() {
if entry.Name() == ".git" || entry.Name() == "root" {
continue
}
id, err := uuid.NewRandom()
if err != nil {
fmt.Fprintf(w, "{\"error\":\"%v\"}", err)
return
}
certdata, err := os.ReadFile(fmt.Sprintf("%s/%s/%s.crt", os.Getenv("CERTS_PATH"), entry.Name(), entry.Name()))
if err != nil {
fmt.Fprintf(w, "{\"error\":\"%v\"}", err)
return
}
keydata, err := os.ReadFile(fmt.Sprintf("%s/%s/%s.key", os.Getenv("CERTS_PATH"), entry.Name(), entry.Name()))
if err != nil {
fmt.Fprintf(w, "{\"error\":\"%v\"}", err)
return
}
new_cert := make(map[string]any)
new_cert["id"] = id.String()
new_cert["domain"] = entry.Name()
new_cert["href"] = fmt.Sprintf("https://%s", entry.Name())
new_cert["cert"] = string(certdata)
new_cert["key"] = string(keydata)
new_cert["certinfo"] = utils.ParseCertificate(fmt.Sprintf("%s/%s/%s.crt", os.Getenv("CERTS_PATH"), entry.Name(), entry.Name()))
certs_info = append(certs_info, new_cert)
}
}
jsondata, err := json.Marshal(certs_info)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(jsondata)
}
+6
View File
@@ -0,0 +1,6 @@
@charset "UTF-8";/*!
* Bootstrap v5.3.7 (https://getbootstrap.com/)
* Copyright 2011-2025 The Bootstrap Authors
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/:root,[data-bs-theme=light]{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-black:#000;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-primary-text-emphasis:#052c65;--bs-secondary-text-emphasis:#2b2f32;--bs-success-text-emphasis:#0a3622;--bs-info-text-emphasis:#055160;--bs-warning-text-emphasis:#664d03;--bs-danger-text-emphasis:#58151c;--bs-light-text-emphasis:#495057;--bs-dark-text-emphasis:#495057;--bs-primary-bg-subtle:#cfe2ff;--bs-secondary-bg-subtle:#e2e3e5;--bs-success-bg-subtle:#d1e7dd;--bs-info-bg-subtle:#cff4fc;--bs-warning-bg-subtle:#fff3cd;--bs-danger-bg-subtle:#f8d7da;--bs-light-bg-subtle:#fcfcfd;--bs-dark-bg-subtle:#ced4da;--bs-primary-border-subtle:#9ec5fe;--bs-secondary-border-subtle:#c4c8cb;--bs-success-border-subtle:#a3cfbb;--bs-info-border-subtle:#9eeaf9;--bs-warning-border-subtle:#ffe69c;--bs-danger-border-subtle:#f1aeb5;--bs-light-border-subtle:#e9ecef;--bs-dark-border-subtle:#adb5bd;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-color-rgb:33,37,41;--bs-body-bg:#fff;--bs-body-bg-rgb:255,255,255;--bs-emphasis-color:#000;--bs-emphasis-color-rgb:0,0,0;--bs-secondary-color:rgba(33, 37, 41, 0.75);--bs-secondary-color-rgb:33,37,41;--bs-secondary-bg:#e9ecef;--bs-secondary-bg-rgb:233,236,239;--bs-tertiary-color:rgba(33, 37, 41, 0.5);--bs-tertiary-color-rgb:33,37,41;--bs-tertiary-bg:#f8f9fa;--bs-tertiary-bg-rgb:248,249,250;--bs-heading-color:inherit;--bs-link-color:#0d6efd;--bs-link-color-rgb:13,110,253;--bs-link-decoration:underline;--bs-link-hover-color:#0a58ca;--bs-link-hover-color-rgb:10,88,202;--bs-code-color:#d63384;--bs-highlight-color:#212529;--bs-highlight-bg:#fff3cd;--bs-border-width:1px;--bs-border-style:solid;--bs-border-color:#dee2e6;--bs-border-color-translucent:rgba(0, 0, 0, 0.175);--bs-border-radius:0.375rem;--bs-border-radius-sm:0.25rem;--bs-border-radius-lg:0.5rem;--bs-border-radius-xl:1rem;--bs-border-radius-xxl:2rem;--bs-border-radius-2xl:var(--bs-border-radius-xxl);--bs-border-radius-pill:50rem;--bs-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-box-shadow-sm:0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);--bs-box-shadow-lg:0 1rem 3rem rgba(0, 0, 0, 0.175);--bs-box-shadow-inset:inset 0 1px 2px rgba(0, 0, 0, 0.075);--bs-focus-ring-width:0.25rem;--bs-focus-ring-opacity:0.25;--bs-focus-ring-color:rgba(13, 110, 253, 0.25);--bs-form-valid-color:#198754;--bs-form-valid-border-color:#198754;--bs-form-invalid-color:#dc3545;--bs-form-invalid-border-color:#dc3545}[data-bs-theme=dark]{color-scheme:dark;--bs-body-color:#dee2e6;--bs-body-color-rgb:222,226,230;--bs-body-bg:#212529;--bs-body-bg-rgb:33,37,41;--bs-emphasis-color:#fff;--bs-emphasis-color-rgb:255,255,255;--bs-secondary-color:rgba(222, 226, 230, 0.75);--bs-secondary-color-rgb:222,226,230;--bs-secondary-bg:#343a40;--bs-secondary-bg-rgb:52,58,64;--bs-tertiary-color:rgba(222, 226, 230, 0.5);--bs-tertiary-color-rgb:222,226,230;--bs-tertiary-bg:#2b3035;--bs-tertiary-bg-rgb:43,48,53;--bs-primary-text-emphasis:#6ea8fe;--bs-secondary-text-emphasis:#a7acb1;--bs-success-text-emphasis:#75b798;--bs-info-text-emphasis:#6edff6;--bs-warning-text-emphasis:#ffda6a;--bs-danger-text-emphasis:#ea868f;--bs-light-text-emphasis:#f8f9fa;--bs-dark-text-emphasis:#dee2e6;--bs-primary-bg-subtle:#031633;--bs-secondary-bg-subtle:#161719;--bs-success-bg-subtle:#051b11;--bs-info-bg-subtle:#032830;--bs-warning-bg-subtle:#332701;--bs-danger-bg-subtle:#2c0b0e;--bs-light-bg-subtle:#343a40;--bs-dark-bg-subtle:#1a1d20;--bs-primary-border-subtle:#084298;--bs-secondary-border-subtle:#41464b;--bs-success-border-subtle:#0f5132;--bs-info-border-subtle:#087990;--bs-warning-border-subtle:#997404;--bs-danger-border-subtle:#842029;--bs-light-border-subtle:#495057;--bs-dark-border-subtle:#343a40;--bs-heading-color:inherLine truncated
/*# sourceMappingURL=bootstrap.min.css.map */
+1
View File
@@ -0,0 +1 @@
{"version":3,"sources":["../../scss/mixins/_banner.scss","../../scss/_root.scss","dist/css/bootstrap.css","../../scss/vendor/_rfs.scss","../../scss/mixins/_color-mode.scss","../../scss/_reboot.scss","../../scss/mixins/_border-radius.scss","../../scss/_type.scss","../../scss/mixins/_lists.scss","../../scss/_images.scss","../../scss/mixins/_image.scss","../../scss/_containers.scss","../../scss/mixins/_container.scss","../../scss/mixins/_breakpoints.scss","../../scss/_grid.scss","../../scss/mixins/_grid.scss","../../scss/_tables.scss","../../scss/mixins/_table-variants.scss","../../scss/forms/_labels.scss","../../scss/forms/_form-text.scss","../../scss/forms/_form-control.scss","../../scss/mixins/_transition.scss","../../scss/mixins/_gradients.scss","../../scss/forms/_form-select.scss","../../scss/forms/_form-check.scss","../../scss/forms/_form-range.scss","../../scss/forms/_floating-labels.scss","../../scss/forms/_input-group.scss","../../scss/mixins/_forms.scss","../../scss/_buttons.scss","../../scss/mixins/_buttons.scss","../../scss/_transitions.scss","../../scss/_dropdown.scss","../../scss/mixins/_caret.scss","../../scss/_button-group.scss","../../scss/_nav.scss","../../scss/_navbar.scss","../../scss/_card.scss","../../scss/_accordion.scss","../../scss/_breadcrumb.scss","../../scss/_pagination.scss","../../scss/mixins/_pagination.scss","../../scss/_badge.scss","../../scss/_alert.scss","../../scss/_progress.scss","../../scss/_list-group.scss","../../scss/_close.scss","../../scss/_toasts.scss","../../scss/_modal.scss","../../scss/mixins/_backdrop.scss","../../scss/_tooltip.scss","../../scss/mixins/_reset-text.scss","../../scss/_popover.scss","../../scss/_carousel.scss","../../scss/mixins/_clearfix.scss","../../scss/_spinners.scss","../../scss/_offcanvas.scss","../../scss/_placeholders.scss","../../scss/helpers/_color-bg.scss","../../scss/helpers/_colored-links.scss","../../scss/helpers/_focus-ring.scss","../../scss/helpers/_icon-link.scss","../../scss/helpers/_ratio.scss","../../scss/helpers/_position.scss","../../scss/helpers/_stacks.scss","../../scss/helpers/_visually-hidden.scss","../../scss/mixins/_visually-hidden.scss","../../scss/helpers/_stretched-link.scss","../../scss/helpers/_text-truncation.scss","../../scss/mixins/_text-truncate.scss","../../scss/helpers/_vr.scss","../../scss/mixins/_utilities.scss","../../scss/utilities/_api.scss"],"names":[],"mappings":"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 truncated
+260
View File
@@ -0,0 +1,260 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="css/bootstrap.min.css" />
<script src="js/bootstrap.min.js" defer></script>
<script src="js/theme.js" defer></script>
<script src="js/certs.js" defer></script>
<title>Certs</title>
</head>
<style>
@media screen and (max-width: 1000px) {
#preview_raw_content {
width: 55%;
}
#raw {
width: 85%;
}
}
@media screen and (max-width: 768px) {
#preview_ssl_content {
flex-wrap: wrap;
}
}
</style>
<body>
<nav class="navbar navbar-expand-lg bg-body-tertiary">
<div class="container-fluid">
<a class="navbar-brand" href="#">SSL Certs</a>
<a class="navbar-brand" style="cursor: pointer" id="time"></a>
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarNav"
aria-controls="navbarNav"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto mb-2 mb-lg-0 gap-3">
<li class="nav-item d-flex align-items-center">
<input
type="checkbox"
class="sc-gJwTLC ikxBAC"
id="dark_theme"
/>
</li>
<li class="nav-item">
<button
type="button"
class="btn btn-primary"
data-bs-toggle="modal"
data-bs-target="#create_certificate"
>
+
</button>
</li>
</ul>
</div>
</div>
</nav>
<div
class="d-flex flex-wrap gap-3 p-3 justify-content-center"
id="cards"
></div>
<div class="modal fade" tabindex="-1" id="create_certificate">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Create SSl Certificate</h5>
<button
type="button"
data-bs-dismiss="modal"
aria-label="Close"
class="btn-close"
></button>
</div>
<div class="modal-body">
<div class="form-floating mb-3">
<input
type="text"
id="domain"
class="form-control"
placeholder="example.domain"
/>
<label for="domain">Domain</label>
</div>
<div class="form-floating mb-3">
<input
type="password"
id="password"
class="form-control"
placeholder="example.domain"
/>
<label for="password">RootCA password</label>
</div>
</div>
<div class="modal-footer">
<button
type="button"
data-bs-dismiss="modal"
class="btn btn-secondary"
>
Close
</button>
<button type="button" class="btn btn-primary">
Save changes
</button>
</div>
</div>
</div>
</div>
<div class="modal fade" tabindex="-1" id="preview">
<div class="modal-dialog modal-fullscreen">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Preview</h5>
<button
type="button"
data-bs-dismiss="modal"
aria-label="Close"
class="btn-close"
></button>
</div>
<div
class="modal-body d-flex gap-3 align-items-center justify-content-center"
id="preview_ssl_content"
>
<div id="certinfo">
<h1 id="cert-title">Information</h1>
<h2>Issuer</h2>
<div class="form-floating mb-3">
<input
type="text"
class="form-control"
placeholder="example.domain"
value="placeholder"
id="issuercommonname"
disabled
/>
<label>Common Name</label>
</div>
<div class="form-floating mb-3">
<input
type="text"
class="form-control"
placeholder="example.domain"
value="Organization"
id="issuerorganization"
disabled
/>
<label>Organization</label>
</div>
<div class="form-floating mb-3">
<input
type="text"
class="form-control"
placeholder="example.domain"
value="Organization Unit"
id="issuerorganizationunit"
disabled
/>
<label>Organization Unit</label>
</div>
<h2>Subject</h2>
<div class="form-floating mb-3">
<input
type="text"
class="form-control"
placeholder="example.domain"
value="placeholder"
id="subjectcommonname"
disabled
/>
<label>Common Name</label>
</div>
<div class="form-floating mb-3">
<input
type="text"
class="form-control"
placeholder="example.domain"
value="Organization"
id="subjectorganization"
disabled
/>
<label>Organization</label>
</div>
<div class="form-floating mb-3">
<input
type="text"
class="form-control"
placeholder="example.domain"
value="Organization Unit"
id="subjectorganizationunit"
disabled
/>
<label>Organization Unit</label>
</div>
<h2>Other</h2>
<div class="form-floating mb-3">
<input
type="text"
class="form-control"
placeholder="example.domain"
value="[dns.home.com]"
id="dnsNames"
disabled
/>
<label>DNSNames</label>
</div>
<div class="form-floating mb-3">
<input
type="text"
class="form-control"
placeholder="example.domain"
value="2025-09-28T00:00:00Z"
id="notBefore"
disabled
/>
<label>NotBefore</label>
</div>
<div class="form-floating mb-3">
<input
type="text"
class="form-control"
placeholder="example.domain"
value="2025-09-28T00:00:00Z"
id="notAfter"
disabled
/>
<label>NotAfter</label>
</div>
</div>
<div
class="d-flex flex-column align-items-start justify-content-start"
style="width: 100%; height: 100%"
id="preview_raw_content"
>
<h1>Raw</h1>
<div id="raw"></div>
</div>
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-secondary"
data-bs-dismiss="modal"
>
Close
</button>
</div>
</div>
</div>
</div>
</body>
</html>
+7
View File
@@ -0,0 +1,7 @@
/*!
* Bootstrap v5.3.7 (https://getbootstrap.com/)
* Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("@popperjs/core")):"function"==typeof define&&define.amd?define(["@popperjs/core"],e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e(t.Popper)}(this,(function(t){"use strict";function e(t){const e=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(t)for(const i in t)if("default"!==i){const s=Object.getOwnPropertyDescriptor(t,i);Object.defineProperty(e,i,s.get?s:{enumerable:!0,get:()=>t[i]})}return e.default=t,Object.freeze(e)}const i=e(t),s=new Map,n={set(t,e,i){s.has(t)||s.set(t,new Map);const n=s.get(t);n.has(e)||0===n.size?n.set(e,i):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(n.keys())[0]}.`)},get:(t,e)=>s.has(t)&&s.get(t).get(e)||null,remove(t,e){if(!s.has(t))return;const i=s.get(t);i.delete(e),0===i.size&&s.delete(t)}},o="transitionend",r=t=>(t&&window.CSS&&window.CSS.escape&&(t=t.replace(/#([^\s"#']+)/g,((t,e)=>`#${CSS.escape(e)}`))),t),a=t=>{t.dispatchEvent(new Event(o))},l=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),c=t=>l(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(r(t)):null,h=t=>{if(!l(t)||0===t.getClientRects().length)return!1;const e="visible"===getComputedStyle(t).getPropertyValue("visibility"),i=t.closest("details:not([open])");if(!i)return e;if(i!==t){const e=t.closest("summary");if(e&&e.parentNode!==i)return!1;if(null===e)return!1}return e},d=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),u=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?u(t.parentNode):null},_=()=>{},g=t=>{t.offsetHeight},f=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,m=[],p=()=>"rtl"===document.documentElement.dir,b=t=>{var e;e=()=>{const e=f();if(e){const i=t.NAME,s=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=s,t.jQueryInterface)}},"loading"===document.readyState?(m.length||document.addEventListener("DOMContentLoaded",(()=>{for(const t of m)t()})),m.push(e)):e()},v=(t,e=[],i=t)=>"function"==typeof t?t.call(...e):i,y=(t,e,i=!0)=>{if(!i)return void v(t);const s=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const s=Number.parseFloat(e),n=Number.parseFloat(i);return s||n?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(e)+5;let n=!1;const r=({target:i})=>{i===e&&(n=!0,e.removeEventListener(o,r),v(t))};e.addEventListener(o,r),setTimeout((()=>{n||a(e)}),s)},w=(t,e,i,s)=>{const n=t.length;let o=t.indexOf(e);return-1===o?!i&&s?t[n-1]:t[0]:(o+=i?1:-1,s&&(o=(o+n)%n),t[Math.max(0,Math.min(o,n-1))])},A=/[^.]*(?=\..*)\.|.*/,E=/\..*/,C=/::\d+$/,T={};let k=1;const $={mouseenter:"mouseover",mouseleave:"mouseout"},S=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function L(t,e){return e&&`${e}::${k++}`||t.uidEvent||k++}function O(t){const e=L(t);return t.uidEvent=e,T[e]=T[e]||{},T[e]}function I(t,e,i=null){return Object.values(t).find((t=>t.callable===e&&t.delegationSelector===i))}function D(t,e,i){const s="string"==typeof e,n=s?i:e||i;let o=M(t);return S.has(o)||(o=t),[s,n,o]}function N(t,e,i,s,n){if("string"!=typeof e||!t)return;let[o,r,a]=D(e,i,s);if(e in $){const t=t=>function(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};r=t(r)}const l=O(t),c=l[a]||(l[a]={}),h=I(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&n);const d=L(r,e.replace(A,"")),u=o?function(t,e,i){return function s(n){const o=t.querySelectorAll(e);for(let{target:r}=n;r&&r!==this;r=r.parentNode)for(const a of o)if(a===r)return F(n,{delegateTarget:r}),s.oneOff&&j.off(t,n.type,e,i),i.apply(r,[n])}}(t,i,r):function(t,e){return function i(s){return F(s,{delegateTarget:t}),i.oneOff&&j.off(t,s.type,e),e.apply(t,[s])}}(t,r);u.delegationSelector=o?i:null,u.callable=r,u.oneOff=n,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function P(t,e,i,s,n){const o=I(e[i],s,n);o&&(t.removeEventListener(i,o,Boolean(n)),delete e[i][o.uidEvent])}function x(t,e,i,s){const n=e[i]||{};for(const[o,r]of Object.entries(n))o.includes(Line truncated
//# sourceMappingURL=bootstrap.min.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"names":["elementMap","Map","Data","set","element","key","instance","has","instanceMap","get","size","console","error","Array","from","keys","remove","delete","TRANSITION_END","parseSelector","selector","window","CSS","escape","replace","match","id","triggerTransitionEnd","dispatchEvent","Event","isElement","object","jquery","nodeType","getElement","length","document","querySelector","isVisible","getClientRects","elementIsVisible","getComputedStyle","getPropertyValue","closedDetails","closest","summary","parentNode","isDisabled","Node","ELEMENT_NODE","classList","contains","disabled","hasAttribute","getAttribute","findShadowRoot","documentElement","attachShadow","getRootNode","root","ShadowRoot","noop","reflow","offsetHeight","getjQuery","jQuery","body","DOMContentLoadedCallbacks","isRTL","dir","defineJQueryPlugin","plugin","callback","$","name","NAME","JQUERY_NO_CONFLICT","fn","jQueryInterface","Constructor","noConflict","readyState","addEventListener","push","execute","possibleCallback","args","defaultValue","call","executeAfterTransition","transitionElement","waitForTransition","emulatedDuration","transitionDuration","transitionDelay","floatTransitionDuration","Number","parseFloat","floatTransitionDelay","split","getTransitionDurationFromElement","called","handler","target","removeEventListener","setTimeout","getNextActiveElement","list","activeElement","shouldGetNext","isCycleAllowed","listLength","index","indexOf","Math","max","min","namespaceRegex","stripNameRegex","stripUidRegex","eventRegistry","uidEvent","customEvents","mouseenter","mouseleave","nativeEvents","Set","makeEventUid","uid","getElementEvents","findHandler","events","callable","delegationSelector","Object","values","find","event","normalizeParameters","originalTypeEvent","delegationFunction","isDelegated","typeEvent","getTypeEvent","addHandler","oneOff","wrapFunction","relatedTarget","delegateTarget","this","handlers","previousFunction","domElements","querySelectorAll","domElement","hydrateObj","EventHandler","off","type","apply","bootstrapDelegationHandler","bootstrapHandler","removeHandler","Boolean","removeNamespacedHandlers","namespace","storeElementEvent","handlerKey","entries","includes","on","one","inNamespace","isNamespace","startsWith","elementEvent","slice","keyHandlers","trigger","jQueryEvent","bubbles","nativeDispatch","defaultPrevented","isPropagationStopped","isImmediatePropagationStopped","isDefaultPrevented","evt","cancelable","preventDefault","obj","meta","value","_unused","defineProperty","configurable","normalizeData","toString","JSON","parse","decodeURIComponent","normalizeDataKey","chr","toLowerCase","Manipulator","setDataAttribute","setAttribute","removeDataAttribute","removeAttribute","getDataAttributes","attributes","bsKeys","dataset","filter","pureKey","charAt","getDataAttribute","Config","Default","DefaultType","Error","_getConfig","config","_mergeConfigObj","_configAfterMerge","_typeCheckConfig","jsonConfig","constructor","configTypes","property","expectedTypes","valueType","prototype","RegExp","test","TypeError","toUpperCase","BaseComponent","super","_element","_config","DATA_KEY","dispose","EVENT_KEY","propertyName","getOwnPropertyNames","_queueCallback","isAnimated","getInstance","getOrCreateInstance","VERSION","eventName","getSelector","hrefAttribute","trim","map","sel","join","SelectorEngine","concat","Element","findOne","children","child","matches","parents","ancestor","prev","previous","previousElementSibling","next","nextElementSibling","focusableChildren","focusables","el","getSelectorFromElement","getElementFromSelector","getMultipleElementsFromSelector","enableDismissTrigger","component","method","clickEvent","tagName","EVENT_CLOSE","EVENT_CLOSED","Alert","close","_destroyElement","each","data","undefined","SELECTOR_DATA_TOGGLE","Button","toggle","button","EVENT_TOUCHSTART","EVENT_TOUCHMOVE","EVENT_TOUCHEND","EVENT_POINTERDOWN","EVENT_POINTERUP","endCallback","leftCallback","rightCallback","Swipe","isSupported","_deltaX","_supportPointerEvents","PointerEvent","_initEvents","_start","_eventIsPointerPenTouch","clientX","touches","_end","_handleSwipe","_move","absDeltaX","abs","direction","add","pointerType","navigator","maxTouchPoints","DATA_API_KEY","ARROW_LEFT_KEY","ARROW_RIGHT_KEY","ORDER_NEXT","ORDER_PREV","DIRECTION_LEFT","DIRECTION_RIGHT","EVENT_SLIDE","EVENT_SLID","EVENT_KEYDOWN","EVENT_MOUSEENTER","EVENT_MOUSELEAVE","EVENT_DRAG_START","EVENT_LOAD_DATA_API","EVENT_CLICK_DATA_API","CLASS_NAME_CAROUSEL","CLASS_NAME_ACTIVE","SELECTOR_ACTIVE","SELECTOR_ITEM","SELECTOR_ACTIVE_ITEM","KEY_TO_DIRECTION","ARROW_LEFT_KEY$1","ARROW_RIGHT_KEY$1","interval","keyboard","pause","ride","touch","wrap","Carousel","_interval","_activeElement","_isSliding","touchTimeout","_swipeHelper","_indicatorsElement","_addEventListeners","cycle","_slide","nextWhenVisible","hidden","_clearInterval","_updateInterval","setInterval","_maybeEnableCycle","to","items","_getItems","activeIndex","_getItemIndex","_getActiLine truncated
+117
View File
@@ -0,0 +1,117 @@
const cards = document.querySelector("#cards");
const preview = document.querySelector("#preview");
var certificates = [];
const renderCards = () => {
cards.innerHTML = "";
certificates.map((cert) => {
cards.innerHTML += `<div class="card" style="width: 16rem">
<div class="card-body">
<h5 class="card-title" style="text-align: center">
<a href="${cert.href}">${cert.domain}</a>
</h5>
</div>
<div
class="card-footer d-flex flex-column align-items-center gap-2"
>
<button type="button"
data-bs-toggle="modal"
data-bs-target="#preview"
data-bs-whatever="${cert.cert}"
data-bs-dnsnames="${cert.certinfo.DNSNames}"
data-bs-notafter="${cert.certinfo.NotAfter}"
data-bs-notbefore="${cert.certinfo.NotBefore}"
data-bs-issuer-commonname="${cert.certinfo.Issuer.CommonName}"
data-bs-issuer-organization="${cert.certinfo.Issuer.Organization}"
data-bs-issuer-organizationunit="${cert.certinfo.Issuer.OrganizationalUnit}"
data-bs-subject-commonname="${cert.certinfo.Subject.CommonName}"
data-bs-subject-organization="${cert.certinfo.Subject.Organization}"
data-bs-subject-organizationunit="${cert.certinfo.Subject.OrganizationalUnit}"
class="btn btn-primary"
style="width: 100%"
>
Cert
</button>
<button class="btn btn-primary" style="width: 100%">
Download
</button>
<button class="btn btn-danger delcert" id="${cert.id}" style="width: 100%">
Delete
</button>
</div>
</div>`;
});
};
const getCerts = async () => {
fetch(window.location.origin + "/api/certs")
.then((data) => data.json())
.then((data) => {
certificates = data;
renderCards();
})
.catch((error) => {
console.error("Error fetching certificates:", error);
alert("Failed to fetch certificates");
});
};
getCerts();
if (preview) {
preview.addEventListener("show.bs.modal", (e) => {
const button = e.relatedTarget;
const recipient = button.getAttribute("data-bs-whatever");
// const modalBodyTextarea = preview.querySelector(".modal-body textarea");
const dnsnames = button.getAttribute("data-bs-dnsnames");
const notafter = button.getAttribute("data-bs-notafter");
const notbefore = button.getAttribute("data-bs-notbefore");
const issuerorganizationunit = button.getAttribute(
"data-bs-issuer-organizationunit",
);
const issuerorganization = button.getAttribute(
"data-bs-issuer-organization",
);
const issuercommonname = button.getAttribute("data-bs-issuer-commonname");
const subjectorganizationunit = button.getAttribute(
"data-bs-subject-organizationunit",
);
const subjectorganization = button.getAttribute(
"data-bs-subject-organization",
);
const subjectcommonname = button.getAttribute("data-bs-subject-commonname");
console.log(
dnsnames,
notafter,
notbefore,
issuerorganizationunit,
issuerorganization,
issuercommonname,
);
preview.querySelector("#raw").innerHTML = recipient.replace(/\n/g, "<br>");
preview.querySelector("#dnsNames").value = dnsnames;
preview.querySelector("#notAfter").value = notafter;
preview.querySelector("#notBefore").value = notbefore;
preview.querySelector("#issuerorganizationunit").value =
issuerorganizationunit;
preview.querySelector("#issuerorganization").value = issuerorganization;
preview.querySelector("#issuercommonname").value = issuercommonname;
preview.querySelector("#subjectorganizationunit").value =
subjectorganizationunit;
preview.querySelector("#subjectorganization").value = subjectorganization;
preview.querySelector("#subjectcommonname").value = subjectcommonname;
});
}
cards.addEventListener("click", (e) => {
if (e.target.classList.contains("delcert")) {
certificates.splice(
certificates.indexOf(
certificates.find((item) => item.id === parseInt(e.target.id)),
),
1,
);
renderCards();
}
});
+21
View File
@@ -0,0 +1,21 @@
const dark_theme = document.querySelector("#dark_theme");
dark_theme.addEventListener("click", () => {
if (dark_theme.checked) {
document.documentElement.setAttribute("data-bs-theme", "dark");
localStorage.setItem("data-bs-theme", "dark");
} else {
document.documentElement.setAttribute("data-bs-theme", "light");
localStorage.setItem("data-bs-theme", "light");
}
});
document.addEventListener("DOMContentLoaded", () => {
var theme = localStorage.getItem("data-bs-theme");
if (theme) {
document.documentElement.setAttribute("data-bs-theme", theme);
if (theme === "dark") {
dark_theme.checked = true;
}
}
});
+23
View File
@@ -0,0 +1,23 @@
package api
import (
"embed"
"fmt"
"net/http"
"path"
)
//go:embed frontend/*
var Frontend embed.FS
func SpaHandler(fs http.FileSystem) http.Handler {
fileServer := http.FileServer(fs)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Println("/frontend" + r.URL.Path)
r.URL.Path = "/frontend" + path.Clean(r.URL.Path)
if _, err := fs.Open(path.Clean(r.URL.Path)); err != nil {
r.URL.Path = "/"
}
fileServer.ServeHTTP(w, r)
})
}
+8
View File
@@ -0,0 +1,8 @@
ENABLE_TLS=true
CERT_FILE=/home/tola/certs/localhost/localhost.crt
KEY_FILE=/home/tola/certs/localhost/localhost.key
CERTS_PATH=/home/tola/certs
SERVER_ADDRESS=0.0.0.0
SERVER_PORT=8777
+8
View File
@@ -0,0 +1,8 @@
module certs
go 1.24.5
require (
github.com/google/uuid v1.6.0
github.com/joho/godotenv v1.5.1
)
+4
View File
@@ -0,0 +1,4 @@
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
+40
View File
@@ -0,0 +1,40 @@
package main
import (
"certs/api"
"log"
"net/http"
"os"
"github.com/joho/godotenv"
)
func main() {
err := godotenv.Load("./certs_service.conf")
if err != nil {
log.Fatal("Error loading .env file")
}
http.Handle("/", api.SpaHandler(http.FS(api.Frontend)))
http.HandleFunc("/api/certs", api.Certs)
if os.Getenv("ENABLE_TLS") == "true" {
e := http.ListenAndServeTLS(
os.Getenv("SERVER_ADDRESS")+":"+os.Getenv("SERVER_PORT"),
os.Getenv("CERT_FILE"),
os.Getenv("KEY_FILE"),
nil,
)
if e != nil {
log.Fatal(e)
}
} else {
e := http.ListenAndServe(
os.Getenv("SERVER_ADDRESS")+":"+os.Getenv("SERVER_PORT"),
nil,
)
if e != nil {
log.Fatal(e)
}
}
}
+41
View File
@@ -0,0 +1,41 @@
package utils
import (
"crypto/x509"
"encoding/pem"
"log"
"os"
)
func ParseCertificate(certPath string) map[string]any {
// Чтение PEM-файла
pemData, err := os.ReadFile(certPath)
if err != nil {
log.Fatal("Ошибка чтения файла:", err)
}
// Декодирование PEM-блока
block, _ := pem.Decode(pemData)
if block == nil {
log.Fatal("Не удалось декодировать PEM блок")
}
// Парсинг сертификата
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
log.Fatal("Ошибка парсинга сертификата:", err)
}
res := make(map[string]any)
res["Subject"] = cert.Subject
res["Issuer"] = cert.Issuer
res["SerialNumber"] = cert.SerialNumber
res["NotBefore"] = cert.NotBefore
res["NotAfter"] = cert.NotAfter
res["DNSNames"] = cert.DNSNames
res["KeyUsage"] = cert.KeyUsage
res["SignatureAlgorithm"] = cert.SignatureAlgorithm
// Вывод информации
return res
}