mirror of
https://github.com/tavo-wasd-gh/conex-builder.git
synced 2025-06-07 20:23:29 -06:00
object storage, misc fixes
This commit is contained in:
parent
906b09eaf7
commit
19b3060514
10 changed files with 394 additions and 172 deletions
5
Makefile
5
Makefile
|
@ -4,11 +4,16 @@ SRC = ${SRCDIR}/main.go \
|
||||||
${SRCDIR}/paypal.go \
|
${SRCDIR}/paypal.go \
|
||||||
${SRCDIR}/db.go \
|
${SRCDIR}/db.go \
|
||||||
${SRCDIR}/auth.go \
|
${SRCDIR}/auth.go \
|
||||||
|
${SRCDIR}/bucket.go \
|
||||||
|
|
||||||
GOFILES = ${SRCDIR}/go.sum ${SRCDIR}/go.mod
|
GOFILES = ${SRCDIR}/go.sum ${SRCDIR}/go.mod
|
||||||
GOMODS = github.com/joho/godotenv \
|
GOMODS = github.com/joho/godotenv \
|
||||||
github.com/lib/pq \
|
github.com/lib/pq \
|
||||||
gopkg.in/gomail.v2 \
|
gopkg.in/gomail.v2 \
|
||||||
|
github.com/aws/aws-sdk-go-v2/aws \
|
||||||
|
github.com/aws/aws-sdk-go-v2/config \
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials \
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/s3 \
|
||||||
|
|
||||||
all: ${BIN} fmt
|
all: ${BIN} fmt
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,7 @@ CREATE TABLE sites (
|
||||||
phone VARCHAR(20),
|
phone VARCHAR(20),
|
||||||
code VARCHAR(2),
|
code VARCHAR(2),
|
||||||
title VARCHAR(35) NOT NULL,
|
title VARCHAR(35) NOT NULL,
|
||||||
slogan VARCHAR(100) NOT NULL,
|
slogan VARCHAR(100),
|
||||||
banner TEXT,
|
banner TEXT,
|
||||||
raw JSONB NOT NULL,
|
raw JSONB NOT NULL,
|
||||||
auth INTEGER,
|
auth INTEGER,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
const savedData = localStorage.getItem('editor_data');
|
const savedData = localStorage.getItem('conex_data');
|
||||||
if (savedData) {
|
if (savedData) {
|
||||||
const parsedData = JSON.parse(savedData);
|
const parsedData = JSON.parse(savedData);
|
||||||
console.log('Loaded parsedData:', parsedData);
|
console.log('Loaded parsedData:', parsedData);
|
||||||
|
@ -11,14 +11,17 @@ document.addEventListener("DOMContentLoaded", function() {
|
||||||
const dialog = document.getElementById("dialog");
|
const dialog = document.getElementById("dialog");
|
||||||
const overlay = document.getElementById("overlay");
|
const overlay = document.getElementById("overlay");
|
||||||
const menu = document.getElementById("floatingButtons");
|
const menu = document.getElementById("floatingButtons");
|
||||||
|
const checkoutErrorMessage = document.getElementById("checkout-error-message");
|
||||||
|
|
||||||
function openDialog() {
|
function openDialog() {
|
||||||
|
checkoutErrorMessage.style.display = "none";
|
||||||
dialog.style.display = "block";
|
dialog.style.display = "block";
|
||||||
overlay.style.display = "block";
|
overlay.style.display = "block";
|
||||||
menu.style.display = "none";
|
menu.style.display = "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeDialog() {
|
function closeDialog() {
|
||||||
|
checkoutErrorMessage.style.display = "none";
|
||||||
dialog.style.display = "none";
|
dialog.style.display = "none";
|
||||||
overlay.style.display = "none";
|
overlay.style.display = "none";
|
||||||
menu.style.display = "block";
|
menu.style.display = "block";
|
||||||
|
@ -37,56 +40,60 @@ function saveEditorData() {
|
||||||
|
|
||||||
editor.save().then((editor_data) => {
|
editor.save().then((editor_data) => {
|
||||||
const dataToSave = {
|
const dataToSave = {
|
||||||
banner: banner,
|
directory: sanitizeDirectoryTitle(title),
|
||||||
|
banner: banner || '/static/svg/banner.svg',
|
||||||
title: title,
|
title: title,
|
||||||
slogan: slogan,
|
slogan: slogan,
|
||||||
editor_data: editor_data
|
editor_data: editor_data
|
||||||
};
|
};
|
||||||
localStorage.setItem('editor_data', JSON.stringify(dataToSave));
|
localStorage.setItem('conex_data', JSON.stringify(dataToSave));
|
||||||
console.log('Editor data saved to localStorage');
|
console.log('Editor data saved to localStorage');
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
console.error('Saving failed:', error);
|
console.error('Saving failed:', error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const directoryInput = document.getElementById('title');
|
|
||||||
const statusPopup = document.getElementById('status-popup');
|
|
||||||
const statusMessage = document.getElementById('status-message');
|
|
||||||
|
|
||||||
let typingTimeout;
|
let typingTimeout;
|
||||||
let hideTimeout;
|
let hideTimeout;
|
||||||
|
const directoryInput = document.getElementById('title');
|
||||||
directoryInput.addEventListener('input', () => {
|
directoryInput.addEventListener('input', () => {
|
||||||
clearTimeout(typingTimeout);
|
clearTimeout(typingTimeout);
|
||||||
typingTimeout = setTimeout(() => {
|
typingTimeout = setTimeout(() => {
|
||||||
const directoryName = directoryInput.value.trim();
|
const directoryTitle = directoryInput.value.trim();
|
||||||
if (directoryName.length > 0) {
|
if (directoryTitle.length > 0) {
|
||||||
const sanitizedDirectoryName = sanitizeDirectoryName(directoryName);
|
const directory = sanitizeDirectoryTitle(directoryTitle);
|
||||||
checkDirectory(sanitizedDirectoryName);
|
checkDirectory(directory);
|
||||||
} else {
|
} else {
|
||||||
hidePopup();
|
hidePopup();
|
||||||
}
|
}
|
||||||
}, 500); // Debounce
|
}, 500); // Debounce
|
||||||
});
|
});
|
||||||
|
|
||||||
function sanitizeDirectoryName(name) {
|
function sanitizeDirectoryTitle(title) {
|
||||||
return name
|
return title
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/\s+/g, '-')
|
.replace(/\s+/g, '-')
|
||||||
|
.normalize('NFD')
|
||||||
|
.replace(/[\u0300-\u036f]/g, '')
|
||||||
.replace(/[^a-z0-9\-]/g, '');
|
.replace(/[^a-z0-9\-]/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkDirectory(name) {
|
function checkDirectory(directory) {
|
||||||
if (name.length < 4) {
|
if (directory.length < 4) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
fetch(`/api/directory/${encodeURIComponent(name)}`)
|
if (directory.length > 35) {
|
||||||
|
showPopup(`El título no puede exceder los 35 caracteres`, 'exists');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fetch(`/api/directory/${encodeURIComponent(directory)}`)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.exists) {
|
if (data.exists) {
|
||||||
showPopup(`El sitio web conex.one/${name} ya existe`, 'exists');
|
showPopup(`El sitio web conex.one/${directory} ya existe`, 'exists');
|
||||||
} else {
|
} else {
|
||||||
showPopup(`Se publicará en conex.one/${name}`, 'available');
|
showPopup(`Se publicará en conex.one/${directory}`, 'available');
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
|
@ -133,16 +140,28 @@ function hidePopup(popup, status) {
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('imageUpload').addEventListener('change', function (event) {
|
document.getElementById('imageUpload').addEventListener('change', function (event) {
|
||||||
|
const savedData = localStorage.getItem('conex_data');
|
||||||
|
const parsedData = savedData ? JSON.parse(savedData) : null;
|
||||||
|
const directory = parsedData?.directory || "temp";
|
||||||
const file = event.target.files[0];
|
const file = event.target.files[0];
|
||||||
const reader = new FileReader();
|
|
||||||
|
|
||||||
reader.onload = function (e) {
|
|
||||||
const base64Image = e.target.result;
|
|
||||||
document.getElementById('banner').src = base64Image;
|
|
||||||
saveEditorData();
|
|
||||||
};
|
|
||||||
|
|
||||||
if (file) {
|
if (file) {
|
||||||
reader.readAsDataURL(file);
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
formData.append('directory', directory);
|
||||||
|
|
||||||
|
fetch('/api/upload', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
}).then(response => response.json()).then(data => {
|
||||||
|
if (data && data.file && data.file.url) {
|
||||||
|
document.getElementById('banner').src = data.file.url;
|
||||||
|
saveEditorData();
|
||||||
|
} else {
|
||||||
|
console.error('Error: Invalid response format', data);
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('Error uploading the image:', error);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -18,8 +18,8 @@
|
||||||
</label>
|
</label>
|
||||||
<img id="banner" name="banner" src="/static/svg/banner.svg" class="banner-image"/>
|
<img id="banner" name="banner" src="/static/svg/banner.svg" class="banner-image"/>
|
||||||
<div class="desc">
|
<div class="desc">
|
||||||
<input type="text" id="title" name="title" class="input-title" placeholder="[Nombre Ejemplo]">
|
<input type="text" id="title" name="title" maxlength="35" class="input-title" placeholder="[Nombre Ejemplo]">
|
||||||
<textarea type="text" id="slogan" name="slogan" class="input-slogan" placeholder="[Slogan llamativo o breve descripción]"></textarea>
|
<textarea type="text" id="slogan" name="slogan" maxlength="100" class="input-slogan" placeholder="[Slogan llamativo o breve descripción]"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div id="status-popup" class="status-popup">
|
<div id="status-popup" class="status-popup">
|
||||||
<span class="close-popup" onclick="hidePopup()">×</span>
|
<span class="close-popup" onclick="hidePopup()">×</span>
|
||||||
|
@ -48,11 +48,12 @@
|
||||||
<h2>Contratar por $20 al año</h2>
|
<h2>Contratar por $20 al año</h2>
|
||||||
<p>Gracias por elegir nuestro servicio para la compra de sitios web. Luego de ser aprobado, su sitio será publicado en menos de 24 horas a partir de la confirmación de tu compra. Utilizaremos los medios de contacto que proporcione para comunicarnos en caso de cualquier inconveniente con la publicación. Si experimenta algún problema, no dude en ponerte en contacto con nosotros a través de los canales:</p>
|
<p>Gracias por elegir nuestro servicio para la compra de sitios web. Luego de ser aprobado, su sitio será publicado en menos de 24 horas a partir de la confirmación de tu compra. Utilizaremos los medios de contacto que proporcione para comunicarnos en caso de cualquier inconveniente con la publicación. Si experimenta algún problema, no dude en ponerte en contacto con nosotros a través de los canales:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Correo electrónico: soporte@tusitio.com</li>
|
<li>Correo electrónico: soporte@conex.one</li>
|
||||||
<li>Teléfono: +1 234 567 890</li>
|
<!-- <li>Teléfono: +1 234 567 890</li> -->
|
||||||
<li>Horario de atención: Lunes a Viernes, de 9:00 a.m. a 6:00 p.m.</li>
|
<li>Horario de atención: L-V: 9:00 a.m. - 6:00 p.m.</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p id="result-message"></p>
|
<div id="checkout-error-message"></div>
|
||||||
|
<div id="checkout-success-message"></div>
|
||||||
<div id="paypal-button-container"></div>
|
<div id="paypal-button-container"></div>
|
||||||
<button id="cancelDialogButton" type="button">
|
<button id="cancelDialogButton" type="button">
|
||||||
<picture>
|
<picture>
|
||||||
|
|
202
public/paypal.js
202
public/paypal.js
|
@ -1,105 +1,119 @@
|
||||||
paypal.Buttons({
|
paypal.Buttons({
|
||||||
style: {
|
style: {
|
||||||
shape: "pill",
|
shape: "pill",
|
||||||
layout: "vertical",
|
layout: "vertical",
|
||||||
color: "black",
|
color: "black",
|
||||||
label: "pay"
|
label: "pay"
|
||||||
},
|
},
|
||||||
async createOrder() {
|
async createOrder() {
|
||||||
const savedData = JSON.parse(localStorage.getItem('editor_data')) || {};
|
const savedData = JSON.parse(localStorage.getItem('conex_data')) || {};
|
||||||
const requestData = {
|
const requestData = {
|
||||||
directory: sanitizeDirectoryName(savedData.title),
|
directory: savedData.directory,
|
||||||
banner: savedData.banner || '/static/svg/banner.svg',
|
};
|
||||||
title: savedData.title,
|
const response = await fetch("/api/orders", {
|
||||||
slogan: savedData.slogan,
|
method: "POST",
|
||||||
editor_data: savedData.editor_data
|
headers: {
|
||||||
};
|
"Content-Type": "application/json",
|
||||||
const response = await fetch("/api/orders", {
|
},
|
||||||
method: "POST",
|
body: JSON.stringify(requestData),
|
||||||
headers: {
|
});
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(requestData),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
if (response.status === 409) {
|
if (response.status === 409) {
|
||||||
resultMessage(`No se puede comprar este sitio, ya existe o tiene un nombre incorrecto. Prueba con un nombre diferente`);
|
checkoutError(`
|
||||||
} else {
|
<p>El título "${savedData.title}" es incorrecto, debe cumplir:<br>
|
||||||
resultMessage(`No se puede realizar la compra en este momento`);
|
<ul>
|
||||||
}
|
<li>Entre 4 y 35 caracteres</li>
|
||||||
console.log(`HTTP Error: ${response.status} - ${response.statusText}`);
|
<li>Debe ser único</li>
|
||||||
return;
|
</ul>
|
||||||
}
|
</p>
|
||||||
|
`);
|
||||||
|
} else {
|
||||||
|
checkoutError(`<p>No se puede realizar la compra en este momento</p>`);
|
||||||
|
}
|
||||||
|
console.log(`HTTP Error: ${response.status} - ${response.statusText}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const orderData = await response.json();
|
const orderData = await response.json();
|
||||||
|
|
||||||
if (orderData.id) {
|
if (orderData.id) {
|
||||||
return orderData.id;
|
return orderData.id;
|
||||||
} else {
|
} else {
|
||||||
const errorDetail = orderData?.details?.[0];
|
const errorDetail = orderData?.details?.[0];
|
||||||
resultMessage(`No se puede realizar la compra en este momento`);
|
checkoutError(`<p>No se puede realizar la compra en este momento</p>`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async onApprove(data, actions) {
|
async onApprove(data, actions) {
|
||||||
try {
|
const savedData = JSON.parse(localStorage.getItem('conex_data')) || {};
|
||||||
// @@@ TODO por alguna razon hizo la compra a pesar de que esto estaba mal puesto
|
try {
|
||||||
const requestData = {
|
const requestData = {
|
||||||
directory: "gofitness",
|
directory: savedData.directory,
|
||||||
editor_data: await editor.save()
|
banner: savedData.banner,
|
||||||
};
|
title: savedData.title,
|
||||||
|
slogan: savedData.slogan,
|
||||||
|
editor_data: savedData.editor_data
|
||||||
|
};
|
||||||
|
|
||||||
const response = await fetch(`/api/orders/${data.orderID}/capture`, {
|
const response = await fetch(`/api/orders/${data.orderID}/capture`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify(requestData),
|
body: JSON.stringify(requestData),
|
||||||
});
|
});
|
||||||
|
|
||||||
const orderData = await response.json();
|
const orderData = await response.json();
|
||||||
// Three cases to handle:
|
// Three cases to handle:
|
||||||
// (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart()
|
// (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart()
|
||||||
// (2) Other non-recoverable errors -> Show a failure message
|
// (2) Other non-recoverable errors -> Show a failure message
|
||||||
// (3) Successful transaction -> Show confirmation or thank you message
|
// (3) Successful transaction -> Show confirmation or thank you message
|
||||||
|
|
||||||
const errorDetail = orderData?.details?.[0];
|
const errorDetail = orderData?.details?.[0];
|
||||||
|
|
||||||
if (errorDetail?.issue === "INSTRUMENT_DECLINED") {
|
if (errorDetail?.issue === "INSTRUMENT_DECLINED") {
|
||||||
// (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart()
|
// (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart()
|
||||||
// recoverable state, per https://developer.paypal.com/docs/checkout/standard/customize/handle-funding-failures/
|
// recoverable state, per https://developer.paypal.com/docs/checkout/standard/customize/handle-funding-failures/
|
||||||
return actions.restart();
|
return actions.restart();
|
||||||
} else if (errorDetail) {
|
} else if (errorDetail) {
|
||||||
// (2) Other non-recoverable errors -> Show a failure message
|
// (2) Other non-recoverable errors -> Show a failure message
|
||||||
throw new Error(`${errorDetail.description} (${orderData.debug_id})`);
|
throw new Error(`${errorDetail.description} (${orderData.debug_id})`);
|
||||||
} else if (!orderData.purchase_units) {
|
} else if (!orderData.purchase_units) {
|
||||||
throw new Error(JSON.stringify(orderData));
|
throw new Error(JSON.stringify(orderData));
|
||||||
} else {
|
} else {
|
||||||
// (3) Successful transaction -> Show confirmation or thank you message
|
// (3) Successful transaction -> Show confirmation or thank you message
|
||||||
// Or go to another URL: actions.redirect('thank_you.html');
|
// Or go to another URL: actions.redirect('thank_you.html');
|
||||||
const transaction =
|
const transaction =
|
||||||
orderData?.purchase_units?.[0]?.payments?.captures?.[0] ||
|
orderData?.purchase_units?.[0]?.payments?.captures?.[0] ||
|
||||||
orderData?.purchase_units?.[0]?.payments?.authorizations?.[0];
|
orderData?.purchase_units?.[0]?.payments?.authorizations?.[0];
|
||||||
resultMessage(
|
checkoutSuccess(`
|
||||||
`Estado: <strong>${transaction.status}</strong><br>ID de transacción: ${transaction.id}<br>Luego de una revisión positiva, su sitio será publicado en menos de 24 horas.`,
|
<p>
|
||||||
);
|
Estado: <strong>${transaction.status}</strong><br>
|
||||||
console.log(
|
ID de transacción: ${transaction.id}<br>
|
||||||
"Capture result",
|
Luego de una revisión positiva, su sitio será publicado en menos de 24 horas.
|
||||||
orderData,
|
<p>
|
||||||
JSON.stringify(orderData, null, 2),
|
`,);
|
||||||
);
|
console.log(
|
||||||
}
|
"Capture result",
|
||||||
} catch (error) {
|
orderData,
|
||||||
console.error(error);
|
JSON.stringify(orderData, null, 2),
|
||||||
resultMessage(
|
);
|
||||||
`Sorry, your transaction could not be processed...<br><br>${error}`,
|
}
|
||||||
);
|
} catch (error) {
|
||||||
}
|
console.error(error);
|
||||||
},
|
checkoutError(`<p>No se puede realizar la compra en este momento</p>`);
|
||||||
|
}
|
||||||
|
},
|
||||||
}).render("#paypal-button-container");
|
}).render("#paypal-button-container");
|
||||||
|
|
||||||
// Example function to show a result to the user. Your site's UI library can be used instead.
|
function checkoutSuccess(message) {
|
||||||
function resultMessage(message) {
|
const container = document.querySelector("#checkout-success-message");
|
||||||
const container = document.querySelector("#result-message");
|
container.style.display = "block";
|
||||||
container.innerHTML = message;
|
container.innerHTML = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkoutError(message) {
|
||||||
|
const container = document.querySelector("#checkout-error-message");
|
||||||
|
container.style.display = "block";
|
||||||
|
container.innerHTML = message;
|
||||||
}
|
}
|
||||||
|
|
|
@ -236,9 +236,23 @@ button {
|
||||||
margin: 1.5em 0 0 0;
|
margin: 1.5em 0 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#result-message {
|
#checkout-success-message,
|
||||||
text-decoration: underline;
|
#checkout-error-message {
|
||||||
color: var(--warning-color);
|
display: none;
|
||||||
|
padding: 0.8em;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
margin: 2em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#checkout-success-message {
|
||||||
|
background-color: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
|
||||||
|
#checkout-error-message {
|
||||||
|
background-color: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
}
|
}
|
||||||
|
|
||||||
#cancelDialogButton {
|
#cancelDialogButton {
|
||||||
|
@ -320,8 +334,8 @@ button {
|
||||||
top: 19%;
|
top: 19%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
padding: 15px;
|
padding: 12px;
|
||||||
border-radius: 5px;
|
border-radius: 8px;
|
||||||
background-color: #f0f0f0;
|
background-color: #f0f0f0;
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
const savedData = localStorage.getItem('editor_data');
|
const savedData = localStorage.getItem('conex_data');
|
||||||
const parsedData = savedData ? JSON.parse(savedData) : null;
|
const parsedData = savedData ? JSON.parse(savedData) : null;
|
||||||
|
const directory = parsedData?.directory || "temp";
|
||||||
|
|
||||||
const editor = new EditorJS({
|
const editor = new EditorJS({
|
||||||
// readOnly: false,
|
// readOnly: false,
|
||||||
|
@ -24,31 +25,13 @@ const editor = new EditorJS({
|
||||||
image: {
|
image: {
|
||||||
class: ImageTool,
|
class: ImageTool,
|
||||||
config: {
|
config: {
|
||||||
uploader: {
|
endpoints: {
|
||||||
uploadByFile(file) {
|
byFile: `${window.location.origin}/api/upload`,
|
||||||
return new Promise((resolve, reject) => {
|
},
|
||||||
const reader = new FileReader();
|
field: 'file',
|
||||||
reader.onload = (event) => {
|
types: 'image/*',
|
||||||
const base64 = event.target.result;
|
additionalRequestData: {
|
||||||
resolve({
|
directory: directory,
|
||||||
success: 1,
|
|
||||||
file: {
|
|
||||||
url: base64,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
reader.onerror = reject;
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
uploadByUrl(url) {
|
|
||||||
return Promise.resolve({
|
|
||||||
success: 1,
|
|
||||||
file: {
|
|
||||||
url: url,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -194,4 +177,3 @@ const editor = new EditorJS({
|
||||||
saveEditorData();
|
saveEditorData();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
66
server/bucket.go
Normal file
66
server/bucket.go
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/aws/aws-sdk-go-v2/aws"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func UploadFile(s3Client *s3.Client, endpoint string, bucketName string,
|
||||||
|
publicEndpoint string, fileContent []byte,
|
||||||
|
objectKey string) (string, error) {
|
||||||
|
if _, err := s3Client.PutObject(context.TODO(), &s3.PutObjectInput{
|
||||||
|
Bucket: aws.String(bucketName),
|
||||||
|
Key: aws.String(objectKey),
|
||||||
|
Body: bytes.NewReader(fileContent),
|
||||||
|
}); err != nil {
|
||||||
|
return "", fmt.Errorf("unable to upload file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s/%s", publicEndpoint, objectKey), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func BucketSizeLimit(apiEndpoint string, apiToken string) error {
|
||||||
|
req, err := http.NewRequest("GET", apiEndpoint, nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer "+apiToken)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to check bucket size 1: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to check bucket size 2: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var bucket struct {
|
||||||
|
Result struct {
|
||||||
|
PayloadSize string `json:"payloadSize"`
|
||||||
|
} `json:"result"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(body, &bucket)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to check bucket size 3: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
payloadBytes, err := strconv.Atoi(bucket.Result.PayloadSize)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to check bucket size 4: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if payloadBytes > maxBucketSize {
|
||||||
|
return fmt.Errorf("unable to check bucket size 5: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
24
server/db.go
24
server/db.go
|
@ -37,9 +37,7 @@ func AvailableSite(db *sql.DB, folder string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func RegisterSitePayment(db *sql.DB,
|
func RegisterSitePayment(db *sql.DB, capture Capture, cart ConexData) error {
|
||||||
capture Capture, directory string, editorData json.RawMessage,
|
|
||||||
) error {
|
|
||||||
var (
|
var (
|
||||||
// Payment
|
// Payment
|
||||||
id string
|
id string
|
||||||
|
@ -55,6 +53,12 @@ func RegisterSitePayment(db *sql.DB,
|
||||||
email string
|
email string
|
||||||
phone string
|
phone string
|
||||||
country string
|
country string
|
||||||
|
// conex_data
|
||||||
|
directory string
|
||||||
|
title string
|
||||||
|
slogan string
|
||||||
|
banner string
|
||||||
|
editorData json.RawMessage
|
||||||
)
|
)
|
||||||
|
|
||||||
captureData := capture.PurchaseUnits[0].Payments.Captures[0]
|
captureData := capture.PurchaseUnits[0].Payments.Captures[0]
|
||||||
|
@ -72,6 +76,12 @@ func RegisterSitePayment(db *sql.DB,
|
||||||
phone = capture.Payer.Phone.PhoneNumber.NationalNumber
|
phone = capture.Payer.Phone.PhoneNumber.NationalNumber
|
||||||
country = capture.Payer.Address.CountryCode
|
country = capture.Payer.Address.CountryCode
|
||||||
|
|
||||||
|
directory = cart.Directory
|
||||||
|
title = cart.Title
|
||||||
|
slogan = cart.Slogan
|
||||||
|
banner = cart.Banner
|
||||||
|
editorData = cart.EditorData
|
||||||
|
|
||||||
var pkey int
|
var pkey int
|
||||||
newSite := db.QueryRow(`
|
newSite := db.QueryRow(`
|
||||||
SELECT id FROM sites WHERE folder = $1
|
SELECT id FROM sites WHERE folder = $1
|
||||||
|
@ -80,12 +90,12 @@ func RegisterSitePayment(db *sql.DB,
|
||||||
if newSite == sql.ErrNoRows {
|
if newSite == sql.ErrNoRows {
|
||||||
if err := db.QueryRow(`
|
if err := db.QueryRow(`
|
||||||
INSERT INTO sites
|
INSERT INTO sites
|
||||||
(folder, status, due, name, sur, email, phone, code, raw)
|
(folder, status, due, name, sur, email, phone, code, title, slogan, banner, raw)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
||||||
RETURNING id
|
RETURNING id
|
||||||
`, directory, wstatus, due,
|
`, directory, wstatus, due,
|
||||||
name, surname, email, phone, country,
|
name, surname, email, phone, country, title, slogan,
|
||||||
editorData).Scan(&pkey); err != nil {
|
banner, editorData).Scan(&pkey); err != nil {
|
||||||
return fmt.Errorf("%s: %v", errDBRegisterSite, err)
|
return fmt.Errorf("%s: %v", errDBRegisterSite, err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
123
server/main.go
123
server/main.go
|
@ -1,20 +1,32 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/aws/aws-sdk-go-v2/aws"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/config"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/credentials"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
_ "github.com/lib/pq"
|
_ "github.com/lib/pq"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
// Limits
|
||||||
|
maxUploadFileSize = 52428800 // 50MB
|
||||||
|
maxBucketSize = 10737418240 // 10GB
|
||||||
|
// Messages
|
||||||
msgClosingDBConn = "Msg: init.go: Closing database connection"
|
msgClosingDBConn = "Msg: init.go: Closing database connection"
|
||||||
msgDBConn = "Msg: init.go: Established database connection"
|
msgDBConn = "Msg: init.go: Established database connection"
|
||||||
errDBConn = "Fatal: init.go: Connect to database"
|
errDBConn = "Fatal: init.go: Connect to database"
|
||||||
|
@ -37,8 +49,17 @@ const (
|
||||||
errUpdateSite = "Error: main.go: Updating site data"
|
errUpdateSite = "Error: main.go: Updating site data"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ConexData struct {
|
||||||
|
Directory string `json:"directory"`
|
||||||
|
Banner string `json:"banner"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Slogan string `json:"slogan"`
|
||||||
|
EditorData json.RawMessage `json:"editor_data"`
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var db *sql.DB
|
var db *sql.DB
|
||||||
|
var s3Client *s3.Client
|
||||||
|
|
||||||
godotenv.Load()
|
godotenv.Load()
|
||||||
var (
|
var (
|
||||||
|
@ -76,11 +97,39 @@ func main() {
|
||||||
|
|
||||||
msg(msgDBConn)
|
msg(msgDBConn)
|
||||||
|
|
||||||
|
var (
|
||||||
|
bucketName = os.Getenv("BUCKET_NAME")
|
||||||
|
endpoint = os.Getenv("BUCKET_ENDPOINT")
|
||||||
|
accessKey = os.Getenv("BUCKET_ACCESSKEY")
|
||||||
|
secretKey = os.Getenv("BUCKET_SECRETKEY")
|
||||||
|
region = os.Getenv("BUCKET_REGION")
|
||||||
|
publicEndpoint = os.Getenv("BUCKET_PUBLIC_ENDPOINT")
|
||||||
|
apiEndpoint = os.Getenv("BUCKET_API_ENDPOINT")
|
||||||
|
apiToken = os.Getenv("BUCKET_API_TOKEN")
|
||||||
|
)
|
||||||
|
|
||||||
|
cfg, err := config.LoadDefaultConfig(context.TODO(),
|
||||||
|
config.WithRegion(region),
|
||||||
|
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(accessKey, secretKey, "")),
|
||||||
|
config.WithEndpointResolver(aws.EndpointResolverFunc(func(service, region string) (aws.Endpoint, error) {
|
||||||
|
return aws.Endpoint{
|
||||||
|
URL: endpoint,
|
||||||
|
SigningRegion: region,
|
||||||
|
}, nil
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
fatal(err, errServerStart)
|
||||||
|
}
|
||||||
|
|
||||||
|
s3Client = s3.NewFromConfig(cfg)
|
||||||
|
|
||||||
http.HandleFunc("/api/orders", CreateOrderHandler(db))
|
http.HandleFunc("/api/orders", CreateOrderHandler(db))
|
||||||
http.HandleFunc("/api/orders/", CaptureOrderHandler(db))
|
http.HandleFunc("/api/orders/", CaptureOrderHandler(db))
|
||||||
http.HandleFunc("/api/update", UpdateSiteHandler(db))
|
http.HandleFunc("/api/update", UpdateSiteHandler(db))
|
||||||
http.HandleFunc("/api/confirm", ConfirmChangesHandler(db))
|
http.HandleFunc("/api/confirm", ConfirmChangesHandler(db))
|
||||||
http.HandleFunc("/api/directory/", VerifyDirectoryHandler(db))
|
http.HandleFunc("/api/directory/", VerifyDirectoryHandler(db))
|
||||||
|
http.HandleFunc("/api/upload", UploadFileHandler(s3Client, endpoint, apiEndpoint, apiToken, bucketName, publicEndpoint))
|
||||||
http.Handle("/", http.FileServer(http.Dir("./public")))
|
http.Handle("/", http.FileServer(http.Dir("./public")))
|
||||||
|
|
||||||
stop := make(chan os.Signal, 1)
|
stop := make(chan os.Signal, 1)
|
||||||
|
@ -128,6 +177,13 @@ func CreateOrderHandler(db *sql.DB) http.HandlerFunc {
|
||||||
httpErrorAndLog(w, err, errReadBody, "Error decoding response")
|
httpErrorAndLog(w, err, errReadBody, "Error decoding response")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(cart.Directory) > 35 {
|
||||||
|
http.Error(w, "Site already exists", http.StatusConflict)
|
||||||
|
log.Printf("%s: %v", "Site title is too long", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err := AvailableSite(db, cart.Directory); err != nil {
|
if err := AvailableSite(db, cart.Directory); err != nil {
|
||||||
http.Error(w, "Site already exists", http.StatusConflict)
|
http.Error(w, "Site already exists", http.StatusConflict)
|
||||||
log.Printf("%s: %v", "Site already exists", err)
|
log.Printf("%s: %v", "Site already exists", err)
|
||||||
|
@ -155,10 +211,7 @@ func CaptureOrderHandler(db *sql.DB) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
errClientNotice := "Error capturing order"
|
errClientNotice := "Error capturing order"
|
||||||
|
|
||||||
var cart struct {
|
var cart ConexData
|
||||||
Directory string `json:"directory"`
|
|
||||||
EditorData json.RawMessage `json:"editor_data"`
|
|
||||||
}
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&cart); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&cart); err != nil {
|
||||||
httpErrorAndLog(w, err, errReadBody, errClientNotice)
|
httpErrorAndLog(w, err, errReadBody, errClientNotice)
|
||||||
return
|
return
|
||||||
|
@ -178,8 +231,7 @@ func CaptureOrderHandler(db *sql.DB) http.HandlerFunc {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := RegisterSitePayment(db, capture, cart.Directory,
|
if err := RegisterSitePayment(db, capture, cart); err != nil {
|
||||||
cart.EditorData); err != nil {
|
|
||||||
httpErrorAndLog(w, err, errRegisterSite+": "+cart.Directory, errClientNotice)
|
httpErrorAndLog(w, err, errRegisterSite+": "+cart.Directory, errClientNotice)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -280,3 +332,62 @@ func VerifyDirectoryHandler(db *sql.DB) http.HandlerFunc {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func UploadFileHandler(s3Client *s3.Client, endpoint string, apiEndpoint string,
|
||||||
|
apiToken string, bucketName string, publicEndpoint string) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if err := r.ParseMultipartForm(10 << 20); err != nil {
|
||||||
|
httpErrorAndLog(w, err, "Unable to parse form", "Unable to parse form")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
directory := r.FormValue("directory")
|
||||||
|
if directory == "" || len(directory) < 4 || len(directory) > 35 {
|
||||||
|
err := fmt.Errorf("invalid directory length")
|
||||||
|
httpErrorAndLog(w, err, "Unable to parse form", "Unable to parse form")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
file, fileHeader, err := r.FormFile("file")
|
||||||
|
if err != nil {
|
||||||
|
httpErrorAndLog(w, err, "Unable to get the file", "Unable to get the file")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
fileContent, err := io.ReadAll(file)
|
||||||
|
if err != nil {
|
||||||
|
httpErrorAndLog(w, err, "Unable to read file", "Unable to read file")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(fileContent) > maxUploadFileSize {
|
||||||
|
httpErrorAndLog(w, err, "File too large", "File too large")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := BucketSizeLimit(apiEndpoint, apiToken); err != nil {
|
||||||
|
httpErrorAndLog(w, err, "Bucket limit", "Bucket limit")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
objectKey := fmt.Sprintf("%s/%s-%s", directory, time.Now().Format("2006-01-02-15-04-05"), fileHeader.Filename)
|
||||||
|
url, err := UploadFile(s3Client, endpoint, bucketName, publicEndpoint, fileContent, objectKey)
|
||||||
|
if err != nil {
|
||||||
|
httpErrorAndLog(w, err, "Unable to upload file", "Unable to upload file")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var response struct {
|
||||||
|
Success int `json:"success"`
|
||||||
|
File struct {
|
||||||
|
URL string `json:"url"`
|
||||||
|
} `json:"file"`
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Success = 1
|
||||||
|
response.File.URL = url
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(response)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue