initial commit
commit
db2a1abc57
@ -0,0 +1,6 @@
|
||||
/frontend/.astro
|
||||
/frontend/dist
|
||||
/frontend/node_modules
|
||||
/frontend/notes
|
||||
/todo
|
||||
/FluxDNS
|
@ -0,0 +1,15 @@
|
||||
{
|
||||
"ipv4_address": "343.234.234",
|
||||
"ipv6_address": "f333:3443:ffff:ffff",
|
||||
"ddns_entries": [
|
||||
{
|
||||
"record": "test.example.com",
|
||||
"updated": false,
|
||||
"provider": "Cloudflare",
|
||||
"provider_data": {
|
||||
"api_token": "dsaufhdskjlfhsdjakfhsdjkfhsk",
|
||||
"zone_id": "dsafjhkl340903490320_"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,228 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
// Standard
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Database
|
||||
type database struct {
|
||||
path string
|
||||
mutex sync.Mutex
|
||||
data databaseData
|
||||
}
|
||||
|
||||
// Database Data
|
||||
type databaseData struct {
|
||||
IPv4Address string `json:"ipv4_address"`
|
||||
IPv6Address string `json:"ipv6_address"`
|
||||
DDNSEntries []DatabaseDataDDNSEntry `json:"ddns_entries"`
|
||||
}
|
||||
|
||||
// Database Data DDNS Entry
|
||||
type DatabaseDataDDNSEntry struct {
|
||||
Record string `json:"record"`
|
||||
Updated bool `json:"updated"`
|
||||
Provider string `json:"provider"`
|
||||
ProviderData interface{} `json:"provider_data"`
|
||||
}
|
||||
|
||||
// Database Data DDNS Entry Provider Data Cloudflare
|
||||
type DatabaseDataDDNSEntryProviderDataCloudflare struct {
|
||||
APIToken string `json:"api_token"`
|
||||
ZoneID string `json:"zone_id"`
|
||||
}
|
||||
|
||||
// Save
|
||||
func (db *database) save() error {
|
||||
// Marshal Data
|
||||
jsonData, err := json.MarshalIndent(db.data, "", " ")
|
||||
// Handle Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Save Data
|
||||
return os.WriteFile(db.path, jsonData, 0644)
|
||||
}
|
||||
|
||||
// Load
|
||||
func (db *database) load() error {
|
||||
// Load Data
|
||||
jsonData, err := os.ReadFile(db.path)
|
||||
// Handle Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Unmarshal Data
|
||||
err = json.Unmarshal(jsonData, &db.data)
|
||||
// Handle Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Unmarshal DDNS Entries
|
||||
for entryNumber, entry := range db.data.DDNSEntries {
|
||||
switch entry.Provider {
|
||||
// Cloudflare
|
||||
case "Cloudflare":
|
||||
// Entry Data
|
||||
var entryData DatabaseDataDDNSEntryProviderDataCloudflare
|
||||
// Marshal Entry Data
|
||||
data, err := json.Marshal(entry.ProviderData)
|
||||
// Handle Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Unmarshal Entry Data
|
||||
err = json.Unmarshal(data, &entryData)
|
||||
// Handle Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Set Provider Data
|
||||
db.data.DDNSEntries[entryNumber].ProviderData = entryData
|
||||
// Default
|
||||
default:
|
||||
return errors.New("unknown ddns entry provider")
|
||||
}
|
||||
}
|
||||
// Success
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create
|
||||
func Create(path string) *database {
|
||||
// Database
|
||||
db := database{
|
||||
path: path,
|
||||
mutex: sync.Mutex{},
|
||||
data: databaseData{},
|
||||
}
|
||||
// Check File
|
||||
_, err := os.Stat(db.path)
|
||||
// File Does Not Exist
|
||||
if os.IsNotExist(err) {
|
||||
// Create File
|
||||
file, err := os.Create(db.path)
|
||||
// Handle Error
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Close File
|
||||
file.Close()
|
||||
// Save Database
|
||||
err = db.save()
|
||||
// Handle Error
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Success
|
||||
return &db
|
||||
} else if err != nil {
|
||||
// Handle Error
|
||||
panic(err)
|
||||
}
|
||||
// Load
|
||||
err = db.load()
|
||||
// Handle Error
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Success
|
||||
return &db
|
||||
}
|
||||
|
||||
// Set IPv4 Address
|
||||
func (db *database) SetIPv4Address(ipv4Address string) error {
|
||||
// Lock
|
||||
db.mutex.Lock()
|
||||
defer db.mutex.Unlock()
|
||||
// Set IPv4 Address
|
||||
db.data.IPv4Address = ipv4Address
|
||||
// Reset Updated
|
||||
for entryNumber := range db.data.DDNSEntries {
|
||||
db.data.DDNSEntries[entryNumber].Updated = false
|
||||
}
|
||||
// Save
|
||||
return db.save()
|
||||
}
|
||||
|
||||
// Set IPv6 Address
|
||||
func (db *database) SetIPv6Address(ipv6Address string) error {
|
||||
// Lock
|
||||
db.mutex.Lock()
|
||||
defer db.mutex.Unlock()
|
||||
// Set IPv6 Address
|
||||
db.data.IPv6Address = ipv6Address
|
||||
// Reset Updated
|
||||
for entryNumber := range db.data.DDNSEntries {
|
||||
db.data.DDNSEntries[entryNumber].Updated = false
|
||||
}
|
||||
// Save
|
||||
return db.save()
|
||||
}
|
||||
|
||||
// Get IPv4 Address
|
||||
func (db *database) GetIPv4Address() string {
|
||||
return db.data.IPv4Address
|
||||
}
|
||||
|
||||
// Get IPv6 Address
|
||||
func (db *database) GetIPv6Address() string {
|
||||
return db.data.IPv6Address
|
||||
}
|
||||
|
||||
// Add Entry
|
||||
func (db *database) AddEntry(entry DatabaseDataDDNSEntry) error {
|
||||
// Lock
|
||||
db.mutex.Lock()
|
||||
defer db.mutex.Unlock()
|
||||
// Check Entry
|
||||
for _, existingEntry := range db.data.DDNSEntries {
|
||||
if entry.Record == existingEntry.Record {
|
||||
return errors.New("entry exists")
|
||||
}
|
||||
}
|
||||
// Add Entry
|
||||
db.data.DDNSEntries = append(db.data.DDNSEntries, entry)
|
||||
// Save
|
||||
return db.save()
|
||||
}
|
||||
|
||||
// Update Entry
|
||||
func (db *database) UpdateEntry(record string) error {
|
||||
// Lock
|
||||
db.mutex.Lock()
|
||||
defer db.mutex.Unlock()
|
||||
// Update Entry
|
||||
for entryNumber, entry := range db.data.DDNSEntries {
|
||||
if entry.Record == record {
|
||||
db.data.DDNSEntries[entryNumber].Updated = true
|
||||
break
|
||||
}
|
||||
}
|
||||
// Save
|
||||
return db.save()
|
||||
}
|
||||
|
||||
// Get Entries
|
||||
func (db *database) GetEntries() []DatabaseDataDDNSEntry {
|
||||
return db.data.DDNSEntries
|
||||
}
|
||||
|
||||
// Delete Entry
|
||||
func (db *database) DeleteEntry(record string) error {
|
||||
// Lock
|
||||
db.mutex.Lock()
|
||||
defer db.mutex.Unlock()
|
||||
// Delete Entry
|
||||
for entryNumber, entry := range db.data.DDNSEntries {
|
||||
if entry.Record == record {
|
||||
db.data.DDNSEntries = append(db.data.DDNSEntries[:entryNumber], db.data.DDNSEntries[entryNumber+1:]...)
|
||||
}
|
||||
}
|
||||
// Save
|
||||
return db.save()
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
FROM alpine:latest
|
||||
|
||||
RUN apk update
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./FluxDNS ./
|
||||
COPY ./database.json ./
|
||||
|
||||
ENTRYPOINT [ "./FluxDNS" ]
|
@ -0,0 +1,8 @@
|
||||
import { defineConfig } from 'astro/config';
|
||||
|
||||
import svelte from '@astrojs/svelte';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
integrations: [svelte()]
|
||||
});
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"start": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/svelte": "^5.7.3",
|
||||
"astro": "^4.16.12",
|
||||
"svelte": "^5.1.16",
|
||||
"typescript": "^5.6.3"
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
|
||||
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
|
||||
<style>
|
||||
path { fill: #000; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
path { fill: #FFF; }
|
||||
}
|
||||
</style>
|
||||
</svg>
|
After Width: | Height: | Size: 749 B |
@ -0,0 +1,93 @@
|
||||
Copyright 2018 The Orbitron Project Authors (https://github.com/theleagueof/orbitron), with Reserved Font Name: "Orbitron"
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://openfontlicense.org
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 160 KiB |
Binary file not shown.
After Width: | Height: | Size: 47 KiB |
@ -0,0 +1,45 @@
|
||||
<script>
|
||||
let address4 = "";
|
||||
let address6 = "";
|
||||
|
||||
fetch("/api/get-addresses").then(data => data.json()).then(json => {
|
||||
address4 = json.ipv4
|
||||
address6 = json.ipv6
|
||||
}).catch(error => {
|
||||
console.log(error)
|
||||
})
|
||||
|
||||
setInterval(() => {
|
||||
fetch("/api/get-addresses").then(data => data.json()).then(json => {
|
||||
address4 = json.ipv4
|
||||
address6 = json.ipv6
|
||||
}).catch(error => {
|
||||
console.log(error)
|
||||
})
|
||||
}, 5000);
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<span>
|
||||
<h2>IPv4 Address: { address4 }</h2>
|
||||
<h2>IPv6 Address: { address6 }</h2>
|
||||
</span>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<style>
|
||||
div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
h2 {
|
||||
color: rgb(255, 255, 255);
|
||||
padding: 10px 30px;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,278 @@
|
||||
<script>
|
||||
let showModal = false;
|
||||
let records = [];
|
||||
|
||||
function openModal() {
|
||||
showModal = true;
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
showModal = false;
|
||||
}
|
||||
|
||||
function handleFormSubmit(event) {
|
||||
event.preventDefault(); // Prevent the default form submission behavior
|
||||
|
||||
// Create an object from form data
|
||||
const formData = {
|
||||
record: event.target.recordName.value,
|
||||
updated: false,
|
||||
provider: "Cloudflare",
|
||||
provider_data: {
|
||||
api_token: event.target.apiToken.value,
|
||||
zone_id: event.target.zoneId.value
|
||||
},
|
||||
};
|
||||
|
||||
// Send the JSON data using the Fetch API
|
||||
fetch("/api/add-entry", {
|
||||
method: "POST", // Change to 'PUT' if updating an existing record
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(formData),
|
||||
})
|
||||
.then((response) => response.json()).then(json => {
|
||||
closeModal();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("Error:", error);
|
||||
});
|
||||
}
|
||||
|
||||
setInterval(() => {
|
||||
fetch("/api/get-entries")
|
||||
.then((response) => response.json()).then(json => {
|
||||
records = json;
|
||||
console.log(records)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("Error:", error);
|
||||
});
|
||||
}, 2000)
|
||||
|
||||
function DeleteRecord(record) {
|
||||
// Log the initial action for debugging
|
||||
console.log('Deleting record:', record.record, 'from provider:', record.provider);
|
||||
|
||||
// Prepare the data payload
|
||||
const data = {
|
||||
recordName: record.record,
|
||||
provider: record.provider
|
||||
};
|
||||
|
||||
// Send a POST request to the API endpoint
|
||||
fetch("/api/delete-record", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(response => {
|
||||
// Check if the response is successful
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(json => {
|
||||
// Log the response from the server
|
||||
console.log('Record deleted successfully:', json);
|
||||
})
|
||||
.catch(error => {
|
||||
// Log and alert if there is an error
|
||||
console.log('Error deleting record:', error);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Button to open the modal -->
|
||||
<h2>DDNS Entries</h2>
|
||||
<button class="square-button" on:click={openModal}>+</button>
|
||||
|
||||
<!-- Modal structure -->
|
||||
{#if showModal}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="modal-overlay" on:click={closeModal}>
|
||||
<div class="modal-content" on:click|stopPropagation>
|
||||
<h2>Add new thing</h2>
|
||||
|
||||
<form id="dnsForm" on:submit={handleFormSubmit}>
|
||||
<label for="apiToken">Cloudflare API Token:</label>
|
||||
<input type="text" id="apiToken" name="apiToken" required />
|
||||
|
||||
<label for="zoneId">Zone ID:</label>
|
||||
<input type="text" id="zoneId" name="zoneId" required />
|
||||
|
||||
|
||||
<label for="recordName">Record Name:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="recordName"
|
||||
name="recordName"
|
||||
placeholder="subdomain.example.com"
|
||||
required
|
||||
/>
|
||||
|
||||
<button type="submit">t</button>
|
||||
</form>
|
||||
|
||||
|
||||
<button class="close-button" on:click={closeModal}>Close</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Provider</th>
|
||||
<th>Record</th>
|
||||
<th>Delete</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each records as record}
|
||||
<tr>
|
||||
<td>{record.provider}</td>
|
||||
<td>{record.record}</td>
|
||||
<td><button on:click={() => DeleteRecord(record)}>Delete</button></td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
<style>
|
||||
|
||||
/* Table Styling */
|
||||
table {
|
||||
width: 100%; /* Full width */
|
||||
border-collapse: collapse; /* Remove space between cells */
|
||||
margin: 20px 0; /* Add some spacing above and below the table */
|
||||
font-size: 16px; /* Adjust font size */
|
||||
text-align: left; /* Align text to the left */
|
||||
}
|
||||
|
||||
/* Table Header */
|
||||
table thead {
|
||||
background-color: #f9f9f90e; /* Light background for header */
|
||||
}
|
||||
|
||||
table thead th {
|
||||
padding: 12px 15px; /* Add padding for better spacing */
|
||||
border-bottom: 2px solid #ddd; /* Bottom border for header */
|
||||
font-weight: bold; /* Emphasize header text */
|
||||
}
|
||||
|
||||
/* Table Body */
|
||||
table tbody tr {
|
||||
border-bottom: 1px solid #ddd; /* Border between rows */
|
||||
}
|
||||
|
||||
table tbody tr:nth-child(even) {
|
||||
background-color: #f9f9f90e; /* Alternate row background color */
|
||||
}
|
||||
|
||||
table tbody td {
|
||||
padding: 12px 15px; /* Add padding for better spacing */
|
||||
}
|
||||
|
||||
/* Buttons in Table */
|
||||
table tbody td button {
|
||||
padding: 6px 12px; /* Button padding */
|
||||
background-color: #007bff; /* Primary button color */
|
||||
color: #fff; /* White text color */
|
||||
border: none; /* Remove border */
|
||||
border-radius: 4px; /* Rounded corners */
|
||||
cursor: pointer; /* Pointer cursor for buttons */
|
||||
}
|
||||
|
||||
table tbody td button:hover {
|
||||
background-color: #0056b3; /* Darker blue on hover */
|
||||
}
|
||||
|
||||
/* Table Footer (Optional) */
|
||||
table tfoot td {
|
||||
padding: 10px; /* Footer cell padding */
|
||||
text-align: right; /* Align footer text to the right */
|
||||
font-weight: bold; /* Footer text weight */
|
||||
}
|
||||
|
||||
|
||||
h2 {
|
||||
display: inline-block;
|
||||
padding-right: 40px;
|
||||
}
|
||||
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: #0a0a23;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
margin-top: 10px;
|
||||
padding: 10px 20px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
form {
|
||||
max-width: 400px;
|
||||
margin: 50px auto;
|
||||
padding: 20px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 10px;
|
||||
}
|
||||
form label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
form input {
|
||||
width: calc(100% - 16px);
|
||||
padding: 8px;
|
||||
margin-bottom: 16px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
}
|
||||
form button {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
form button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
</style>
|
@ -0,0 +1 @@
|
||||
/// <reference path="../.astro/types.d.ts" />
|
@ -0,0 +1,82 @@
|
||||
---
|
||||
interface Props {
|
||||
title: string;
|
||||
}
|
||||
|
||||
const { title } = Astro.props;
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="description" content="Astro description" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>{title}</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<main>
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
<style is:global>
|
||||
|
||||
@font-face {
|
||||
font-family: 'Orbitron';
|
||||
src: url('/fonts/Orbitron/Orbitron-Regular.woff2') format('woff2'),
|
||||
url('/fonts/Orbitron/Orbitron-Regular.woff') format('woff'),
|
||||
url('/fonts/Orbitron/Orbitron-Regular.ttf') format('truetype');
|
||||
font-weight: 400; /* Regular weight */
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Orbitron';
|
||||
src: url('/fonts/Orbitron/Orbitron-Bold.woff2') format('woff2'),
|
||||
url('/fonts/Orbitron/Orbitron-Bold.woff') format('woff'),
|
||||
url('/fonts/Orbitron/Orbitron-Bold.ttf') format('truetype');
|
||||
font-weight: 700; /* Bold weight */
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
/* Apply the font globally or to specific elements */
|
||||
body {
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
}
|
||||
* {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
main {
|
||||
color: white;
|
||||
width: 100vw;
|
||||
max-width: 1100px;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #231f20;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.square-button {
|
||||
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
font-size: 24px;
|
||||
border-radius: 10px;
|
||||
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
|
||||
background-color: #00FFFF;
|
||||
color: black;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,82 @@
|
||||
---
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
import Addresses from "../components/Addresses.svelte";
|
||||
import Records from "../components/Records.svelte"
|
||||
---
|
||||
|
||||
<Layout title="Flux DNS">
|
||||
|
||||
<div class = "header">
|
||||
<img src="/logo.png" alt="">
|
||||
|
||||
<Addresses client:load/>
|
||||
</div>
|
||||
|
||||
<Records client:load/>
|
||||
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
|
||||
.header {
|
||||
|
||||
display: flex;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.095);
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
.header img {
|
||||
width: 250px;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.astro-a {
|
||||
position: absolute;
|
||||
top: -32px;
|
||||
left: 50%;
|
||||
transform: translatex(-50%);
|
||||
width: 220px;
|
||||
height: auto;
|
||||
z-index: -1;
|
||||
}
|
||||
h1 {
|
||||
font-size: 4rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.text-gradient {
|
||||
background-image: var(--accent-gradient);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-size: 400%;
|
||||
background-position: 0%;
|
||||
}
|
||||
.instructions {
|
||||
margin-bottom: 2rem;
|
||||
border: 1px solid rgba(var(--accent-light), 25%);
|
||||
background: linear-gradient(rgba(var(--accent-dark), 66%), rgba(var(--accent-dark), 33%));
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.instructions code {
|
||||
font-size: 0.8em;
|
||||
font-weight: bold;
|
||||
background: rgba(var(--accent-light), 12%);
|
||||
color: rgb(var(--accent-light));
|
||||
border-radius: 4px;
|
||||
padding: 0.3em 0.4em;
|
||||
}
|
||||
.instructions strong {
|
||||
color: rgb(var(--accent-light));
|
||||
}
|
||||
.link-card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(24ch, 1fr));
|
||||
gap: 2rem;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,5 @@
|
||||
import { vitePreprocess } from '@astrojs/svelte';
|
||||
|
||||
export default {
|
||||
preprocess: vitePreprocess(),
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/base"
|
||||
}
|
@ -0,0 +1,363 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"FluxDNS/database"
|
||||
"embed"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
//go:embed frontend/dist/*
|
||||
var staticFiles embed.FS
|
||||
|
||||
var l4 string = ""
|
||||
var l6 string = ""
|
||||
|
||||
var LastAddress4 = ""
|
||||
var LastAddress6 = ""
|
||||
|
||||
var (
|
||||
loginAttempts = make(map[string]int)
|
||||
mu sync.Mutex
|
||||
)
|
||||
|
||||
// AuthMiddleware is a middleware function that checks for basic authentication
|
||||
func AuthMiddleware(username, password string, maxAttempts int, lockoutDuration time.Duration) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ip := r.RemoteAddr // Track login attempts by IP address
|
||||
|
||||
mu.Lock()
|
||||
attempts := loginAttempts[ip]
|
||||
mu.Unlock()
|
||||
|
||||
// If the number of attempts exceeds the max, return a lockout response
|
||||
if attempts >= maxAttempts {
|
||||
http.Error(w, "Too many failed attempts. Try again later.", http.StatusTooManyRequests)
|
||||
return
|
||||
}
|
||||
|
||||
auth := r.Header.Get("Authorization")
|
||||
if auth == "" {
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted Access"`)
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Expected format: "Basic <base64-encoded-username:password>"
|
||||
const prefix = "Basic "
|
||||
if !strings.HasPrefix(auth, prefix) {
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted Access"`)
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Decode the base64 string
|
||||
decoded, err := base64.StdEncoding.DecodeString(auth[len(prefix):])
|
||||
if err != nil {
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted Access"`)
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Split the decoded string into username and password
|
||||
creds := strings.SplitN(string(decoded), ":", 2)
|
||||
if len(creds) != 2 || creds[0] != username || creds[1] != password {
|
||||
mu.Lock()
|
||||
loginAttempts[ip]++
|
||||
mu.Unlock()
|
||||
|
||||
// Reset the attempt counter after lockout duration
|
||||
go func() {
|
||||
time.Sleep(lockoutDuration)
|
||||
mu.Lock()
|
||||
delete(loginAttempts, ip)
|
||||
mu.Unlock()
|
||||
}()
|
||||
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted Access"`)
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Clear failed attempts on successful login
|
||||
mu.Lock()
|
||||
delete(loginAttempts, ip)
|
||||
mu.Unlock()
|
||||
|
||||
// Call the next handler if authenticated
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
Start()
|
||||
|
||||
username := "admin" // Load from env
|
||||
password := "password" // Load from env
|
||||
maxAttempts := 3
|
||||
lockoutDuration := 1 * time.Minute
|
||||
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// Create a subdirectory to strip the "static" prefix
|
||||
subFS, err := fs.Sub(staticFiles, "frontend/dist")
|
||||
if err != nil {
|
||||
panic(err) // Handle error appropriately in production code
|
||||
}
|
||||
|
||||
// Serve the embedded static files without the "static" prefix
|
||||
fileServer := http.FileServer(http.FS(subFS))
|
||||
mux.Handle("/", fileServer)
|
||||
|
||||
// Database
|
||||
db := database.Create("./database.json")
|
||||
|
||||
l4 = db.GetIPv4Address()
|
||||
l6 = db.GetIPv6Address()
|
||||
|
||||
// API - Add Entry
|
||||
mux.HandleFunc("/api/add-entry", func(w http.ResponseWriter, r *http.Request) {
|
||||
// Ensure the request method is POST
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Invalid request method", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Read the body of the request
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to read request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
fmt.Println(string(body))
|
||||
|
||||
// Parse the JSON
|
||||
var entry database.DatabaseDataDDNSEntry
|
||||
var entryData database.DatabaseDataDDNSEntryProviderDataCloudflare
|
||||
|
||||
if err := json.Unmarshal(body, &entry); err != nil {
|
||||
http.Error(w, "Failed to parse JSON", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
dat, err := json.Marshal(entry.ProviderData)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(dat, &entryData); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
entry.ProviderData = entryData
|
||||
|
||||
// Print the received data (or handle it as needed)
|
||||
fmt.Printf("Received entry: %+v\n", entry)
|
||||
|
||||
// Save
|
||||
db.AddEntry(entry)
|
||||
|
||||
// Respond with a success message
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`{"success": true}`))
|
||||
})
|
||||
|
||||
// API - Get Addresses
|
||||
mux.HandleFunc("/api/get-addresses", func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(fmt.Sprintf(`{"ipv4": "%s","ipv6": "%s"}`, LastAddress4, LastAddress6)))
|
||||
})
|
||||
|
||||
// API - Get Entries
|
||||
mux.HandleFunc("/api/get-entries", func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Define the struct
|
||||
type Ent struct {
|
||||
Provider string `json:"provider"`
|
||||
Record string `json:"record"`
|
||||
}
|
||||
|
||||
// Fill the ents slice with data from entries
|
||||
dat := db.GetEntries()
|
||||
|
||||
var ents []Ent
|
||||
for _, entry := range dat {
|
||||
|
||||
ents = append(ents, Ent{
|
||||
Provider: entry.Provider,
|
||||
Record: entry.Record,
|
||||
})
|
||||
}
|
||||
|
||||
// Convert the ents slice to JSON and write it to the response
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(ents); err != nil {
|
||||
http.Error(w, "Failed to encode entries", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
// API - Delete Record
|
||||
mux.HandleFunc("/api/delete-record", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Invalid request method", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse JSON from the request body
|
||||
var requestData struct {
|
||||
RecordName string `json:"recordName"`
|
||||
Provider string `json:"provider"`
|
||||
}
|
||||
|
||||
err := json.NewDecoder(r.Body).Decode(&requestData)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to parse request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
// Find and remove the entry
|
||||
|
||||
err = db.DeleteEntry(requestData.RecordName)
|
||||
|
||||
if err == nil {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`{"message": "Record deleted successfully"}`))
|
||||
} else {
|
||||
http.Error(w, "Record not found", http.StatusNotFound)
|
||||
}
|
||||
})
|
||||
|
||||
// Update
|
||||
go func(l4, l42, l6, l62 *string) {
|
||||
time.Sleep(5 * time.Second)
|
||||
for true {
|
||||
entries := db.GetEntries()
|
||||
if *l4 != *l42 {
|
||||
*l4 = *l42
|
||||
|
||||
err = db.SetIPv4Address(*l4)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.Provider == "Cloudflare" {
|
||||
// Assert that entry.Data is of type CloudflareData
|
||||
data, ok := entry.ProviderData.(database.DatabaseDataDDNSEntryProviderDataCloudflare)
|
||||
if !ok {
|
||||
fmt.Println("ERROR")
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Println("4")
|
||||
err := OverwriteDNSRecord(data.APIToken, data.ZoneID, entry.Record, "A", *l4, 1)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
} else {
|
||||
db.UpdateEntry(entry.Record)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if *l6 != *l62 {
|
||||
*l6 = *l62
|
||||
db.SetIPv6Address(*l6)
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.Provider == "Cloudflare" {
|
||||
// Assert that entry.Data is of type CloudflareData
|
||||
data, ok := entry.ProviderData.(database.DatabaseDataDDNSEntryProviderDataCloudflare)
|
||||
if !ok {
|
||||
fmt.Println("ERROR")
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Println("6")
|
||||
err := OverwriteDNSRecord(data.APIToken, data.ZoneID, entry.Record, "AAAA", *l6, 1)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
} else {
|
||||
db.UpdateEntry(entry.Record)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
}(&l4, &LastAddress4, &l6, &LastAddress6)
|
||||
|
||||
// Wrap all handlers with the AuthMiddleware
|
||||
http.Handle("/", AuthMiddleware(username, password, maxAttempts, lockoutDuration)(mux))
|
||||
|
||||
port := "9008" // Replace with an environment variable if needed
|
||||
println("Server running on port", port)
|
||||
http.ListenAndServe(":"+port, nil)
|
||||
}
|
||||
|
||||
func FetchAddress(addressType string) (string, error) {
|
||||
response, err := http.Get(fmt.Sprintf("http://%s.getmyip.dev", addressType))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
// Read the response body
|
||||
body, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(body), nil
|
||||
}
|
||||
|
||||
// Start function to get the IP address and print it
|
||||
func Start() {
|
||||
|
||||
go func() {
|
||||
for true {
|
||||
r, err := FetchAddress("ipv4")
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
} else {
|
||||
if r != LastAddress4 {
|
||||
// UPDATE 4
|
||||
LastAddress4 = r
|
||||
fmt.Println("IPV4 Updated: " + r)
|
||||
}
|
||||
}
|
||||
r, err = FetchAddress("ipv6")
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
} else {
|
||||
if r != LastAddress6 {
|
||||
// UPDATE 6
|
||||
LastAddress6 = r
|
||||
fmt.Println("IPV6 Updated: " + r)
|
||||
}
|
||||
}
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
}()
|
||||
|
||||
}
|
@ -0,0 +1,130 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type DNSRecord struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
TTL int `json:"ttl"`
|
||||
}
|
||||
|
||||
type DNSListResponse struct {
|
||||
Result []DNSRecord `json:"result"`
|
||||
}
|
||||
|
||||
type CreateOrUpdateResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Errors []struct {
|
||||
Message string `json:"message"`
|
||||
} `json:"errors"`
|
||||
}
|
||||
|
||||
// OverwriteDNSRecord overwrites or creates a DNS record using Cloudflare's API.
|
||||
func OverwriteDNSRecord(apiToken, zoneID, recordName, recordType, recordContent string, ttl int) error {
|
||||
baseURL := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records", zoneID)
|
||||
|
||||
// Fetch existing DNS records
|
||||
req, err := http.NewRequest("GET", baseURL, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+apiToken)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch DNS records: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("failed to fetch DNS records: status code %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read response body: %w", err)
|
||||
}
|
||||
|
||||
var listResponse DNSListResponse
|
||||
if err := json.Unmarshal(body, &listResponse); err != nil {
|
||||
return fmt.Errorf("failed to parse response body: %w", err)
|
||||
}
|
||||
|
||||
var existingRecordID string
|
||||
for _, record := range listResponse.Result {
|
||||
if record.Name == recordName && record.Type == recordType {
|
||||
existingRecordID = record.ID
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare the DNS record payload
|
||||
dnsRecord := DNSRecord{
|
||||
Type: recordType,
|
||||
Name: recordName,
|
||||
Content: recordContent,
|
||||
TTL: ttl,
|
||||
}
|
||||
var method string
|
||||
var url string
|
||||
|
||||
if existingRecordID != "" {
|
||||
// Update existing record
|
||||
method = "PUT"
|
||||
url = fmt.Sprintf("%s/%s", baseURL, existingRecordID)
|
||||
} else {
|
||||
// Create a new record
|
||||
method = "POST"
|
||||
url = baseURL
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(dnsRecord)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal JSON: %w", err)
|
||||
}
|
||||
|
||||
req, err = http.NewRequest(method, url, bytes.NewBuffer(payload))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+apiToken)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err = client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
|
||||
return fmt.Errorf("request failed with status code %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var result CreateOrUpdateResponse
|
||||
body, err = ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read response body: %w", err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return fmt.Errorf("failed to parse response body: %w", err)
|
||||
}
|
||||
|
||||
if !result.Success {
|
||||
return fmt.Errorf("request failed with error: %v", result.Errors)
|
||||
}
|
||||
|
||||
log.Printf("DNS record %s successfully %s", recordName, method)
|
||||
return nil
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
cd ./frontend
|
||||
npm run build
|
||||
cd -
|
||||
GOARCH=amd64 GOOS=linux go build -o FluxDNS main.go mc.go
|
||||
docker build -t flux-dns .
|
@ -0,0 +1,2 @@
|
||||
docker network create --driver bridge --subnet 172.20.0.0/16 --ipv6 --subnet "2001:db8:1::/64" flux-dns-network
|
||||
docker run -e USER=tylan -e PASSWORD=password --network flux-dns-network -p 9008:9008 flux-dns
|
Loading…
Reference in New Issue