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()
|
||||
}
|
||||
}
|
||||
}
|
||||