mirror of
https://github.com/marcusDenslow/terminalShop.git
synced 2026-10-25 10:02:21 +01:00
cleanup
This commit is contained in:
@@ -1,58 +0,0 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"terminalShop/api/middleware"
|
||||
"terminalShop/pkg/database"
|
||||
"terminalShop/pkg/models"
|
||||
"terminalShop/pkg/utils"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
type SSHKeyHandler struct{}
|
||||
|
||||
func NewSSHKeyHandler() *SSHKeyHandler {
|
||||
return &SSHKeyHandler{}
|
||||
}
|
||||
|
||||
// GetSSHKeys returns all ssh keys for the authenticated user.
|
||||
func (h *SSHKeyHandler) GetSSHKeys(w http.ResponseWriter, r *http.Request) {
|
||||
db := database.GetDB()
|
||||
userID := middleware.UserIDFromContext(r.Context())
|
||||
|
||||
var keys []models.SSHKey
|
||||
db.Where("user_id = ?", userID).Order("created_at ASC").Find(&keys)
|
||||
|
||||
utils.RespondSuccess(w, http.StatusOK, map[string]interface{}{
|
||||
"ssh_keys": keys,
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteSSHKeys removes one of the users ssh keys by ID
|
||||
func (h *SSHKeyHandler) DeleteSSHKeys(w http.ResponseWriter, r *http.Request) {
|
||||
db := database.GetDB()
|
||||
userID := middleware.UserIDFromContext(r.Context())
|
||||
|
||||
idStr := chi.URLParam(r, "id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
utils.RespondError(w, http.StatusBadRequest, "INVALID_ID", "invalid ssh key id", nil)
|
||||
return
|
||||
}
|
||||
|
||||
var key models.SSHKey
|
||||
if err := db.Where("id = ? AND user_id = ?", id, userID).First(&key).Error; err != nil {
|
||||
utils.RespondError(w, http.StatusNotFound, "SSH_KEY_NOT_FOUND", "ssh key not found", nil)
|
||||
return
|
||||
}
|
||||
if err := db.Delete(&key).Error; err != nil {
|
||||
utils.RespondError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to delete ssh key", nil)
|
||||
return
|
||||
}
|
||||
|
||||
utils.RespondSuccess(w, http.StatusOK, map[string]interface{}{
|
||||
"message": "ssh key deleted",
|
||||
})
|
||||
}
|
||||
225
loadtest/overnight.js
Normal file
225
loadtest/overnight.js
Normal file
@@ -0,0 +1,225 @@
|
||||
import http from 'k6/http';
|
||||
|
||||
const BASE = __ENV.BASE_URL || 'https://api.sshops.uk';
|
||||
const SECRET = __ENV.CLIENT_SECRET || '';
|
||||
|
||||
// Per-VU JWT cache. JWTs are valid for 30 minutes; we refresh ~5 min early.
|
||||
let cachedToken = null;
|
||||
let cachedExpiresAt = 0;
|
||||
let cachedFingerprint = null;
|
||||
|
||||
// SSHPublicKey has a uniqueIndex constraint, so each VU needs a distinct
|
||||
// (fake but unique) public key string. Suffixing with VU+timestamp keeps
|
||||
// the full text unique without affecting the rest of the body.
|
||||
let cachedPubkey = null;
|
||||
|
||||
function getToken() {
|
||||
const now = Date.now();
|
||||
if (cachedToken && now < cachedExpiresAt) return cachedToken;
|
||||
|
||||
if (!cachedFingerprint) {
|
||||
cachedFingerprint = `SHA256:k6-${__VU}-${now.toString(36)}`;
|
||||
cachedPubkey = `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILoadTest${__VU}-${now.toString(36)} k6@vu${__VU}`;
|
||||
}
|
||||
|
||||
const res = http.post(
|
||||
`${BASE}/api/v1/auth/token`,
|
||||
JSON.stringify({
|
||||
client_secret: SECRET,
|
||||
fingerprint: cachedFingerprint,
|
||||
ssh_public_key: cachedPubkey,
|
||||
}),
|
||||
{ headers: { 'Content-Type': 'application/json' }, tags: { name: 'auth_token_refresh' } },
|
||||
);
|
||||
|
||||
if (res.status !== 200) return null;
|
||||
const body = JSON.parse(res.body);
|
||||
cachedToken = body.access_token;
|
||||
cachedExpiresAt = now + (body.expires_in - 300) * 1000;
|
||||
return cachedToken;
|
||||
}
|
||||
|
||||
function authHeaders() {
|
||||
const tok = getToken();
|
||||
return tok ? { Authorization: `Bearer ${tok}`, 'Content-Type': 'application/json' } : null;
|
||||
}
|
||||
|
||||
export const options = {
|
||||
discardResponseBodies: true,
|
||||
scenarios: {
|
||||
browse: {
|
||||
executor: 'ramping-arrival-rate',
|
||||
startRate: 6,
|
||||
timeUnit: '1m',
|
||||
preAllocatedVUs: 10,
|
||||
maxVUs: 50,
|
||||
stages: [
|
||||
{ target: 12, duration: '1h' },
|
||||
{ target: 30, duration: '2h' },
|
||||
{ target: 90, duration: '2h' },
|
||||
{ target: 60, duration: '2h' },
|
||||
{ target: 120, duration: '3h' },
|
||||
{ target: 30, duration: '2h' },
|
||||
],
|
||||
exec: 'browse',
|
||||
},
|
||||
authed_browse: {
|
||||
executor: 'ramping-arrival-rate',
|
||||
startRate: 2,
|
||||
timeUnit: '1m',
|
||||
preAllocatedVUs: 5,
|
||||
maxVUs: 20,
|
||||
stages: [
|
||||
{ target: 10, duration: '4h' },
|
||||
{ target: 30, duration: '4h' },
|
||||
{ target: 8, duration: '4h' },
|
||||
],
|
||||
exec: 'authedBrowse',
|
||||
},
|
||||
convert_fail: {
|
||||
// /cart/convert is rate-limited 10/min per user. We share one loadtest
|
||||
// user across all VUs so ceiling is 10/min; we stay under at 4/min.
|
||||
executor: 'ramping-arrival-rate',
|
||||
startRate: 1,
|
||||
timeUnit: '1m',
|
||||
preAllocatedVUs: 3,
|
||||
maxVUs: 5,
|
||||
stages: [
|
||||
{ target: 2, duration: '4h' },
|
||||
{ target: 4, duration: '4h' },
|
||||
{ target: 2, duration: '4h' },
|
||||
],
|
||||
exec: 'convertFail',
|
||||
},
|
||||
address_bad: {
|
||||
executor: 'ramping-arrival-rate',
|
||||
startRate: 1,
|
||||
timeUnit: '1m',
|
||||
preAllocatedVUs: 3,
|
||||
maxVUs: 5,
|
||||
stages: [
|
||||
{ target: 3, duration: '12h' },
|
||||
],
|
||||
exec: 'addressBad',
|
||||
},
|
||||
auth_warn: {
|
||||
executor: 'ramping-arrival-rate',
|
||||
startRate: 1,
|
||||
timeUnit: '1m',
|
||||
preAllocatedVUs: 5,
|
||||
maxVUs: 10,
|
||||
stages: [
|
||||
{ target: 3, duration: '4h' },
|
||||
{ target: 10, duration: '4h' },
|
||||
{ target: 5, duration: '4h' },
|
||||
],
|
||||
exec: 'authBad',
|
||||
},
|
||||
auth_lookup: {
|
||||
executor: 'ramping-arrival-rate',
|
||||
startRate: 1,
|
||||
timeUnit: '1m',
|
||||
preAllocatedVUs: 5,
|
||||
maxVUs: 10,
|
||||
stages: [
|
||||
{ target: 4, duration: '6h' },
|
||||
{ target: 8, duration: '6h' },
|
||||
],
|
||||
exec: 'authLookup',
|
||||
},
|
||||
err_404: {
|
||||
executor: 'ramping-arrival-rate',
|
||||
startRate: 1,
|
||||
timeUnit: '1m',
|
||||
preAllocatedVUs: 3,
|
||||
maxVUs: 5,
|
||||
stages: [
|
||||
{ target: 5, duration: '12h' },
|
||||
],
|
||||
exec: 'notFound',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default function () {
|
||||
// Smoke-test entry point for `--vus N --duration T` runs.
|
||||
browse();
|
||||
authBad();
|
||||
authLookup();
|
||||
notFound();
|
||||
if (SECRET) {
|
||||
authedBrowse();
|
||||
convertFail();
|
||||
addressBad();
|
||||
}
|
||||
}
|
||||
|
||||
export function browse() {
|
||||
const r = Math.random();
|
||||
if (r < 0.6) http.get(`${BASE}/api/v1/products`, { tags: { name: 'list_products' } });
|
||||
else if (r < 0.8) http.get(`${BASE}/api/v1/products/1`, { tags: { name: 'get_product' } });
|
||||
else if (r < 0.95) http.get(`${BASE}/api/v1/health`, { tags: { name: 'health' } });
|
||||
else http.get(`${BASE}/api/v1/ping`, { tags: { name: 'ping' } });
|
||||
}
|
||||
|
||||
export function authedBrowse() {
|
||||
const h = authHeaders();
|
||||
if (!h) return;
|
||||
const r = Math.random();
|
||||
if (r < 0.4) http.get(`${BASE}/api/v1/cart`, { headers: h, tags: { name: 'authed_get_cart' } });
|
||||
else if (r < 0.6) http.get(`${BASE}/api/v1/cards`, { headers: h, tags: { name: 'authed_list_cards' } });
|
||||
else if (r < 0.8) http.get(`${BASE}/api/v1/addresses`, { headers: h, tags: { name: 'authed_list_addresses' } });
|
||||
else http.get(`${BASE}/api/v1/orders`, { headers: h, tags: { name: 'authed_list_orders' } });
|
||||
}
|
||||
|
||||
export function convertFail() {
|
||||
const h = authHeaders();
|
||||
if (!h) return;
|
||||
// Reset cart, drop one item in, attempt convert. No card/address selected
|
||||
// → ConvertCart hits validation_missing_address → cart_conversion_total
|
||||
// outcome=validation_missing_address + audit.OrderFailed warn.
|
||||
http.del(`${BASE}/api/v1/cart`, null, { headers: h, tags: { name: 'cart_clear' } });
|
||||
http.put(
|
||||
`${BASE}/api/v1/cart/item`,
|
||||
JSON.stringify({ coffee_id: 1, quantity: 1 }),
|
||||
{ headers: h, tags: { name: 'cart_add_item' } },
|
||||
);
|
||||
http.post(`${BASE}/api/v1/cart/convert`, null, { headers: h, tags: { name: 'cart_convert_fail' } });
|
||||
}
|
||||
|
||||
export function addressBad() {
|
||||
const h = authHeaders();
|
||||
if (!h) return;
|
||||
// Junk US zip + street → shippo validation fails → addressLog.Warn.
|
||||
http.post(
|
||||
`${BASE}/api/v1/addresses`,
|
||||
JSON.stringify({
|
||||
name: 'Load Test',
|
||||
street: 'asdfasdfasdf',
|
||||
city: 'Nowhere',
|
||||
state: 'XX',
|
||||
zip: '00000',
|
||||
country: 'US',
|
||||
phone: '+15555550000',
|
||||
}),
|
||||
{ headers: h, tags: { name: 'address_create_bad' } },
|
||||
);
|
||||
}
|
||||
|
||||
export function authBad() {
|
||||
http.post(
|
||||
`${BASE}/api/v1/auth/token`,
|
||||
JSON.stringify({ client_secret: 'definitely_wrong', fingerprint: 'SHA256:fake' }),
|
||||
{ headers: { 'Content-Type': 'application/json' }, tags: { name: 'auth_bad_creds' } },
|
||||
);
|
||||
}
|
||||
|
||||
export function authLookup() {
|
||||
const fp = `SHA256:${Math.random().toString(36).slice(2, 14)}`;
|
||||
http.get(`${BASE}/api/v1/auth/user?fingerprint=${fp}`, { tags: { name: 'auth_user_lookup' } });
|
||||
}
|
||||
|
||||
export function notFound() {
|
||||
const id = 99000 + Math.floor(Math.random() * 1000);
|
||||
http.get(`${BASE}/api/v1/products/${id}`, { tags: { name: 'product_not_found' } });
|
||||
}
|
||||
13
main.go
13
main.go
@@ -22,10 +22,6 @@ import (
|
||||
"terminalShop/pkg/tui"
|
||||
)
|
||||
|
||||
const (
|
||||
port = "23456"
|
||||
)
|
||||
|
||||
func listenHost() string {
|
||||
if h := os.Getenv("SSH_HOST"); h != "" {
|
||||
return h
|
||||
@@ -33,13 +29,6 @@ func listenHost() string {
|
||||
return "localhost"
|
||||
}
|
||||
|
||||
func listenPort() string {
|
||||
if p := os.Getenv("SSH_PORT"); p != "" {
|
||||
return p
|
||||
}
|
||||
return port
|
||||
}
|
||||
|
||||
var (
|
||||
apiURL string
|
||||
authFingerprintKey string
|
||||
@@ -78,7 +67,7 @@ func main() {
|
||||
|
||||
// Create SSH server
|
||||
host := listenHost()
|
||||
p := listenPort()
|
||||
p := cfg.SSHPort
|
||||
s, err := wish.NewServer(
|
||||
wish.WithAddress(fmt.Sprintf("%s:%s", host, p)),
|
||||
wish.WithHostKeyPath(".ssh/term_info_ed25519"),
|
||||
|
||||
@@ -155,7 +155,7 @@ func (m Model) generateReviewContent() string {
|
||||
view.WriteString(fmt.Sprintf("subtotal: %s\n", formatUSD(subtotal)))
|
||||
view.WriteString(m.theme.TextAccent().Render(fmt.Sprintf("total: %s", formatUSD(subtotal))) + "\n")
|
||||
view.WriteString("\n")
|
||||
view.WriteString(m.theme.TextError().Render("press enter to confirm") + "\n")
|
||||
view.WriteString(m.theme.TextBrand().Render("press enter to confirm") + "\n")
|
||||
|
||||
return m.theme.Base().Padding(0, 1).Render(view.String())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user