Compare commits
23 Commits
26f4ed4fde
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
5d81d4a922
|
|||
|
cdc53641a5
|
|||
|
0366109e7d
|
|||
|
ec2a023db3
|
|||
|
35289a04f2
|
|||
|
500c080d02
|
|||
|
fb7decc608
|
|||
|
e8fdbaba84
|
|||
|
3934751615
|
|||
|
ed5e40b3f4
|
|||
| 6800d329d4 | |||
| d093e4709f | |||
| 70ffdbe8d5 | |||
| 8ea67b9a8c | |||
|
|
9ec0447b84 | ||
| 75eddcb6fd | |||
| 704ca27cd7 | |||
| 4b694fed62 | |||
| f9c244e392 | |||
| bd524f13b2 | |||
| 07a19be4b3 | |||
| 261b9686bc | |||
| 679b748b2f |
43
.gitea/workflows/build-and-deploy.yml
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
name: Build and Deploy Website
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
paths:
|
||||||
|
- 'Backend/**'
|
||||||
|
- 'Frontend/**'
|
||||||
|
- 'Dockerfile'
|
||||||
|
- '.gitea/workflows/build-and-deploy.yml'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: mac
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: registry.smpark.in
|
||||||
|
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||||
|
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
tags: registry.smpark.in/smparkin/website:latest
|
||||||
|
cache-from: type=registry,ref=registry.smpark.in/smparkin/website:buildcache
|
||||||
|
cache-to: type=registry,ref=registry.smpark.in/smparkin/website:buildcache,mode=max
|
||||||
|
|
||||||
|
- name: Summary
|
||||||
|
run: |
|
||||||
|
echo "Built and pushed website:latest"
|
||||||
10
.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
.vscode
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Frontend
|
||||||
|
Frontend/node_modules
|
||||||
|
Frontend/dist
|
||||||
|
|
||||||
|
# Backend
|
||||||
|
Backend/server
|
||||||
|
Backend/public/app/
|
||||||
7
Backend/go.mod
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
module smpark.in
|
||||||
|
|
||||||
|
go 1.26.0
|
||||||
|
|
||||||
|
require golang.org/x/time v0.14.0
|
||||||
|
|
||||||
|
require golang.org/x/crypto v0.48.0 // indirect
|
||||||
4
Backend/go.sum
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||||
|
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||||
|
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||||
|
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||||
46
Backend/main.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/time/rate"
|
||||||
|
"smpark.in/paste"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
publicDir := "./public"
|
||||||
|
spaIndex := filepath.Join(publicDir, "app", "index.html")
|
||||||
|
|
||||||
|
store := paste.NewStore()
|
||||||
|
|
||||||
|
go paste.StartExpiryWorker(context.Background(), store, 30*time.Second)
|
||||||
|
|
||||||
|
rl := paste.NewRateLimiter(rate.Every(30*time.Second), 1)
|
||||||
|
|
||||||
|
handlers := paste.NewHandlers(store)
|
||||||
|
http.HandleFunc("POST /api/paste", rl.Middleware(handlers.Create))
|
||||||
|
http.HandleFunc("GET /api/paste/{id}/raw", handlers.GetRaw)
|
||||||
|
http.HandleFunc("GET /api/paste/{id}", handlers.Get)
|
||||||
|
|
||||||
|
fs := http.FileServer(http.Dir(publicDir))
|
||||||
|
|
||||||
|
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
path := filepath.Join(publicDir, filepath.Clean(r.URL.Path))
|
||||||
|
if info, err := os.Stat(path); err == nil && !info.IsDir() {
|
||||||
|
fs.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.ServeFile(w, r, spaIndex)
|
||||||
|
})
|
||||||
|
|
||||||
|
addr := ":8080"
|
||||||
|
slog.Info("listening", "addr", addr)
|
||||||
|
if err := http.ListenAndServe(addr, nil); err != nil {
|
||||||
|
slog.Error("server error", "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
42
Backend/paste/expiry.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package paste
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func StartExpiryWorker(ctx context.Context, s *Store, interval time.Duration) {
|
||||||
|
ticker := time.NewTicker(interval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
deleteExpired(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteExpired(s *Store) {
|
||||||
|
var expired []string
|
||||||
|
err := s.ForEach(func(p *Paste) error {
|
||||||
|
if p.ExpiresAt != nil && time.Now().After(*p.ExpiresAt) {
|
||||||
|
expired = append(expired, p.ID)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("expiry scan error", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, id := range expired {
|
||||||
|
if err := s.Delete(id); err != nil {
|
||||||
|
slog.Error("expiry delete error", "id", id, "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(expired) > 0 {
|
||||||
|
slog.Info("deleted expired pastes", "count", len(expired))
|
||||||
|
}
|
||||||
|
}
|
||||||
151
Backend/paste/handlers.go
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
package paste
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Handlers struct {
|
||||||
|
store *Store
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHandlers(store *Store) *Handlers {
|
||||||
|
return &Handlers{store: store}
|
||||||
|
}
|
||||||
|
|
||||||
|
type createRequest struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
Language string `json:"language"`
|
||||||
|
Expiry int `json:"expiry"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeJSON(w http.ResponseWriter, status int, v any) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(status)
|
||||||
|
json.NewEncoder(w).Encode(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeError(w http.ResponseWriter, status int, msg string) {
|
||||||
|
writeJSON(w, status, map[string]string{"error": msg})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handlers) Create(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, 1_100_000)
|
||||||
|
|
||||||
|
var req createRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, "invalid request body")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.Content == "" {
|
||||||
|
writeError(w, http.StatusBadRequest, "content is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(req.Content) > 1_000_000 {
|
||||||
|
writeError(w, http.StatusRequestEntityTooLarge, "content exceeds 1,000,000 characters")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(req.Title) > 200 {
|
||||||
|
writeError(w, http.StatusBadRequest, "title exceeds 200 characters")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var passwordHash string
|
||||||
|
if req.Password != "" {
|
||||||
|
if len(req.Password) > 72 {
|
||||||
|
writeError(w, http.StatusBadRequest, "password exceeds 72 characters")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, "failed to process password")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
passwordHash = string(hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
p := &Paste{
|
||||||
|
Title: req.Title,
|
||||||
|
Content: req.Content,
|
||||||
|
Language: req.Language,
|
||||||
|
PasswordHash: passwordHash,
|
||||||
|
Protected: passwordHash != "",
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Expiry < 1 || req.Expiry > 10 {
|
||||||
|
writeError(w, http.StatusBadRequest, "expiry must be between 1 and 10 minutes")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t := time.Now().Add(time.Duration(req.Expiry) * time.Minute)
|
||||||
|
p.ExpiresAt = &t
|
||||||
|
|
||||||
|
if err := h.store.Create(p); err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, "failed to create paste")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSON(w, http.StatusCreated, map[string]string{
|
||||||
|
"id": p.ID,
|
||||||
|
"url": "/paste/" + p.ID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handlers) Get(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id := r.PathValue("id")
|
||||||
|
p, err := h.store.Get(id)
|
||||||
|
if errors.Is(err, ErrNotFound) {
|
||||||
|
writeError(w, http.StatusNotFound, "paste not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, "failed to retrieve paste")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if p.ExpiresAt != nil && time.Now().After(*p.ExpiresAt) {
|
||||||
|
h.store.Delete(id)
|
||||||
|
writeError(w, http.StatusGone, "paste has expired")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if p.Protected {
|
||||||
|
pw := r.Header.Get("X-Paste-Password")
|
||||||
|
if pw == "" || bcrypt.CompareHashAndPassword([]byte(p.PasswordHash), []byte(pw)) != nil {
|
||||||
|
writeJSON(w, http.StatusForbidden, map[string]any{"protected": true, "error": "password required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handlers) GetRaw(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id := r.PathValue("id")
|
||||||
|
p, err := h.store.Get(id)
|
||||||
|
if errors.Is(err, ErrNotFound) {
|
||||||
|
http.Error(w, "paste not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "failed to retrieve paste", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if p.ExpiresAt != nil && time.Now().After(*p.ExpiresAt) {
|
||||||
|
h.store.Delete(id)
|
||||||
|
http.Error(w, "paste has expired", http.StatusGone)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if p.Protected {
|
||||||
|
pw := r.Header.Get("X-Paste-Password")
|
||||||
|
if pw == "" || bcrypt.CompareHashAndPassword([]byte(p.PasswordHash), []byte(pw)) != nil {
|
||||||
|
http.Error(w, "password required", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||||
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||||
|
w.Write([]byte(p.Content))
|
||||||
|
}
|
||||||
82
Backend/paste/ratelimit.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package paste
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/time/rate"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ipLimiter struct {
|
||||||
|
limiter *rate.Limiter
|
||||||
|
lastSeen time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type RateLimiter struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
limiters map[string]*ipLimiter
|
||||||
|
rate rate.Limit
|
||||||
|
burst int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRateLimiter(r rate.Limit, burst int) *RateLimiter {
|
||||||
|
rl := &RateLimiter{
|
||||||
|
limiters: make(map[string]*ipLimiter),
|
||||||
|
rate: r,
|
||||||
|
burst: burst,
|
||||||
|
}
|
||||||
|
go rl.cleanup()
|
||||||
|
return rl
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *RateLimiter) get(ip string) *rate.Limiter {
|
||||||
|
rl.mu.Lock()
|
||||||
|
defer rl.mu.Unlock()
|
||||||
|
entry, ok := rl.limiters[ip]
|
||||||
|
if !ok {
|
||||||
|
entry = &ipLimiter{limiter: rate.NewLimiter(rl.rate, rl.burst)}
|
||||||
|
rl.limiters[ip] = entry
|
||||||
|
}
|
||||||
|
entry.lastSeen = time.Now()
|
||||||
|
return entry.limiter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *RateLimiter) cleanup() {
|
||||||
|
for range time.Tick(5 * time.Minute) {
|
||||||
|
rl.mu.Lock()
|
||||||
|
for ip, entry := range rl.limiters {
|
||||||
|
if time.Since(entry.lastSeen) > 10*time.Minute {
|
||||||
|
delete(rl.limiters, ip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rl.mu.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *RateLimiter) Middleware(next http.HandlerFunc) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ip := extractIP(r)
|
||||||
|
if !rl.get(ip).Allow() {
|
||||||
|
slog.Warn("rate limit exceeded", "ip", ip)
|
||||||
|
writeError(w, http.StatusTooManyRequests, "rate limit exceeded")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
next(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractIP(r *http.Request) string {
|
||||||
|
if fwd := r.Header.Get("X-Forwarded-For"); fwd != "" {
|
||||||
|
parts := strings.SplitN(fwd, ",", 2)
|
||||||
|
return strings.TrimSpace(parts[0])
|
||||||
|
}
|
||||||
|
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||||
|
if err != nil {
|
||||||
|
return r.RemoteAddr
|
||||||
|
}
|
||||||
|
return host
|
||||||
|
}
|
||||||
97
Backend/paste/store.go
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
package paste
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"errors"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrNotFound = errors.New("paste not found")
|
||||||
|
|
||||||
|
type Paste struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
Language string `json:"language"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
ExpiresAt *time.Time `json:"expires_at"`
|
||||||
|
PasswordHash string `json:"-"`
|
||||||
|
Protected bool `json:"protected"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Store struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
pastes map[string]*Paste
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStore() *Store {
|
||||||
|
return &Store{pastes: make(map[string]*Paste)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) Create(p *Paste) error {
|
||||||
|
id, err := generateID()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.ID = id
|
||||||
|
p.CreatedAt = time.Now().UTC()
|
||||||
|
|
||||||
|
s.mu.Lock()
|
||||||
|
s.pastes[id] = p
|
||||||
|
s.mu.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) Get(id string) (*Paste, error) {
|
||||||
|
s.mu.RLock()
|
||||||
|
p, ok := s.pastes[id]
|
||||||
|
s.mu.RUnlock()
|
||||||
|
if !ok {
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) Delete(id string) error {
|
||||||
|
s.mu.Lock()
|
||||||
|
delete(s.pastes, id)
|
||||||
|
s.mu.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) ForEach(fn func(p *Paste) error) error {
|
||||||
|
s.mu.RLock()
|
||||||
|
ids := make([]string, 0, len(s.pastes))
|
||||||
|
for id := range s.pastes {
|
||||||
|
ids = append(ids, id)
|
||||||
|
}
|
||||||
|
s.mu.RUnlock()
|
||||||
|
|
||||||
|
for _, id := range ids {
|
||||||
|
s.mu.RLock()
|
||||||
|
p, ok := s.pastes[id]
|
||||||
|
s.mu.RUnlock()
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := fn(p); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const base62Chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||||
|
|
||||||
|
func generateID() (string, error) {
|
||||||
|
b := make([]byte, 8)
|
||||||
|
if _, err := rand.Read(b); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
result := make([]byte, 8)
|
||||||
|
for i, v := range b {
|
||||||
|
result[i] = base62Chars[int(v)%62]
|
||||||
|
}
|
||||||
|
return string(result), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-----BEGIN PUBLIC KEY-----
|
||||||
|
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEHZR4mQ31/kYqSOc2CWN7iZVmF/6f
|
||||||
|
6Rg39BboB9K8ckj02PvNK/JheaOaQWYuNirsnfn1cGBmhE4k8p6hL2dLvQ==
|
||||||
|
-----END PUBLIC KEY-----
|
||||||
4
Backend/public/favicon.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||||
|
<rect width="100" height="100" rx="16" fill="#9370db"/>
|
||||||
|
<text x="50" y="68" font-family="system-ui, sans-serif" font-size="48" font-weight="bold" fill="white" text-anchor="middle">SP</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 264 B |
|
Before Width: | Height: | Size: 544 KiB After Width: | Height: | Size: 544 KiB |
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1.0 MiB |
40
Dockerfile
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# ================================
|
||||||
|
# Frontend build
|
||||||
|
# ================================
|
||||||
|
FROM node:22-slim AS frontend
|
||||||
|
|
||||||
|
WORKDIR /frontend
|
||||||
|
COPY Frontend/package.json Frontend/package-lock.json ./
|
||||||
|
RUN npm ci
|
||||||
|
COPY Frontend/ ./
|
||||||
|
RUN npm run build -- --outDir dist --emptyOutDir
|
||||||
|
|
||||||
|
# ================================
|
||||||
|
# Backend build
|
||||||
|
# ================================
|
||||||
|
FROM golang:1.26-alpine AS backend
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
COPY Backend/ ./
|
||||||
|
RUN CGO_ENABLED=0 go build -o server .
|
||||||
|
|
||||||
|
# ================================
|
||||||
|
# Runtime
|
||||||
|
# ================================
|
||||||
|
FROM alpine:3.21
|
||||||
|
|
||||||
|
RUN adduser -D -h /app app
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy Go binary
|
||||||
|
COPY --from=backend --chown=app:app /build/server ./
|
||||||
|
|
||||||
|
# Copy static assets
|
||||||
|
COPY --chown=app:app Backend/public/ ./public/
|
||||||
|
|
||||||
|
# Copy built frontend
|
||||||
|
COPY --from=frontend --chown=app:app /frontend/dist ./public/app/
|
||||||
|
|
||||||
|
USER app
|
||||||
|
EXPOSE 8080
|
||||||
|
ENTRYPOINT ["./server"]
|
||||||
24
Frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
7
Frontend/.prettierrc
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": false,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"tabWidth": 2,
|
||||||
|
"printWidth": 100
|
||||||
|
}
|
||||||
23
Frontend/eslint.config.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import tseslint from 'typescript-eslint'
|
||||||
|
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
extends: [
|
||||||
|
js.configs.recommended,
|
||||||
|
tseslint.configs.recommended,
|
||||||
|
reactHooks.configs.flat.recommended,
|
||||||
|
reactRefresh.configs.vite,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
16
Frontend/index.html
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Stephen Parkinson</title>
|
||||||
|
</head>
|
||||||
|
<body style="margin: 0">
|
||||||
|
<script>
|
||||||
|
document.body.style.backgroundColor = window.matchMedia("(prefers-color-scheme: dark)").matches ? "#161d26" : "#ffffff";
|
||||||
|
</script>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
3782
Frontend/package-lock.json
generated
Normal file
37
Frontend/package.json
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "prettier --write src/ && tsc -b && vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"format": "prettier --write src/",
|
||||||
|
"format:check": "prettier --check src/",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@cloudscape-design/code-view": "^3.0.102",
|
||||||
|
"@cloudscape-design/components": "^3.0.1200",
|
||||||
|
"@cloudscape-design/global-styles": "^1.0.50",
|
||||||
|
"react": "^19.2.0",
|
||||||
|
"react-dom": "^19.2.0",
|
||||||
|
"react-router-dom": "^7.13.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.39.1",
|
||||||
|
"@types/node": "^24.10.1",
|
||||||
|
"@types/react": "^19.2.7",
|
||||||
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"@vitejs/plugin-react": "^5.1.1",
|
||||||
|
"eslint": "^9.39.1",
|
||||||
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.24",
|
||||||
|
"globals": "^16.5.0",
|
||||||
|
"prettier": "^3.8.1",
|
||||||
|
"typescript": "~5.9.3",
|
||||||
|
"typescript-eslint": "^8.48.0",
|
||||||
|
"vite": "^7.3.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
19
Frontend/src/App.css
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-image {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-image {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
52
Frontend/src/App.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { BrowserRouter, Routes, Route, useNavigate, useLocation } from "react-router-dom";
|
||||||
|
import AppLayout from "@cloudscape-design/components/app-layout";
|
||||||
|
import SideNavigation from "@cloudscape-design/components/side-navigation";
|
||||||
|
import "@cloudscape-design/global-styles/index.css";
|
||||||
|
import "./App.css";
|
||||||
|
import Home from "./pages/Home";
|
||||||
|
import Resume from "./pages/Resume";
|
||||||
|
import Paste from "./pages/Paste";
|
||||||
|
import PasteView from "./pages/PasteView";
|
||||||
|
import NotFound from "./pages/NotFound";
|
||||||
|
|
||||||
|
function AppContent() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppLayout
|
||||||
|
toolsHide
|
||||||
|
navigation={
|
||||||
|
<SideNavigation
|
||||||
|
header={{ text: "Stephen Parkinson", href: "/" }}
|
||||||
|
activeHref={location.pathname}
|
||||||
|
onFollow={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
navigate(event.detail.href);
|
||||||
|
}}
|
||||||
|
items={[
|
||||||
|
{ type: "link", text: "Home", href: "/" },
|
||||||
|
{ type: "link", text: "Resume", href: "/resume" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
content={
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<Home />} />
|
||||||
|
<Route path="/resume" element={<Resume />} />
|
||||||
|
<Route path="/paste" element={<Paste />} />
|
||||||
|
<Route path="/paste/:id" element={<PasteView />} />
|
||||||
|
<Route path="*" element={<NotFound />} />
|
||||||
|
</Routes>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
return (
|
||||||
|
<BrowserRouter>
|
||||||
|
<AppContent />
|
||||||
|
</BrowserRouter>
|
||||||
|
);
|
||||||
|
}
|
||||||
17
Frontend/src/main.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { StrictMode } from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import { applyMode, Mode } from "@cloudscape-design/global-styles";
|
||||||
|
|
||||||
|
import App from "./App.tsx";
|
||||||
|
|
||||||
|
const darkQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||||
|
applyMode(darkQuery.matches ? Mode.Dark : Mode.Light);
|
||||||
|
darkQuery.addEventListener("change", (e) => {
|
||||||
|
applyMode(e.matches ? Mode.Dark : Mode.Light);
|
||||||
|
});
|
||||||
|
|
||||||
|
createRoot(document.getElementById("root")!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
);
|
||||||
42
Frontend/src/pages/Home.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import ContentLayout from "@cloudscape-design/components/content-layout";
|
||||||
|
import Header from "@cloudscape-design/components/header";
|
||||||
|
import Container from "@cloudscape-design/components/container";
|
||||||
|
import SpaceBetween from "@cloudscape-design/components/space-between";
|
||||||
|
import Link from "@cloudscape-design/components/link";
|
||||||
|
import Box from "@cloudscape-design/components/box";
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<ContentLayout
|
||||||
|
header={
|
||||||
|
<div className="page-header">
|
||||||
|
<img src="/images/me.jpg" alt="Stephen Parkinson" className="profile-image" />
|
||||||
|
<Header variant="h1" description="Systems Development Engineer">
|
||||||
|
Stephen Parkinson
|
||||||
|
</Header>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SpaceBetween size="l">
|
||||||
|
<Container header={<Header variant="h2">About Me</Header>}>
|
||||||
|
<Box variant="p">I'm doing a lot of stuff with computers right now.</Box>
|
||||||
|
</Container>
|
||||||
|
|
||||||
|
<Container>
|
||||||
|
<img src="/images/trees.jpg" alt="Trees" className="hero-image" />
|
||||||
|
</Container>
|
||||||
|
|
||||||
|
<Container header={<Header variant="h2">Contact</Header>}>
|
||||||
|
<SpaceBetween size="s" direction="horizontal">
|
||||||
|
<Link href="https://www.linkedin.com/in/stephen-parkinson" external>
|
||||||
|
LinkedIn
|
||||||
|
</Link>
|
||||||
|
<Link href="https://github.com/smparkin" external>
|
||||||
|
GitHub
|
||||||
|
</Link>
|
||||||
|
</SpaceBetween>
|
||||||
|
</Container>
|
||||||
|
</SpaceBetween>
|
||||||
|
</ContentLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
14
Frontend/src/pages/NotFound.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import ContentLayout from "@cloudscape-design/components/content-layout";
|
||||||
|
import Header from "@cloudscape-design/components/header";
|
||||||
|
import Box from "@cloudscape-design/components/box";
|
||||||
|
import Link from "@cloudscape-design/components/link";
|
||||||
|
|
||||||
|
export default function NotFound() {
|
||||||
|
return (
|
||||||
|
<ContentLayout header={<Header variant="h1">404</Header>}>
|
||||||
|
<Box variant="p">
|
||||||
|
The page you're looking for doesn't exist. <Link href="/">Go home</Link>
|
||||||
|
</Box>
|
||||||
|
</ContentLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
139
Frontend/src/pages/Paste.tsx
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import ContentLayout from "@cloudscape-design/components/content-layout";
|
||||||
|
import Header from "@cloudscape-design/components/header";
|
||||||
|
import Container from "@cloudscape-design/components/container";
|
||||||
|
import FormField from "@cloudscape-design/components/form-field";
|
||||||
|
import Input from "@cloudscape-design/components/input";
|
||||||
|
import Textarea from "@cloudscape-design/components/textarea";
|
||||||
|
import Select from "@cloudscape-design/components/select";
|
||||||
|
import Slider from "@cloudscape-design/components/slider";
|
||||||
|
import Button from "@cloudscape-design/components/button";
|
||||||
|
import Alert from "@cloudscape-design/components/alert";
|
||||||
|
import SpaceBetween from "@cloudscape-design/components/space-between";
|
||||||
|
|
||||||
|
const LANGUAGE_OPTIONS = [
|
||||||
|
{ label: "Plain Text", value: "plaintext" },
|
||||||
|
{ label: "Bash", value: "bash" },
|
||||||
|
{ label: "C", value: "c" },
|
||||||
|
{ label: "C++", value: "cpp" },
|
||||||
|
{ label: "CSS", value: "css" },
|
||||||
|
{ label: "Dockerfile", value: "dockerfile" },
|
||||||
|
{ label: "Go", value: "go" },
|
||||||
|
{ label: "HTML", value: "html" },
|
||||||
|
{ label: "Java", value: "java" },
|
||||||
|
{ label: "JavaScript", value: "javascript" },
|
||||||
|
{ label: "JSON", value: "json" },
|
||||||
|
{ label: "Kotlin", value: "kotlin" },
|
||||||
|
{ label: "Markdown", value: "markdown" },
|
||||||
|
{ label: "Python", value: "python" },
|
||||||
|
{ label: "Ruby", value: "ruby" },
|
||||||
|
{ label: "Rust", value: "rust" },
|
||||||
|
{ label: "SQL", value: "sql" },
|
||||||
|
{ label: "Swift", value: "swift" },
|
||||||
|
{ label: "TypeScript", value: "typescript" },
|
||||||
|
{ label: "YAML", value: "yaml" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function Paste() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [title, setTitle] = useState("");
|
||||||
|
const [content, setContent] = useState("");
|
||||||
|
const [language, setLanguage] = useState(LANGUAGE_OPTIONS[0]);
|
||||||
|
const [expiry, setExpiry] = useState(5);
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
if (!content.trim()) {
|
||||||
|
setError("Content is required.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/paste", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
title: title.trim(),
|
||||||
|
content,
|
||||||
|
language: language.value,
|
||||||
|
expiry,
|
||||||
|
password: password || undefined,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok) {
|
||||||
|
setError(data.error ?? "Failed to create paste.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
navigate(`/paste/${data.id}`, { state: { password: password || undefined } });
|
||||||
|
} catch {
|
||||||
|
setError("Network error. Please try again.");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ContentLayout header={<Header variant="h1">New Paste</Header>}>
|
||||||
|
<Container>
|
||||||
|
<SpaceBetween size="m">
|
||||||
|
{error && (
|
||||||
|
<Alert type="error" onDismiss={() => setError(null)} dismissible>
|
||||||
|
{error}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
<FormField label="Title" description="Optional title for your paste.">
|
||||||
|
<Input
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => setTitle(e.detail.value)}
|
||||||
|
placeholder="Untitled"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
<FormField label="Content" constraintText="Required. Maximum 1 MB.">
|
||||||
|
<Textarea
|
||||||
|
value={content}
|
||||||
|
onChange={(e) => setContent(e.detail.value)}
|
||||||
|
placeholder="Paste your content here..."
|
||||||
|
rows={20}
|
||||||
|
spellcheck={false}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
<FormField label="Language">
|
||||||
|
<Select
|
||||||
|
selectedOption={language}
|
||||||
|
onChange={(e) =>
|
||||||
|
setLanguage(e.detail.selectedOption as { label: string; value: string })
|
||||||
|
}
|
||||||
|
options={LANGUAGE_OPTIONS}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
<FormField label="Password" description="Optional. Leave blank for a public paste.">
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.detail.value)}
|
||||||
|
placeholder="Leave blank for no password"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
<FormField label="Expires" description={`${expiry} minute${expiry === 1 ? "" : "s"}`}>
|
||||||
|
<Slider
|
||||||
|
value={expiry}
|
||||||
|
onChange={(e) => setExpiry(e.detail.value)}
|
||||||
|
min={1}
|
||||||
|
max={10}
|
||||||
|
tickMarks
|
||||||
|
valueFormatter={(v) => `${v}m`}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
<Button variant="primary" onClick={handleSubmit} loading={loading}>
|
||||||
|
Create Paste
|
||||||
|
</Button>
|
||||||
|
</SpaceBetween>
|
||||||
|
</Container>
|
||||||
|
</ContentLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
251
Frontend/src/pages/PasteView.tsx
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useParams, Link, useLocation } from "react-router-dom";
|
||||||
|
import ContentLayout from "@cloudscape-design/components/content-layout";
|
||||||
|
import Header from "@cloudscape-design/components/header";
|
||||||
|
import Container from "@cloudscape-design/components/container";
|
||||||
|
import Box from "@cloudscape-design/components/box";
|
||||||
|
import Button from "@cloudscape-design/components/button";
|
||||||
|
import SpaceBetween from "@cloudscape-design/components/space-between";
|
||||||
|
import Spinner from "@cloudscape-design/components/spinner";
|
||||||
|
import Alert from "@cloudscape-design/components/alert";
|
||||||
|
import ColumnLayout from "@cloudscape-design/components/column-layout";
|
||||||
|
import FormField from "@cloudscape-design/components/form-field";
|
||||||
|
import Input from "@cloudscape-design/components/input";
|
||||||
|
import { CodeView } from "@cloudscape-design/code-view";
|
||||||
|
|
||||||
|
interface Paste {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
language: string;
|
||||||
|
created_at: string;
|
||||||
|
expires_at: string | null;
|
||||||
|
protected: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(iso: string) {
|
||||||
|
return new Date(iso).toLocaleString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PasteView() {
|
||||||
|
const { id } = useParams<{ id: string }>();
|
||||||
|
const location = useLocation();
|
||||||
|
const [paste, setPaste] = useState<Paste | null>(null);
|
||||||
|
const [status, setStatus] = useState<
|
||||||
|
"loading" | "ok" | "notfound" | "expired" | "error" | "locked"
|
||||||
|
>("loading");
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
const [linkCopied, setLinkCopied] = useState(false);
|
||||||
|
const [passwordInput, setPasswordInput] = useState("");
|
||||||
|
const [passwordError, setPasswordError] = useState<string | null>(null);
|
||||||
|
const [unlocking, setUnlocking] = useState(false);
|
||||||
|
|
||||||
|
const initialPassword = (location.state as { password?: string })?.password;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!id) return;
|
||||||
|
const headers: HeadersInit = {};
|
||||||
|
if (initialPassword) headers["X-Paste-Password"] = initialPassword;
|
||||||
|
fetch(`/api/paste/${id}`, { headers })
|
||||||
|
.then((res) => {
|
||||||
|
if (res.status === 404) {
|
||||||
|
setStatus("notfound");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (res.status === 410) {
|
||||||
|
setStatus("expired");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (res.status === 403) {
|
||||||
|
setStatus("locked");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!res.ok) {
|
||||||
|
setStatus("error");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
if (data) {
|
||||||
|
setPaste(data);
|
||||||
|
setStatus("ok");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => setStatus("error"));
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
async function handleUnlock() {
|
||||||
|
if (!id || !passwordInput) return;
|
||||||
|
setUnlocking(true);
|
||||||
|
setPasswordError(null);
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/paste/${id}`, {
|
||||||
|
headers: { "X-Paste-Password": passwordInput },
|
||||||
|
});
|
||||||
|
if (res.status === 403) {
|
||||||
|
setPasswordError("Incorrect password.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (res.status === 404) {
|
||||||
|
setStatus("notfound");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (res.status === 410) {
|
||||||
|
setStatus("expired");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!res.ok) {
|
||||||
|
setStatus("error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = await res.json();
|
||||||
|
setPaste(data);
|
||||||
|
setStatus("ok");
|
||||||
|
} catch {
|
||||||
|
setPasswordError("Network error. Please try again.");
|
||||||
|
} finally {
|
||||||
|
setUnlocking(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCopyLink() {
|
||||||
|
navigator.clipboard.writeText(window.location.href).then(() => {
|
||||||
|
setLinkCopied(true);
|
||||||
|
setTimeout(() => setLinkCopied(false), 2000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCopy() {
|
||||||
|
if (!paste) return;
|
||||||
|
navigator.clipboard.writeText(paste.content).then(() => {
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === "loading") {
|
||||||
|
return (
|
||||||
|
<ContentLayout header={<Header variant="h1">Loading…</Header>}>
|
||||||
|
<Container>
|
||||||
|
<Spinner size="large" />
|
||||||
|
</Container>
|
||||||
|
</ContentLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === "notfound") {
|
||||||
|
return (
|
||||||
|
<ContentLayout header={<Header variant="h1">Not Found</Header>}>
|
||||||
|
<Container>
|
||||||
|
<SpaceBetween size="m">
|
||||||
|
<Alert type="error">This paste does not exist.</Alert>
|
||||||
|
<Link to="/paste">Create a new paste</Link>
|
||||||
|
</SpaceBetween>
|
||||||
|
</Container>
|
||||||
|
</ContentLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === "expired") {
|
||||||
|
return (
|
||||||
|
<ContentLayout header={<Header variant="h1">Expired</Header>}>
|
||||||
|
<Container>
|
||||||
|
<SpaceBetween size="m">
|
||||||
|
<Alert type="warning">This paste has expired and is no longer available.</Alert>
|
||||||
|
<Link to="/paste">Create a new paste</Link>
|
||||||
|
</SpaceBetween>
|
||||||
|
</Container>
|
||||||
|
</ContentLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === "locked") {
|
||||||
|
return (
|
||||||
|
<ContentLayout header={<Header variant="h1">Password Required</Header>}>
|
||||||
|
<Container>
|
||||||
|
<SpaceBetween size="m">
|
||||||
|
{passwordError && (
|
||||||
|
<Alert type="error" onDismiss={() => setPasswordError(null)} dismissible>
|
||||||
|
{passwordError}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
<FormField label="Password" description="This paste is password-protected.">
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
value={passwordInput}
|
||||||
|
onChange={(e) => setPasswordInput(e.detail.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.detail.key === "Enter") handleUnlock();
|
||||||
|
}}
|
||||||
|
placeholder="Enter password"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
<Button variant="primary" onClick={handleUnlock} loading={unlocking}>
|
||||||
|
Unlock
|
||||||
|
</Button>
|
||||||
|
</SpaceBetween>
|
||||||
|
</Container>
|
||||||
|
</ContentLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === "error" || !paste) {
|
||||||
|
return (
|
||||||
|
<ContentLayout header={<Header variant="h1">Error</Header>}>
|
||||||
|
<Container>
|
||||||
|
<Alert type="error">Failed to load paste. Please try again.</Alert>
|
||||||
|
</Container>
|
||||||
|
</ContentLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ContentLayout
|
||||||
|
header={
|
||||||
|
<Header
|
||||||
|
variant="h1"
|
||||||
|
actions={
|
||||||
|
<SpaceBetween direction="horizontal" size="xs">
|
||||||
|
<Button onClick={handleCopyLink} iconName={linkCopied ? "status-positive" : "share"}>
|
||||||
|
{linkCopied ? "Copied!" : "Copy Link"}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleCopy} iconName={copied ? "status-positive" : "copy"}>
|
||||||
|
{copied ? "Copied!" : "Copy Text"}
|
||||||
|
</Button>
|
||||||
|
{!paste.protected && (
|
||||||
|
<Button href={`/api/paste/${id}/raw`} target="_blank" iconName="external">
|
||||||
|
Raw
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</SpaceBetween>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{paste.title || "Untitled"}
|
||||||
|
</Header>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SpaceBetween size="m">
|
||||||
|
<Container>
|
||||||
|
<ColumnLayout columns={3} variant="text-grid">
|
||||||
|
<div>
|
||||||
|
<Box variant="awsui-key-label">Language</Box>
|
||||||
|
<Box>{paste.language || "Plain Text"}</Box>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Box variant="awsui-key-label">Created</Box>
|
||||||
|
<Box>{formatDate(paste.created_at)}</Box>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Box variant="awsui-key-label">Expires</Box>
|
||||||
|
<Box>{paste.expires_at ? formatDate(paste.expires_at) : "Never"}</Box>
|
||||||
|
</div>
|
||||||
|
</ColumnLayout>
|
||||||
|
</Container>
|
||||||
|
<Container>
|
||||||
|
<CodeView content={paste.content} lineNumbers wrapLines />
|
||||||
|
</Container>
|
||||||
|
</SpaceBetween>
|
||||||
|
</ContentLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
137
Frontend/src/pages/Resume.tsx
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
import ContentLayout from "@cloudscape-design/components/content-layout";
|
||||||
|
import Header from "@cloudscape-design/components/header";
|
||||||
|
import Container from "@cloudscape-design/components/container";
|
||||||
|
import SpaceBetween from "@cloudscape-design/components/space-between";
|
||||||
|
import Box from "@cloudscape-design/components/box";
|
||||||
|
import ColumnLayout from "@cloudscape-design/components/column-layout";
|
||||||
|
|
||||||
|
export default function Resume() {
|
||||||
|
return (
|
||||||
|
<ContentLayout header={<Header variant="h1">Resume</Header>}>
|
||||||
|
<SpaceBetween size="l">
|
||||||
|
<Container header={<Header variant="h2">Employment</Header>}>
|
||||||
|
<SpaceBetween size="l">
|
||||||
|
<SpaceBetween size="xxs">
|
||||||
|
<Header variant="h3">Systems Development Engineer, Amazon Security</Header>
|
||||||
|
<Box variant="small" color="text-body-secondary">
|
||||||
|
San Luis Obispo, CA · April 2020 – Present (Promoted to full-time July
|
||||||
|
2022)
|
||||||
|
</Box>
|
||||||
|
<Box variant="p">
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Create and deploy serverless applications to collect and enrich data for
|
||||||
|
analysis.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Add monitoring and alarming to existing services to allow for easier diagnosing
|
||||||
|
of potential issues.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Work with customers to understand requirements and deliver results quickly.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</Box>
|
||||||
|
</SpaceBetween>
|
||||||
|
|
||||||
|
<SpaceBetween size="xxs">
|
||||||
|
<Header variant="h3">Student Assistant, On-Site Support Cal Poly ITS</Header>
|
||||||
|
<Box variant="small" color="text-body-secondary">
|
||||||
|
San Luis Obispo, CA · May 2019 – February 2020
|
||||||
|
</Box>
|
||||||
|
<Box variant="p">
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Responsible for support of computer services and equipment across multiple
|
||||||
|
departments.
|
||||||
|
</li>
|
||||||
|
<li>Troubleshooting, detecting, and solving of technical problems.</li>
|
||||||
|
<li>
|
||||||
|
Managing macOS and Windows based computers using Active Directory, SCCM, and
|
||||||
|
Jamf.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</Box>
|
||||||
|
</SpaceBetween>
|
||||||
|
</SpaceBetween>
|
||||||
|
</Container>
|
||||||
|
|
||||||
|
<Container header={<Header variant="h2">Education</Header>}>
|
||||||
|
<SpaceBetween size="l">
|
||||||
|
<SpaceBetween size="xxs">
|
||||||
|
<Header variant="h3">California Polytechnic State University</Header>
|
||||||
|
<Box variant="small" color="text-body-secondary">
|
||||||
|
2017 – 2022
|
||||||
|
</Box>
|
||||||
|
<Box variant="p">
|
||||||
|
<strong>Software Engineering</strong>
|
||||||
|
<br />
|
||||||
|
Relevant Coursework: Introduction to Computing, Fundamentals of Computer Science,
|
||||||
|
Data Structures, Project-Based Object-Oriented Programming & Design,
|
||||||
|
Introduction to Computer Organization, Systems Programming, and Introduction to
|
||||||
|
Operating Systems.
|
||||||
|
</Box>
|
||||||
|
</SpaceBetween>
|
||||||
|
|
||||||
|
<SpaceBetween size="xxs">
|
||||||
|
<Header variant="h3">Cal Poly Security Education Club — President</Header>
|
||||||
|
<Box variant="small" color="text-body-secondary">
|
||||||
|
Spring 2020 – Spring 2021
|
||||||
|
</Box>
|
||||||
|
<Box variant="p">
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Responsible for planning and logistics of events including iFixit Triathlon and
|
||||||
|
Security Career Fair.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Manage a team of student officers to introduce students to security topics at
|
||||||
|
all skill levels.
|
||||||
|
</li>
|
||||||
|
<li>Coordinate with companies for presentations and special events.</li>
|
||||||
|
<li>
|
||||||
|
Presented technical talks such as "iOS Security and Jailbreaking", "machswap, a
|
||||||
|
vulnerability in XNU IPC", "Intro to SSH", "Nintendo Switch Security", "macOS
|
||||||
|
Security", and "Mac File Systems and APFS Security".
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</Box>
|
||||||
|
</SpaceBetween>
|
||||||
|
</SpaceBetween>
|
||||||
|
</Container>
|
||||||
|
|
||||||
|
<Container header={<Header variant="h2">Skills</Header>}>
|
||||||
|
<ColumnLayout columns={3} variant="text-grid">
|
||||||
|
<SpaceBetween size="xxs">
|
||||||
|
<Box variant="h3">Languages & Tools</Box>
|
||||||
|
<Box variant="p">
|
||||||
|
Python, TypeScript, React, JavaScript, C, Swift, git, zsh, Burp Suite
|
||||||
|
</Box>
|
||||||
|
</SpaceBetween>
|
||||||
|
<SpaceBetween size="xxs">
|
||||||
|
<Box variant="h3">AWS</Box>
|
||||||
|
<Box variant="p">Lambda, S3, DynamoDB, CloudWatch, KMS, SSM, SES, SQS, SNS, CDK</Box>
|
||||||
|
</SpaceBetween>
|
||||||
|
<SpaceBetween size="xxs">
|
||||||
|
<Box variant="h3">Operating Systems</Box>
|
||||||
|
<Box variant="p">macOS, Windows, Linux</Box>
|
||||||
|
</SpaceBetween>
|
||||||
|
</ColumnLayout>
|
||||||
|
</Container>
|
||||||
|
|
||||||
|
<Container header={<Header variant="h2">Projects</Header>}>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Developed a watchOS app to show the status of the White Hat lab on an Apple Watch.
|
||||||
|
</li>
|
||||||
|
<li>Created a Python script to control Spotify's API from the command line.</li>
|
||||||
|
<li>
|
||||||
|
Upgraded White Hat's network infrastructure to take advantage of the 1Gbps connection
|
||||||
|
to the internet.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</Container>
|
||||||
|
</SpaceBetween>
|
||||||
|
</ContentLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
28
Frontend/tsconfig.app.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2022",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"types": ["vite/client"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
7
Frontend/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
26
Frontend/tsconfig.node.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2023",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"types": ["node"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
25
Frontend/vite.config.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
base: '/app/',
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
'/api': 'http://localhost:8080',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: '../Backend/public/app',
|
||||||
|
emptyOutDir: true,
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks: {
|
||||||
|
vendor: ['react', 'react-dom'],
|
||||||
|
cloudscape: ['@cloudscape-design/components', '@cloudscape-design/global-styles'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
22
Makefile
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
.PHONY: all frontend backend run dev clean upgrade
|
||||||
|
|
||||||
|
all: frontend backend
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
cd Frontend && npm run build
|
||||||
|
|
||||||
|
backend:
|
||||||
|
cd Backend && go build -o server .
|
||||||
|
|
||||||
|
run: all
|
||||||
|
cd Backend && ./server
|
||||||
|
|
||||||
|
dev:
|
||||||
|
cd Frontend && npm run dev
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf Backend/server Backend/public/app
|
||||||
|
|
||||||
|
upgrade:
|
||||||
|
cd Frontend && npm upgrade
|
||||||
|
cd Backend && go get -u ./... && go mod tidy
|
||||||
@@ -1,439 +0,0 @@
|
|||||||
// !$*UTF8*$!
|
|
||||||
{
|
|
||||||
archiveVersion = 1;
|
|
||||||
classes = {
|
|
||||||
};
|
|
||||||
objectVersion = 56;
|
|
||||||
objects = {
|
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
|
||||||
C041F2C22AB021E30011752C /* smparkinApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C041F2C12AB021E30011752C /* smparkinApp.swift */; };
|
|
||||||
C041F2C42AB021E30011752C /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C041F2C32AB021E30011752C /* ContentView.swift */; };
|
|
||||||
C041F2C62AB021E40011752C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C041F2C52AB021E40011752C /* Assets.xcassets */; };
|
|
||||||
C041F2C92AB021E40011752C /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C041F2C82AB021E40011752C /* Preview Assets.xcassets */; };
|
|
||||||
C041F2D22AB0238F0011752C /* Vapor in Frameworks */ = {isa = PBXBuildFile; productRef = C041F2D12AB0238F0011752C /* Vapor */; };
|
|
||||||
C041F2D72AB023CA0011752C /* Leaf in Frameworks */ = {isa = PBXBuildFile; productRef = C041F2D62AB023CA0011752C /* Leaf */; };
|
|
||||||
C041F2DA2AB0243C0011752C /* Public in Resources */ = {isa = PBXBuildFile; fileRef = C041F2D82AB0243C0011752C /* Public */; };
|
|
||||||
C041F2DD2AB024510011752C /* Routes.swift in Sources */ = {isa = PBXBuildFile; fileRef = C041F2DC2AB024510011752C /* Routes.swift */; };
|
|
||||||
C041F2DF2AB0246C0011752C /* Server.swift in Sources */ = {isa = PBXBuildFile; fileRef = C041F2DE2AB0246C0011752C /* Server.swift */; };
|
|
||||||
C0C8E2BE2AB0304D00E809B4 /* home.leaf in Resources */ = {isa = PBXBuildFile; fileRef = C0C8E2BB2AB02B6C00E809B4 /* home.leaf */; };
|
|
||||||
C0C8E2BF2AB0304D00E809B4 /* privacy.leaf in Resources */ = {isa = PBXBuildFile; fileRef = C0C8E2BC2AB02B6C00E809B4 /* privacy.leaf */; };
|
|
||||||
C0C8E2C02AB0304D00E809B4 /* welcome.leaf in Resources */ = {isa = PBXBuildFile; fileRef = C0C8E2BD2AB02B6C00E809B4 /* welcome.leaf */; };
|
|
||||||
C0C8E2C12AB0304D00E809B4 /* 404.leaf in Resources */ = {isa = PBXBuildFile; fileRef = C0C8E2BA2AB02B6C00E809B4 /* 404.leaf */; };
|
|
||||||
C0C8E2C52AB03D6200E809B4 /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0C8E2C42AB03D6200E809B4 /* Log.swift */; };
|
|
||||||
/* End PBXBuildFile section */
|
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
|
||||||
C041F2BE2AB021E30011752C /* smparkin.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = smparkin.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
|
||||||
C041F2C12AB021E30011752C /* smparkinApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = smparkinApp.swift; sourceTree = "<group>"; };
|
|
||||||
C041F2C32AB021E30011752C /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
|
||||||
C041F2C52AB021E40011752C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
|
||||||
C041F2C82AB021E40011752C /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
|
||||||
C041F2D82AB0243C0011752C /* Public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Public; sourceTree = "<group>"; };
|
|
||||||
C041F2DC2AB024510011752C /* Routes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Routes.swift; sourceTree = "<group>"; };
|
|
||||||
C041F2DE2AB0246C0011752C /* Server.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Server.swift; sourceTree = "<group>"; };
|
|
||||||
C0C8E2BA2AB02B6C00E809B4 /* 404.leaf */ = {isa = PBXFileReference; lastKnownFileType = text; path = 404.leaf; sourceTree = "<group>"; };
|
|
||||||
C0C8E2BB2AB02B6C00E809B4 /* home.leaf */ = {isa = PBXFileReference; lastKnownFileType = text; path = home.leaf; sourceTree = "<group>"; };
|
|
||||||
C0C8E2BC2AB02B6C00E809B4 /* privacy.leaf */ = {isa = PBXFileReference; lastKnownFileType = text; path = privacy.leaf; sourceTree = "<group>"; };
|
|
||||||
C0C8E2BD2AB02B6C00E809B4 /* welcome.leaf */ = {isa = PBXFileReference; lastKnownFileType = text; path = welcome.leaf; sourceTree = "<group>"; };
|
|
||||||
C0C8E2C42AB03D6200E809B4 /* Log.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Log.swift; sourceTree = "<group>"; };
|
|
||||||
/* End PBXFileReference section */
|
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
|
||||||
C041F2BB2AB021E30011752C /* Frameworks */ = {
|
|
||||||
isa = PBXFrameworksBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
C041F2D72AB023CA0011752C /* Leaf in Frameworks */,
|
|
||||||
C041F2D22AB0238F0011752C /* Vapor in Frameworks */,
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
/* End PBXFrameworksBuildPhase section */
|
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
|
||||||
C041F2B52AB021E30011752C = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
C041F2C02AB021E30011752C /* smparkin */,
|
|
||||||
C041F2BF2AB021E30011752C /* Products */,
|
|
||||||
);
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
C041F2BF2AB021E30011752C /* Products */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
C041F2BE2AB021E30011752C /* smparkin.app */,
|
|
||||||
);
|
|
||||||
name = Products;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
C041F2C02AB021E30011752C /* smparkin */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
C041F2C12AB021E30011752C /* smparkinApp.swift */,
|
|
||||||
C041F2C32AB021E30011752C /* ContentView.swift */,
|
|
||||||
C041F2DE2AB0246C0011752C /* Server.swift */,
|
|
||||||
C041F2DC2AB024510011752C /* Routes.swift */,
|
|
||||||
C0C8E2C42AB03D6200E809B4 /* Log.swift */,
|
|
||||||
C0C8E2B92AB02B6C00E809B4 /* Views */,
|
|
||||||
C041F2D82AB0243C0011752C /* Public */,
|
|
||||||
C041F2C52AB021E40011752C /* Assets.xcassets */,
|
|
||||||
C041F2C72AB021E40011752C /* Preview Content */,
|
|
||||||
);
|
|
||||||
path = smparkin;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
C041F2C72AB021E40011752C /* Preview Content */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
C041F2C82AB021E40011752C /* Preview Assets.xcassets */,
|
|
||||||
);
|
|
||||||
path = "Preview Content";
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
C0C8E2B92AB02B6C00E809B4 /* Views */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
C0C8E2BA2AB02B6C00E809B4 /* 404.leaf */,
|
|
||||||
C0C8E2BB2AB02B6C00E809B4 /* home.leaf */,
|
|
||||||
C0C8E2BC2AB02B6C00E809B4 /* privacy.leaf */,
|
|
||||||
C0C8E2BD2AB02B6C00E809B4 /* welcome.leaf */,
|
|
||||||
);
|
|
||||||
path = Views;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
/* End PBXGroup section */
|
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
|
||||||
C041F2BD2AB021E30011752C /* smparkin */ = {
|
|
||||||
isa = PBXNativeTarget;
|
|
||||||
buildConfigurationList = C041F2CC2AB021E40011752C /* Build configuration list for PBXNativeTarget "smparkin" */;
|
|
||||||
buildPhases = (
|
|
||||||
C041F2BA2AB021E30011752C /* Sources */,
|
|
||||||
C041F2BB2AB021E30011752C /* Frameworks */,
|
|
||||||
C041F2BC2AB021E30011752C /* Resources */,
|
|
||||||
);
|
|
||||||
buildRules = (
|
|
||||||
);
|
|
||||||
dependencies = (
|
|
||||||
);
|
|
||||||
name = smparkin;
|
|
||||||
packageProductDependencies = (
|
|
||||||
C041F2D12AB0238F0011752C /* Vapor */,
|
|
||||||
C041F2D62AB023CA0011752C /* Leaf */,
|
|
||||||
);
|
|
||||||
productName = smparkin;
|
|
||||||
productReference = C041F2BE2AB021E30011752C /* smparkin.app */;
|
|
||||||
productType = "com.apple.product-type.application";
|
|
||||||
};
|
|
||||||
/* End PBXNativeTarget section */
|
|
||||||
|
|
||||||
/* Begin PBXProject section */
|
|
||||||
C041F2B62AB021E30011752C /* Project object */ = {
|
|
||||||
isa = PBXProject;
|
|
||||||
attributes = {
|
|
||||||
BuildIndependentTargetsInParallel = 1;
|
|
||||||
LastSwiftUpdateCheck = 1500;
|
|
||||||
LastUpgradeCheck = 1500;
|
|
||||||
TargetAttributes = {
|
|
||||||
C041F2BD2AB021E30011752C = {
|
|
||||||
CreatedOnToolsVersion = 15.0;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
buildConfigurationList = C041F2B92AB021E30011752C /* Build configuration list for PBXProject "smparkin" */;
|
|
||||||
compatibilityVersion = "Xcode 14.0";
|
|
||||||
developmentRegion = en;
|
|
||||||
hasScannedForEncodings = 0;
|
|
||||||
knownRegions = (
|
|
||||||
en,
|
|
||||||
Base,
|
|
||||||
);
|
|
||||||
mainGroup = C041F2B52AB021E30011752C;
|
|
||||||
packageReferences = (
|
|
||||||
C041F2D02AB0238F0011752C /* XCRemoteSwiftPackageReference "vapor" */,
|
|
||||||
C041F2D52AB023CA0011752C /* XCRemoteSwiftPackageReference "leaf" */,
|
|
||||||
);
|
|
||||||
productRefGroup = C041F2BF2AB021E30011752C /* Products */;
|
|
||||||
projectDirPath = "";
|
|
||||||
projectRoot = "";
|
|
||||||
targets = (
|
|
||||||
C041F2BD2AB021E30011752C /* smparkin */,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
/* End PBXProject section */
|
|
||||||
|
|
||||||
/* Begin PBXResourcesBuildPhase section */
|
|
||||||
C041F2BC2AB021E30011752C /* Resources */ = {
|
|
||||||
isa = PBXResourcesBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
C0C8E2BE2AB0304D00E809B4 /* home.leaf in Resources */,
|
|
||||||
C0C8E2C12AB0304D00E809B4 /* 404.leaf in Resources */,
|
|
||||||
C041F2C92AB021E40011752C /* Preview Assets.xcassets in Resources */,
|
|
||||||
C041F2DA2AB0243C0011752C /* Public in Resources */,
|
|
||||||
C041F2C62AB021E40011752C /* Assets.xcassets in Resources */,
|
|
||||||
C0C8E2C02AB0304D00E809B4 /* welcome.leaf in Resources */,
|
|
||||||
C0C8E2BF2AB0304D00E809B4 /* privacy.leaf in Resources */,
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
/* End PBXResourcesBuildPhase section */
|
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
|
||||||
C041F2BA2AB021E30011752C /* Sources */ = {
|
|
||||||
isa = PBXSourcesBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
C041F2DF2AB0246C0011752C /* Server.swift in Sources */,
|
|
||||||
C041F2C42AB021E30011752C /* ContentView.swift in Sources */,
|
|
||||||
C0C8E2C52AB03D6200E809B4 /* Log.swift in Sources */,
|
|
||||||
C041F2C22AB021E30011752C /* smparkinApp.swift in Sources */,
|
|
||||||
C041F2DD2AB024510011752C /* Routes.swift in Sources */,
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
/* End PBXSourcesBuildPhase section */
|
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration section */
|
|
||||||
C041F2CA2AB021E40011752C /* Debug */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
|
||||||
CLANG_ENABLE_MODULES = YES;
|
|
||||||
CLANG_ENABLE_OBJC_ARC = YES;
|
|
||||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
|
||||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
|
||||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_COMMA = YES;
|
|
||||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
|
||||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
|
||||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
|
||||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
|
||||||
CLANG_WARN_EMPTY_BODY = YES;
|
|
||||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
|
||||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
|
||||||
CLANG_WARN_INT_CONVERSION = YES;
|
|
||||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
|
||||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
|
||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
|
||||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
|
||||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
|
||||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
|
||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
|
||||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
|
||||||
COPY_PHASE_STRIP = NO;
|
|
||||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
|
||||||
ENABLE_TESTABILITY = YES;
|
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
|
||||||
GCC_DYNAMIC_NO_PIC = NO;
|
|
||||||
GCC_NO_COMMON_BLOCKS = YES;
|
|
||||||
GCC_OPTIMIZATION_LEVEL = 0;
|
|
||||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
|
||||||
"DEBUG=1",
|
|
||||||
"$(inherited)",
|
|
||||||
);
|
|
||||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
|
||||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
|
||||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
|
||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
|
||||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
|
||||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
|
||||||
MTL_FAST_MATH = YES;
|
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
|
||||||
SDKROOT = iphoneos;
|
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
|
||||||
};
|
|
||||||
name = Debug;
|
|
||||||
};
|
|
||||||
C041F2CB2AB021E40011752C /* Release */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
|
||||||
CLANG_ENABLE_MODULES = YES;
|
|
||||||
CLANG_ENABLE_OBJC_ARC = YES;
|
|
||||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
|
||||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
|
||||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_COMMA = YES;
|
|
||||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
|
||||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
|
||||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
|
||||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
|
||||||
CLANG_WARN_EMPTY_BODY = YES;
|
|
||||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
|
||||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
|
||||||
CLANG_WARN_INT_CONVERSION = YES;
|
|
||||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
|
||||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
|
||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
|
||||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
|
||||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
|
||||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
|
||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
|
||||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
|
||||||
COPY_PHASE_STRIP = NO;
|
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
|
||||||
ENABLE_NS_ASSERTIONS = NO;
|
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
|
||||||
GCC_NO_COMMON_BLOCKS = YES;
|
|
||||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
|
||||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
|
||||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
|
||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
|
||||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
|
||||||
MTL_FAST_MATH = YES;
|
|
||||||
SDKROOT = iphoneos;
|
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
|
||||||
VALIDATE_PRODUCT = YES;
|
|
||||||
};
|
|
||||||
name = Release;
|
|
||||||
};
|
|
||||||
C041F2CD2AB021E40011752C /* Debug */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
|
||||||
CODE_SIGN_STYLE = Automatic;
|
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"smparkin/Preview Content\"";
|
|
||||||
DEVELOPMENT_TEAM = QR825BQSTL;
|
|
||||||
ENABLE_PREVIEWS = YES;
|
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
|
||||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
|
||||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
|
||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
|
||||||
"$(inherited)",
|
|
||||||
"@executable_path/Frameworks",
|
|
||||||
);
|
|
||||||
MARKETING_VERSION = 1.0;
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = in.smpark.smparkin;
|
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
|
||||||
SWIFT_VERSION = 5.0;
|
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
|
||||||
};
|
|
||||||
name = Debug;
|
|
||||||
};
|
|
||||||
C041F2CE2AB021E40011752C /* Release */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
|
||||||
CODE_SIGN_STYLE = Automatic;
|
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"smparkin/Preview Content\"";
|
|
||||||
DEVELOPMENT_TEAM = QR825BQSTL;
|
|
||||||
ENABLE_PREVIEWS = YES;
|
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
|
||||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
|
||||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
|
||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
|
||||||
"$(inherited)",
|
|
||||||
"@executable_path/Frameworks",
|
|
||||||
);
|
|
||||||
MARKETING_VERSION = 1.0;
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = in.smpark.smparkin;
|
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
|
||||||
SWIFT_VERSION = 5.0;
|
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
|
||||||
};
|
|
||||||
name = Release;
|
|
||||||
};
|
|
||||||
/* End XCBuildConfiguration section */
|
|
||||||
|
|
||||||
/* Begin XCConfigurationList section */
|
|
||||||
C041F2B92AB021E30011752C /* Build configuration list for PBXProject "smparkin" */ = {
|
|
||||||
isa = XCConfigurationList;
|
|
||||||
buildConfigurations = (
|
|
||||||
C041F2CA2AB021E40011752C /* Debug */,
|
|
||||||
C041F2CB2AB021E40011752C /* Release */,
|
|
||||||
);
|
|
||||||
defaultConfigurationIsVisible = 0;
|
|
||||||
defaultConfigurationName = Release;
|
|
||||||
};
|
|
||||||
C041F2CC2AB021E40011752C /* Build configuration list for PBXNativeTarget "smparkin" */ = {
|
|
||||||
isa = XCConfigurationList;
|
|
||||||
buildConfigurations = (
|
|
||||||
C041F2CD2AB021E40011752C /* Debug */,
|
|
||||||
C041F2CE2AB021E40011752C /* Release */,
|
|
||||||
);
|
|
||||||
defaultConfigurationIsVisible = 0;
|
|
||||||
defaultConfigurationName = Release;
|
|
||||||
};
|
|
||||||
/* End XCConfigurationList section */
|
|
||||||
|
|
||||||
/* Begin XCRemoteSwiftPackageReference section */
|
|
||||||
C041F2D02AB0238F0011752C /* XCRemoteSwiftPackageReference "vapor" */ = {
|
|
||||||
isa = XCRemoteSwiftPackageReference;
|
|
||||||
repositoryURL = "https://github.com/vapor/vapor.git";
|
|
||||||
requirement = {
|
|
||||||
kind = upToNextMajorVersion;
|
|
||||||
minimumVersion = 4.81.0;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
C041F2D52AB023CA0011752C /* XCRemoteSwiftPackageReference "leaf" */ = {
|
|
||||||
isa = XCRemoteSwiftPackageReference;
|
|
||||||
repositoryURL = "https://github.com/vapor/leaf.git";
|
|
||||||
requirement = {
|
|
||||||
kind = upToNextMajorVersion;
|
|
||||||
minimumVersion = 4.2.4;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
/* End XCRemoteSwiftPackageReference section */
|
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
|
||||||
C041F2D12AB0238F0011752C /* Vapor */ = {
|
|
||||||
isa = XCSwiftPackageProductDependency;
|
|
||||||
package = C041F2D02AB0238F0011752C /* XCRemoteSwiftPackageReference "vapor" */;
|
|
||||||
productName = Vapor;
|
|
||||||
};
|
|
||||||
C041F2D62AB023CA0011752C /* Leaf */ = {
|
|
||||||
isa = XCSwiftPackageProductDependency;
|
|
||||||
package = C041F2D52AB023CA0011752C /* XCRemoteSwiftPackageReference "leaf" */;
|
|
||||||
productName = Leaf;
|
|
||||||
};
|
|
||||||
/* End XCSwiftPackageProductDependency section */
|
|
||||||
};
|
|
||||||
rootObject = C041F2B62AB021E30011752C /* Project object */;
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<Workspace
|
|
||||||
version = "1.0">
|
|
||||||
<FileRef
|
|
||||||
location = "self:">
|
|
||||||
</FileRef>
|
|
||||||
</Workspace>
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>IDEDidComputeMac32BitWarning</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
@@ -1,194 +0,0 @@
|
|||||||
{
|
|
||||||
"pins" : [
|
|
||||||
{
|
|
||||||
"identity" : "async-http-client",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/swift-server/async-http-client.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "16f7e62c08c6969899ce6cc277041e868364e5cf",
|
|
||||||
"version" : "1.19.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "async-kit",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/vapor/async-kit.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "eab9edff78e8ace20bd7cb6e792ab46d54f59ab9",
|
|
||||||
"version" : "1.18.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "console-kit",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/vapor/console-kit.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "9a12000f4064a2bdc49068d7258292ec1bdc88fc",
|
|
||||||
"version" : "4.7.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "leaf",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/vapor/leaf.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "6fe0e843c6599f5189e45c7b08739ebc5c410c3b",
|
|
||||||
"version" : "4.2.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "leaf-kit",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/vapor/leaf-kit.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "13f2fc4c8479113cd23876d9a434ef4573e368bb",
|
|
||||||
"version" : "1.10.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "multipart-kit",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/vapor/multipart-kit.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "1adfd69df2da08f7931d4281b257475e32c96734",
|
|
||||||
"version" : "4.5.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "routing-kit",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/vapor/routing-kit.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "e0539da5b60a60d7381f44cdcf04036f456cee2f",
|
|
||||||
"version" : "4.8.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "swift-algorithms",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/apple/swift-algorithms.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "b14b7f4c528c942f121c8b860b9410b2bf57825e",
|
|
||||||
"version" : "1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "swift-atomics",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/apple/swift-atomics.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "6c89474e62719ddcc1e9614989fff2f68208fe10",
|
|
||||||
"version" : "1.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "swift-collections",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/apple/swift-collections.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2",
|
|
||||||
"version" : "1.0.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "swift-crypto",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/apple/swift-crypto.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "60f13f60c4d093691934dc6cfdf5f508ada1f894",
|
|
||||||
"version" : "2.6.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "swift-log",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/apple/swift-log.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "532d8b529501fb73a2455b179e0bbb6d49b652ed",
|
|
||||||
"version" : "1.5.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "swift-metrics",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/apple/swift-metrics.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "971ba26378ab69c43737ee7ba967a896cb74c0d1",
|
|
||||||
"version" : "2.4.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "swift-nio",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/apple/swift-nio.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "cf281631ff10ec6111f2761052aa81896a83a007",
|
|
||||||
"version" : "2.58.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "swift-nio-extras",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/apple/swift-nio-extras.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "0e0d0aab665ff1a0659ce75ac003081f2b1c8997",
|
|
||||||
"version" : "1.19.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "swift-nio-http2",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/apple/swift-nio-http2.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "a8ccf13fa62775277a5d56844878c828bbb3be1a",
|
|
||||||
"version" : "1.27.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "swift-nio-ssl",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/apple/swift-nio-ssl.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "320bd978cceb8e88c125dcbb774943a92f6286e9",
|
|
||||||
"version" : "2.25.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "swift-nio-transport-services",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/apple/swift-nio-transport-services.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "e7403c35ca6bb539a7ca353b91cc2d8ec0362d58",
|
|
||||||
"version" : "1.19.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "swift-numerics",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/apple/swift-numerics",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "0a5bc04095a675662cf24757cc0640aa2204253b",
|
|
||||||
"version" : "1.0.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "vapor",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/vapor/vapor.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "03a08f6e88d5ca8c1cfd84f8367b21dfe050d082",
|
|
||||||
"version" : "4.81.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "websocket-kit",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/vapor/websocket-kit.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "53fe0639a98903858d0196b699720decb42aee7b",
|
|
||||||
"version" : "2.14.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"version" : 2
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>SchemeUserState</key>
|
|
||||||
<dict>
|
|
||||||
<key>smparkin.xcscheme_^#shared#^_</key>
|
|
||||||
<dict>
|
|
||||||
<key>orderHint</key>
|
|
||||||
<integer>0</integer>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"colors" : [
|
|
||||||
{
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"platform" : "ios",
|
|
||||||
"size" : "1024x1024"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
//
|
|
||||||
// ContentView.swift
|
|
||||||
// smparkin
|
|
||||||
//
|
|
||||||
// Created by Stephen Parkinson on 9/11/23.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct ContentView: View {
|
|
||||||
@StateObject var server: Server = Server(port: 8080)
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack {
|
|
||||||
Text(ProcessInfo().hostName + ":\(server.port)")
|
|
||||||
List(server.logs.reversed(), id: \.self) { log in
|
|
||||||
Text(log)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.onAppear {
|
|
||||||
server.start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview {
|
|
||||||
ContentView()
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
//
|
|
||||||
// Log.swift
|
|
||||||
// smparkin
|
|
||||||
//
|
|
||||||
// Created by Stephen Parkinson on 9/11/23.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Vapor
|
|
||||||
|
|
||||||
struct LogMiddleware: Middleware {
|
|
||||||
let server: Server
|
|
||||||
|
|
||||||
init(server: Server) {
|
|
||||||
self.server = server
|
|
||||||
}
|
|
||||||
|
|
||||||
func respond(to request: Request, chainingTo next: Responder) -> EventLoopFuture<Response> {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
server.logs.append("\(server.dateFormatter.string(from: Date.now)) \(request.url.path)")
|
|
||||||
}
|
|
||||||
return next.respond(to: request)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
* {
|
|
||||||
-webkit-box-sizing: border-box;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#notfound {
|
|
||||||
position: relative;
|
|
||||||
height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
#notfound .notfound {
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
top: 50%;
|
|
||||||
-webkit-transform: translate(-50%, -50%);
|
|
||||||
-ms-transform: translate(-50%, -50%);
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.notfound {
|
|
||||||
max-width: 410px;
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notfound .notfound-404 {
|
|
||||||
height: 280px;
|
|
||||||
position: relative;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notfound .notfound-404 h1 {
|
|
||||||
font-family: 'Montserrat', sans-serif;
|
|
||||||
font-size: 230px;
|
|
||||||
margin: 0px;
|
|
||||||
font-weight: 900;
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
-webkit-transform: translateX(-50%);
|
|
||||||
-ms-transform: translateX(-50%);
|
|
||||||
transform: translateX(-50%);
|
|
||||||
background: url('../images/bg.jpg') no-repeat;
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-size: cover;
|
|
||||||
background-position: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.notfound h2 {
|
|
||||||
font-family: 'Montserrat', sans-serif;
|
|
||||||
color: #000;
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 700;
|
|
||||||
text-transform: uppercase;
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notfound p {
|
|
||||||
font-family: 'Montserrat', sans-serif;
|
|
||||||
color: #000;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 400;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
margin-top: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notfound a {
|
|
||||||
font-family: 'Montserrat', sans-serif;
|
|
||||||
font-size: 14px;
|
|
||||||
text-decoration: none;
|
|
||||||
text-transform: uppercase;
|
|
||||||
background: #0046d5;
|
|
||||||
display: inline-block;
|
|
||||||
padding: 15px 30px;
|
|
||||||
border-radius: 40px;
|
|
||||||
color: #fff;
|
|
||||||
font-weight: 700;
|
|
||||||
-webkit-box-shadow: 0px 4px 15px -5px #0046d5;
|
|
||||||
box-shadow: 0px 4px 15px -5px #0046d5;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@media only screen and (max-width: 767px) {
|
|
||||||
.notfound .notfound-404 {
|
|
||||||
height: 142px;
|
|
||||||
}
|
|
||||||
.notfound .notfound-404 h1 {
|
|
||||||
font-size: 112px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 497 KiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 466 KiB |
@@ -1,20 +0,0 @@
|
|||||||
import Vapor
|
|
||||||
|
|
||||||
// Register your application's routes here.
|
|
||||||
public func routes(_ app: Application) throws {
|
|
||||||
app.get("") { req async throws -> View in
|
|
||||||
return try await req.view.render("home")
|
|
||||||
}
|
|
||||||
app.get("index.html") { req async throws -> View in
|
|
||||||
return try await req.view.render("home")
|
|
||||||
}
|
|
||||||
app.get("privacy") { req async throws -> View in
|
|
||||||
return try await req.view.render("privacy")
|
|
||||||
}
|
|
||||||
app.get("welcome") { req async throws -> View in
|
|
||||||
return try await req.view.render("welcome")
|
|
||||||
}
|
|
||||||
app.get("**") { req async throws -> View in
|
|
||||||
return try await req.view.render("404")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
import Leaf
|
|
||||||
import Vapor
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
class Server: ObservableObject {
|
|
||||||
@Published var logs: [String] = [String]()
|
|
||||||
let dateFormatter = ISO8601DateFormatter()
|
|
||||||
var app: Application
|
|
||||||
let port: Int
|
|
||||||
|
|
||||||
init(port: Int) {
|
|
||||||
self.port = port
|
|
||||||
app = Application(.production)
|
|
||||||
configure(app)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Called before your application initializes.
|
|
||||||
private func configure(_ app: Application) {
|
|
||||||
app.http.server.configuration.hostname = "0.0.0.0"
|
|
||||||
app.http.server.configuration.port = port
|
|
||||||
|
|
||||||
//leaf
|
|
||||||
app.views.use(.leaf)
|
|
||||||
app.leaf.cache.isEnabled = app.environment.isRelease
|
|
||||||
app.leaf.configuration.rootDirectory = Bundle.main.bundlePath
|
|
||||||
|
|
||||||
//Register middleware
|
|
||||||
app.middleware.use(FileMiddleware(publicDirectory: "\(Bundle.main.bundlePath)/\(app.directory.publicDirectory)"))
|
|
||||||
app.middleware.use(ErrorMiddleware.default(environment: app.environment))
|
|
||||||
app.middleware.use(LogMiddleware.init(server: self))
|
|
||||||
|
|
||||||
//routes
|
|
||||||
do {
|
|
||||||
try routes(app)
|
|
||||||
} catch {
|
|
||||||
fatalError(error.localizedDescription)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func start() {
|
|
||||||
Task(priority: .background) {
|
|
||||||
do {
|
|
||||||
try app.start()
|
|
||||||
} catch {
|
|
||||||
fatalError(error.localizedDescription)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func stop() {
|
|
||||||
app.shutdown()
|
|
||||||
}
|
|
||||||
|
|
||||||
func restart() {
|
|
||||||
stop()
|
|
||||||
start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
|
|
||||||
|
|
||||||
<title>404</title>
|
|
||||||
|
|
||||||
<!-- Google font -->
|
|
||||||
<link href="https://fonts.googleapis.com/css?family=Montserrat:400,700,900" rel="stylesheet">
|
|
||||||
|
|
||||||
<!-- Custom stlylesheet -->
|
|
||||||
<link type="text/css" rel="stylesheet" href="/css/404style.css" />
|
|
||||||
|
|
||||||
<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
|
|
||||||
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
|
|
||||||
<!--[if lt IE 9]>
|
|
||||||
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
|
|
||||||
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
|
|
||||||
<![endif]-->
|
|
||||||
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<div id="notfound">
|
|
||||||
<div class="notfound">
|
|
||||||
<div class="notfound-404">
|
|
||||||
<h1>Oops!</h1>
|
|
||||||
</div>
|
|
||||||
<h2>404 - Page not found</h2>
|
|
||||||
<p>The page you are looking for might have been removed had its name changed or is temporarily unavailable.</p>
|
|
||||||
<a href="https://smpark.in">Home</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</body><!-- This templates was made by Colorlib (https://colorlib.com) -->
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Stephen Parkinson</title>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
|
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Raleway">
|
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
|
|
||||||
<style>
|
|
||||||
body,h1,h2,h3,h4,h5,h6 {font-family: "Raleway", sans-serif}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body class="w3-light-grey w3-content" style="max-width:1600px">
|
|
||||||
|
|
||||||
<!-- Sidebar/menu -->
|
|
||||||
<nav class="w3-sidebar w3-collapse w3-white w3-animate-left" style="z-index:3;width:300px;" id="mySidebar"><br>
|
|
||||||
<div class="w3-container">
|
|
||||||
<a href="#" onclick="w3_close()" class="w3-hide-large w3-right w3-jumbo w3-padding w3-hover-grey" title="close menu">
|
|
||||||
<i class="fa fa-remove"></i>
|
|
||||||
</a>
|
|
||||||
<img src="/images/me.jpg" style="width:45%;" class="w3-round"><br><br>
|
|
||||||
<h4><b>Stephen Parkinson</b></h4>
|
|
||||||
<p class="w3-text-grey">Software Engineer</p>
|
|
||||||
</div>
|
|
||||||
<div class="w3-bar-block">
|
|
||||||
<a href="#about" onclick="w3_close()" class="w3-bar-item w3-button w3-padding"><i class="fa fa-user fa-fw w3-margin-right"></i>ABOUT</a>
|
|
||||||
</div>
|
|
||||||
<div class="w3-panel w3-large">
|
|
||||||
<a href="https://www.linkedin.com/in/stephen-parkinson/">
|
|
||||||
<i class="fa fa-linkedin w3-hover-opacity"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<!-- Overlay effect when opening sidebar on small screens -->
|
|
||||||
<div class="w3-overlay w3-hide-large w3-animate-opacity" onclick="w3_close()" style="cursor:pointer" title="close side menu" id="myOverlay"></div>
|
|
||||||
|
|
||||||
<!-- !PAGE CONTENT! -->
|
|
||||||
<div class="w3-main" style="margin-left:300px">
|
|
||||||
|
|
||||||
<!-- Header -->
|
|
||||||
<header id="portfolio">
|
|
||||||
<a href="#"><img src="/images/me.jpg" style="width:65px;" class="w3-circle w3-right w3-margin w3-hide-large w3-hover-opacity"></a>
|
|
||||||
<span class="w3-button w3-hide-large w3-xxlarge w3-hover-text-grey" onclick="w3_open()"><i class="fa fa-bars"></i></span>
|
|
||||||
<div class="w3-container">
|
|
||||||
<h1><b></b></h1>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- About -->
|
|
||||||
<div class="w3-row-padding w3-padding-16" id="about">
|
|
||||||
<div class="w3-col m6">
|
|
||||||
<img src="/images/trees.jpg" alt="A picture I took" style="width:100%">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="w3-container w3-padding-large" style="margin-bottom:32px">
|
|
||||||
<h4><b>About Me</b></h4>
|
|
||||||
<p>I'm doing a lot of stuff with computers right now.</p>
|
|
||||||
<hr>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Contact Section -->
|
|
||||||
<div class="w3-container w3-padding-large w3-grey">
|
|
||||||
<h4 id="contact"><b>Contact Me</b></h4>
|
|
||||||
<div class="w3-row-padding w3-center w3-padding-24" style="margin:0 -16px">
|
|
||||||
<div class="w3-third w3-dark-grey">
|
|
||||||
<a href="mailto:stephen@smpark.in">
|
|
||||||
<p><i class="fa fa-envelope w3-xxlarge w3-text-light-grey"></i></p>
|
|
||||||
<p>stephen@smpark.in</p>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="w3-black w3-center w3-padding-24">Powered by <a href="https://www.w3schools.com/w3css/default.asp" title="W3.CSS" target="_blank" class="w3-hover-opacity">w3.css</a></div>
|
|
||||||
|
|
||||||
<!-- End page content -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// Script to open and close sidebar
|
|
||||||
function w3_open() {
|
|
||||||
document.getElementById("mySidebar").style.display = "block";
|
|
||||||
document.getElementById("myOverlay").style.display = "block";
|
|
||||||
}
|
|
||||||
|
|
||||||
function w3_close() {
|
|
||||||
document.getElementById("mySidebar").style.display = "none";
|
|
||||||
document.getElementById("myOverlay").style.display = "none";
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset='utf-8'>
|
|
||||||
<meta name='viewport' content='width=device-width'>
|
|
||||||
<title>Privacy Policy</title>
|
|
||||||
<style> body { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; padding:1em; } </style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<strong>Privacy Policy</strong>
|
|
||||||
<p>
|
|
||||||
Stephen Parkinson built the Monitorr app as
|
|
||||||
a Free app. This SERVICE is provided by
|
|
||||||
Stephen Parkinson at no cost and is intended for use as
|
|
||||||
is.
|
|
||||||
</p> <p>
|
|
||||||
This page is used to inform visitors regarding my
|
|
||||||
policies with the collection, use, and disclosure of Personal
|
|
||||||
Information if anyone decided to use my Service.
|
|
||||||
</p> <p>
|
|
||||||
If you choose to use my Service, then you agree to
|
|
||||||
the collection and use of information in relation to this
|
|
||||||
policy. The Personal Information that I collect is
|
|
||||||
used for providing and improving the Service. I will not use or share your information with
|
|
||||||
anyone except as described in this Privacy Policy.
|
|
||||||
</p> <p>
|
|
||||||
The terms used in this Privacy Policy have the same meanings
|
|
||||||
as in our Terms and Conditions, which are accessible at
|
|
||||||
Monitorr unless otherwise defined in this Privacy Policy.
|
|
||||||
</p> <p><strong>Information Collection and Use</strong></p> <p>
|
|
||||||
For a better experience, while using our Service, I
|
|
||||||
may require you to provide us with certain personally
|
|
||||||
identifiable information. The information that
|
|
||||||
I request will be retained on your device and is not collected by me in any way.
|
|
||||||
</p> <!----> <p><strong>Log Data</strong></p> <p>
|
|
||||||
I want to inform you that whenever you
|
|
||||||
use my Service, in a case of an error in the app
|
|
||||||
I collect data and information (through third-party
|
|
||||||
products) on your phone called Log Data. This Log Data may
|
|
||||||
include information such as your device Internet Protocol
|
|
||||||
(“IP”) address, device name, operating system version, the
|
|
||||||
configuration of the app when utilizing my Service,
|
|
||||||
the time and date of your use of the Service, and other
|
|
||||||
statistics.
|
|
||||||
</p> <p><strong>Cookies</strong></p> <p>
|
|
||||||
Cookies are files with a small amount of data that are
|
|
||||||
commonly used as anonymous unique identifiers. These are sent
|
|
||||||
to your browser from the websites that you visit and are
|
|
||||||
stored on your device's internal memory.
|
|
||||||
</p> <p>
|
|
||||||
This Service does not use these “cookies” explicitly. However,
|
|
||||||
the app may use third-party code and libraries that use
|
|
||||||
“cookies” to collect information and improve their services.
|
|
||||||
You have the option to either accept or refuse these cookies
|
|
||||||
and know when a cookie is being sent to your device. If you
|
|
||||||
choose to refuse our cookies, you may not be able to use some
|
|
||||||
portions of this Service.
|
|
||||||
</p> <p><strong>Service Providers</strong></p> <p>
|
|
||||||
I may employ third-party companies and
|
|
||||||
individuals due to the following reasons:
|
|
||||||
</p> <ul><li>To facilitate our Service;</li> <li>To provide the Service on our behalf;</li> <li>To perform Service-related services; or</li> <li>To assist us in analyzing how our Service is used.</li></ul> <p>
|
|
||||||
I want to inform users of this Service
|
|
||||||
that these third parties have access to their Personal
|
|
||||||
Information. The reason is to perform the tasks assigned to
|
|
||||||
them on our behalf. However, they are obligated not to
|
|
||||||
disclose or use the information for any other purpose.
|
|
||||||
</p> <p><strong>Security</strong></p> <p>
|
|
||||||
I value your trust in providing us your
|
|
||||||
Personal Information, thus we are striving to use commercially
|
|
||||||
acceptable means of protecting it. But remember that no method
|
|
||||||
of transmission over the internet, or method of electronic
|
|
||||||
storage is 100% secure and reliable, and I cannot
|
|
||||||
guarantee its absolute security.
|
|
||||||
</p> <p><strong>Links to Other Sites</strong></p> <p>
|
|
||||||
This Service may contain links to other sites. If you click on
|
|
||||||
a third-party link, you will be directed to that site. Note
|
|
||||||
that these external sites are not operated by me.
|
|
||||||
Therefore, I strongly advise you to review the
|
|
||||||
Privacy Policy of these websites. I have
|
|
||||||
no control over and assume no responsibility for the content,
|
|
||||||
privacy policies, or practices of any third-party sites or
|
|
||||||
services.
|
|
||||||
</p> <p><strong>Children’s Privacy</strong></p> <!----> <div><p>
|
|
||||||
I do not knowingly collect personally
|
|
||||||
identifiable information from children. I
|
|
||||||
encourage all children to never submit any personally
|
|
||||||
identifiable information through
|
|
||||||
the Application and/or Services.
|
|
||||||
I encourage parents and legal guardians to monitor
|
|
||||||
their children's Internet usage and to help enforce this Policy by instructing
|
|
||||||
their children never to provide personally identifiable information through the Application and/or Services without their permission. If you have reason to believe that a child
|
|
||||||
has provided personally identifiable information to us through the Application and/or Services,
|
|
||||||
please contact us. You must also be at least 16 years of age to consent to the processing
|
|
||||||
of your personally identifiable information in your country (in some countries we may allow your parent
|
|
||||||
or guardian to do so on your behalf).
|
|
||||||
</p></div> <p><strong>Changes to This Privacy Policy</strong></p> <p>
|
|
||||||
I may update our Privacy Policy from
|
|
||||||
time to time. Thus, you are advised to review this page
|
|
||||||
periodically for any changes. I will
|
|
||||||
notify you of any changes by posting the new Privacy Policy on
|
|
||||||
this page.
|
|
||||||
</p> <p>This policy is effective as of 2023-03-12</p> <p><strong>Contact Us</strong></p> <p>
|
|
||||||
If you have any questions or suggestions about my
|
|
||||||
Privacy Policy, do not hesitate to contact me at stephen@smpark.in.
|
|
||||||
</p> <p>This privacy policy page was created at <a href="https://privacypolicytemplate.net" target="_blank" rel="noopener noreferrer">privacypolicytemplate.net </a>and modified/generated by <a href="https://app-privacy-policy-generator.nisrulz.com/" target="_blank" rel="noopener noreferrer">App Privacy Policy Generator</a></p>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
//
|
|
||||||
// smparkinApp.swift
|
|
||||||
// smparkin
|
|
||||||
//
|
|
||||||
// Created by Stephen Parkinson on 9/11/23.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
@main
|
|
||||||
struct smparkinApp: App {
|
|
||||||
var body: some Scene {
|
|
||||||
WindowGroup {
|
|
||||||
ContentView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||