refactor: move gateway package to app directory

This commit is contained in:
Prad Nukala
2024-12-09 18:34:38 -05:00
parent 67432d3fdf
commit f250082fff
32 changed files with 13 additions and 13 deletions
+111
View File
@@ -0,0 +1,111 @@
// Code generated from Pkl module `sonr.hway.Env`. DO NOT EDIT.
package config
import (
"context"
"github.com/apple/pkl-go/pkl"
)
type Env interface {
GetServePort() int
GetConfigDir() string
GetSqliteFile() string
GetChainId() string
GetIpfsGatewayUrl() string
GetSonrApiUrl() string
GetSonrGrpcUrl() string
GetSonrRpcUrl() string
GetTurnstileSiteKey() string
}
var _ Env = (*EnvImpl)(nil)
type EnvImpl struct {
ServePort int `pkl:"servePort"`
ConfigDir string `pkl:"configDir"`
SqliteFile string `pkl:"sqliteFile"`
ChainId string `pkl:"chainId"`
IpfsGatewayUrl string `pkl:"ipfsGatewayUrl"`
SonrApiUrl string `pkl:"sonrApiUrl"`
SonrGrpcUrl string `pkl:"sonrGrpcUrl"`
SonrRpcUrl string `pkl:"sonrRpcUrl"`
TurnstileSiteKey string `pkl:"turnstileSiteKey"`
}
func (rcv *EnvImpl) GetServePort() int {
return rcv.ServePort
}
func (rcv *EnvImpl) GetConfigDir() string {
return rcv.ConfigDir
}
func (rcv *EnvImpl) GetSqliteFile() string {
return rcv.SqliteFile
}
func (rcv *EnvImpl) GetChainId() string {
return rcv.ChainId
}
func (rcv *EnvImpl) GetIpfsGatewayUrl() string {
return rcv.IpfsGatewayUrl
}
func (rcv *EnvImpl) GetSonrApiUrl() string {
return rcv.SonrApiUrl
}
func (rcv *EnvImpl) GetSonrGrpcUrl() string {
return rcv.SonrGrpcUrl
}
func (rcv *EnvImpl) GetSonrRpcUrl() string {
return rcv.SonrRpcUrl
}
func (rcv *EnvImpl) GetTurnstileSiteKey() string {
return rcv.TurnstileSiteKey
}
// LoadFromPath loads the pkl module at the given path and evaluates it into a Env
func LoadFromPath(ctx context.Context, path string) (ret Env, err error) {
evaluator, err := pkl.NewEvaluator(ctx, pkl.PreconfiguredOptions)
if err != nil {
return nil, err
}
defer func() {
cerr := evaluator.Close()
if err == nil {
err = cerr
}
}()
ret, err = Load(ctx, evaluator, pkl.FileSource(path))
return ret, err
}
// Load loads the pkl module at the given source and evaluates it with the given evaluator into a Env
func Load(ctx context.Context, evaluator pkl.Evaluator, source *pkl.ModuleSource) (Env, error) {
var ret EnvImpl
if err := evaluator.EvaluateModule(ctx, source, &ret); err != nil {
return nil, err
}
return &ret, nil
}
+45
View File
@@ -0,0 +1,45 @@
package config
import (
"context"
"github.com/apple/pkl-go/pkl"
)
// LoadFromBytes loads the environment from the given bytes
func LoadFromBytes(data []byte) (Env, error) {
text := string(data)
return LoadFromString(text)
}
// LoadFromString loads the environment from the given string
func LoadFromString(text string) (Env, error) {
evaluator, err := pkl.NewEvaluator(context.Background(), pkl.PreconfiguredOptions)
if err != nil {
return nil, err
}
defer func() {
cerr := evaluator.Close()
if err == nil {
err = cerr
}
}()
ret, err := Load(context.Background(), evaluator, pkl.TextSource(text))
return ret, err
}
// LoadFromURL loads the environment from the given URL
func LoadFromURL(url string) (Env, error) {
evaluator, err := pkl.NewEvaluator(context.Background(), pkl.PreconfiguredOptions)
if err != nil {
return nil, err
}
defer func() {
cerr := evaluator.Close()
if err == nil {
err = cerr
}
}()
ret, err := Load(context.Background(), evaluator, pkl.UriSource(url))
return ret, err
}
+8
View File
@@ -0,0 +1,8 @@
// Code generated from Pkl module `sonr.hway.Env`. DO NOT EDIT.
package config
import "github.com/apple/pkl-go/pkl"
func init() {
pkl.RegisterMapping("sonr.hway.Env", EnvImpl{})
}
+138
View File
@@ -0,0 +1,138 @@
<!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.0" />
<title>Sonr DWN</title>
<!-- HTMX -->
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<!-- WASM Support -->
<script src="https://cdn.jsdelivr.net/gh/golang/go@go1.22.5/misc/wasm/wasm_exec.js"></script>
<!-- Main JS -->
<script src="main.js"></script>
<!-- Tailwind (assuming you're using it based on your classes) -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Add manifest for PWA support -->
<link
rel="manifest"
href="/app.webmanifest"
crossorigin="use-credentials"
/>
<!-- Offline detection styles -->
<style>
.offline-indicator {
display: none;
}
body.offline .offline-indicator {
display: block;
background: #f44336;
color: white;
text-align: center;
padding: 0.5rem;
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
}
</style>
</head>
<body
class="flex items-center justify-center h-full bg-zinc-50 lg:p-24 md:16 p-4"
>
<!-- Offline indicator -->
<div class="offline-indicator">
You are currently offline. Some features may be limited.
</div>
<!-- Loading indicator -->
<div
id="loading-indicator"
class="fixed top-0 left-0 w-full h-1 bg-blue-200 transition-all duration-300"
style="display: none"
>
<div class="h-full bg-blue-600 w-0 transition-all duration-300"></div>
</div>
<main
class="flex-row items-center justify-center mx-auto w-fit max-w-screen-sm gap-y-3"
>
<div
id="content"
hx-get="/#"
hx-trigger="load"
hx-swap="outerHTML"
hx-indicator="#loading-indicator"
>
Loading...
</div>
</main>
<!-- WASM Ready Indicator (hidden) -->
<div
id="wasm-status"
class="hidden fixed bottom-4 right-4 p-2 rounded-md bg-green-500 text-white"
hx-swap-oob="true"
>
WASM Ready
</div>
<script>
// Initialize service worker
if ("serviceWorker" in navigator) {
window.addEventListener("load", async function () {
try {
const registration =
await navigator.serviceWorker.register("/sw.js");
console.log(
"Service Worker registered with scope:",
registration.scope,
);
} catch (error) {
console.error("Service Worker registration failed:", error);
}
});
}
// HTMX loading indicator
htmx.on("htmx:beforeRequest", function (evt) {
document.getElementById("loading-indicator").style.display = "block";
});
htmx.on("htmx:afterRequest", function (evt) {
document.getElementById("loading-indicator").style.display = "none";
});
// WASM ready event handler
document.addEventListener("wasm-ready", function () {
const status = document.getElementById("wasm-status");
status.classList.remove("hidden");
setTimeout(() => {
status.classList.add("hidden");
}, 3000);
});
// Offline status handler
window.addEventListener("offline", function () {
document.body.classList.add("offline");
});
window.addEventListener("online", function () {
document.body.classList.remove("offline");
});
// Initial offline check
if (!navigator.onLine) {
document.body.classList.add("offline");
}
</script>
</body>
</html>
+152
View File
@@ -0,0 +1,152 @@
// MessageChannel for WASM communication
let wasmChannel;
let wasmPort;
async function initWasmChannel() {
wasmChannel = new MessageChannel();
wasmPort = wasmChannel.port1;
// Setup message handling from WASM
wasmPort.onmessage = (event) => {
const { type, data } = event.data;
switch (type) {
case 'WASM_READY':
console.log('WASM is ready');
document.dispatchEvent(new CustomEvent('wasm-ready'));
break;
case 'RESPONSE':
handleWasmResponse(data);
break;
case 'SYNC_COMPLETE':
handleSyncComplete(data);
break;
}
};
}
// Initialize WebAssembly and Service Worker
async function init() {
try {
// Register service worker
if ('serviceWorker' in navigator) {
const registration = await navigator.serviceWorker.register('./sw.js');
console.log('ServiceWorker registered');
// Wait for the service worker to be ready
await navigator.serviceWorker.ready;
// Initialize MessageChannel
await initWasmChannel();
// Send the MessageChannel port to the service worker
navigator.serviceWorker.controller.postMessage({
type: 'PORT_INITIALIZATION',
port: wasmChannel.port2
}, [wasmChannel.port2]);
// Register for periodic sync if available
if ('periodicSync' in registration) {
try {
await registration.periodicSync.register('wasm-sync', {
minInterval: 24 * 60 * 60 * 1000 // 24 hours
});
} catch (error) {
console.log('Periodic sync could not be registered:', error);
}
}
}
// Initialize HTMX with custom config
htmx.config.withCredentials = true;
htmx.config.wsReconnectDelay = 'full-jitter';
// Override HTMX's internal request handling
htmx.config.beforeRequest = function (config) {
// Add request ID for tracking
const requestId = 'req_' + Date.now();
config.headers['X-Wasm-Request-ID'] = requestId;
// If offline, handle through service worker
if (!navigator.onLine) {
return false; // Let service worker handle it
}
return true;
};
// Handle HTMX after request
htmx.config.afterRequest = function (config) {
// Additional processing after request if needed
};
// Handle HTMX errors
htmx.config.errorHandler = function (error) {
console.error('HTMX Error:', error);
};
} catch (error) {
console.error('Initialization failed:', error);
}
}
function handleWasmResponse(data) {
const { requestId, response } = data;
// Process the WASM response
// This might update the UI or trigger HTMX swaps
const targetElement = document.querySelector(`[data-request-id="${requestId}"]`);
if (targetElement) {
htmx.process(targetElement);
}
}
function handleSyncComplete(data) {
const { url } = data;
// Handle successful sync
// Maybe refresh the relevant part of the UI
htmx.trigger('body', 'sync:complete', { url });
}
// Handle offline status changes
window.addEventListener('online', () => {
document.body.classList.remove('offline');
// Trigger sync when back online
if (wasmPort) {
wasmPort.postMessage({ type: 'SYNC_REQUEST' });
}
});
window.addEventListener('offline', () => {
document.body.classList.add('offline');
});
// Custom event handlers for HTMX
document.addEventListener('htmx:beforeRequest', (event) => {
const { elt, xhr } = event.detail;
// Add request tracking
const requestId = xhr.headers['X-Wasm-Request-ID'];
elt.setAttribute('data-request-id', requestId);
});
document.addEventListener('htmx:afterRequest', (event) => {
const { elt, successful } = event.detail;
if (successful) {
elt.removeAttribute('data-request-id');
}
});
// Initialize everything when the page loads
document.addEventListener('DOMContentLoaded', init);
// Export functions that might be needed by WASM
window.wasmBridge = {
triggerUIUpdate: function (selector, content) {
const target = document.querySelector(selector);
if (target) {
htmx.process(htmx.parse(content).forEach(node => target.appendChild(node)));
}
},
showNotification: function (message, type = 'info') {
// Implement notification system
console.log(`${type}: ${message}`);
}
};
+14
View File
@@ -0,0 +1,14 @@
package internal
import (
_ "embed"
)
//go:embed index.html
var IndexHTML []byte
//go:embed main.js
var MainJS []byte
//go:embed sw.js
var WorkerJS []byte
+258
View File
@@ -0,0 +1,258 @@
// Cache names for different types of resources
const CACHE_NAMES = {
wasm: 'wasm-cache-v1',
static: 'static-cache-v1',
dynamic: 'dynamic-cache-v1'
};
// Import required scripts
importScripts(
"https://cdn.jsdelivr.net/gh/golang/go@go1.22.5/misc/wasm/wasm_exec.js",
"https://cdn.jsdelivr.net/gh/nlepage/go-wasm-http-server@v1.1.0/sw.js",
);
// Initialize WASM HTTP listener
const wasmInstance = registerWasmHTTPListener("https://cdn.sonr.id/wasm/app.wasm");
// MessageChannel port for WASM communication
let wasmPort;
// Request queue for offline operations
let requestQueue = new Map();
// Setup message channel handler
self.addEventListener('message', async (event) => {
if (event.data.type === 'PORT_INITIALIZATION') {
wasmPort = event.data.port;
setupWasmCommunication();
}
});
function setupWasmCommunication() {
wasmPort.onmessage = async (event) => {
const { type, data } = event.data;
switch (type) {
case 'WASM_REQUEST':
handleWasmRequest(data);
break;
case 'SYNC_REQUEST':
processSyncQueue();
break;
}
};
// Notify that WASM is ready
wasmPort.postMessage({ type: 'WASM_READY' });
}
// Enhanced install event
self.addEventListener("install", (event) => {
event.waitUntil(
Promise.all([
skipWaiting(),
// Cache WASM binary and essential resources
caches.open(CACHE_NAMES.wasm).then(cache =>
cache.addAll([
'https://cdn.sonr.id/wasm/app.wasm',
'https://cdn.jsdelivr.net/gh/golang/go@go1.22.5/misc/wasm/wasm_exec.js'
])
)
])
);
});
// Enhanced activate event
self.addEventListener("activate", (event) => {
event.waitUntil(
Promise.all([
clients.claim(),
// Clean up old caches
caches.keys().then(keys =>
Promise.all(
keys.map(key => {
if (!Object.values(CACHE_NAMES).includes(key)) {
return caches.delete(key);
}
})
)
)
])
);
});
// Intercept fetch events
self.addEventListener('fetch', (event) => {
const request = event.request;
// Handle API requests differently from static resources
if (request.url.includes('/api/')) {
event.respondWith(handleApiRequest(request));
} else {
event.respondWith(handleStaticRequest(request));
}
});
async function handleApiRequest(request) {
try {
// Try to make the request
const response = await fetch(request.clone());
// If successful, pass through WASM handler
if (response.ok) {
return await processWasmResponse(request, response);
}
// If offline or failed, queue the request
await queueRequest(request);
// Return cached response if available
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
// Return offline response
return new Response(
JSON.stringify({ error: 'Currently offline' }),
{
status: 503,
headers: { 'Content-Type': 'application/json' }
}
);
} catch (error) {
await queueRequest(request);
return new Response(
JSON.stringify({ error: 'Request failed' }),
{
status: 500,
headers: { 'Content-Type': 'application/json' }
}
);
}
}
async function handleStaticRequest(request) {
// Check cache first
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
try {
const response = await fetch(request);
// Cache successful responses
if (response.ok) {
const cache = await caches.open(CACHE_NAMES.static);
cache.put(request, response.clone());
}
return response;
} catch (error) {
// Return offline page for navigation requests
if (request.mode === 'navigate') {
return caches.match('/offline.html');
}
throw error;
}
}
async function processWasmResponse(request, response) {
// Clone the response before processing
const responseClone = response.clone();
try {
// Process through WASM
const processedResponse = await wasmInstance.processResponse(responseClone);
// Notify client through message channel
if (wasmPort) {
wasmPort.postMessage({
type: 'RESPONSE',
requestId: request.headers.get('X-Wasm-Request-ID'),
response: processedResponse
});
}
return processedResponse;
} catch (error) {
console.error('WASM processing error:', error);
return response;
}
}
async function queueRequest(request) {
const serializedRequest = await serializeRequest(request);
requestQueue.set(request.url, serializedRequest);
// Register for background sync
try {
await self.registration.sync.register('wasm-sync');
} catch (error) {
console.error('Sync registration failed:', error);
}
}
async function serializeRequest(request) {
const headers = {};
for (const [key, value] of request.headers.entries()) {
headers[key] = value;
}
return {
url: request.url,
method: request.method,
headers,
body: await request.text(),
timestamp: Date.now()
};
}
// Handle background sync
self.addEventListener('sync', (event) => {
if (event.tag === 'wasm-sync') {
event.waitUntil(processSyncQueue());
}
});
async function processSyncQueue() {
const requests = Array.from(requestQueue.values());
for (const serializedRequest of requests) {
try {
const response = await fetch(new Request(serializedRequest.url, {
method: serializedRequest.method,
headers: serializedRequest.headers,
body: serializedRequest.body
}));
if (response.ok) {
requestQueue.delete(serializedRequest.url);
// Notify client of successful sync
if (wasmPort) {
wasmPort.postMessage({
type: 'SYNC_COMPLETE',
url: serializedRequest.url
});
}
}
} catch (error) {
console.error('Sync failed for request:', error);
}
}
}
// Handle payment requests
self.addEventListener("canmakepayment", function (e) {
e.respondWith(Promise.resolve(true));
});
// Handle periodic sync if available
self.addEventListener('periodicsync', (event) => {
if (event.tag === 'wasm-sync') {
event.waitUntil(processSyncQueue());
}
});
+124
View File
@@ -0,0 +1,124 @@
package config
import "encoding/json"
func newWebManifestBytes() ([]byte, error) {
return json.Marshal(baseWebManifest)
}
var baseWebManifest = WebManifest{
Name: "Sonr Vault",
ShortName: "Sonr.ID",
StartURL: "/index.html",
Display: "standalone",
DisplayOverride: []string{
"fullscreen",
"minimal-ui",
},
Icons: []IconDefinition{
{
Src: "/icons/icon-192x192.png",
Sizes: "192x192",
Type: "image/png",
},
},
ServiceWorker: ServiceWorker{
Scope: "/",
Src: "/sw.js",
UseCache: true,
},
ProtocolHandlers: []ProtocolHandler{
{
Scheme: "did.sonr",
URL: "/resolve/sonr/%s",
},
{
Scheme: "did.eth",
URL: "/resolve/eth/%s",
},
{
Scheme: "did.btc",
URL: "/resolve/btc/%s",
},
{
Scheme: "did.usdc",
URL: "/resolve/usdc/%s",
},
{
Scheme: "did.ipfs",
URL: "/resolve/ipfs/%s",
},
},
}
type WebManifest struct {
// Required fields
Name string `json:"name"` // Full name of the application
ShortName string `json:"short_name"` // Short version of the name
// Display and appearance
Description string `json:"description,omitempty"` // Purpose and features of the application
Display string `json:"display,omitempty"` // Preferred display mode: fullscreen, standalone, minimal-ui, browser
DisplayOverride []string `json:"display_override,omitempty"`
ThemeColor string `json:"theme_color,omitempty"` // Default theme color for the application
BackgroundColor string `json:"background_color,omitempty"` // Background color during launch
Orientation string `json:"orientation,omitempty"` // Default orientation: any, natural, landscape, portrait
// URLs and scope
StartURL string `json:"start_url"` // Starting URL when launching
Scope string `json:"scope,omitempty"` // Navigation scope of the web application
ServiceWorker ServiceWorker `json:"service_worker,omitempty"`
// Icons
Icons []IconDefinition `json:"icons,omitempty"`
// Optional features
RelatedApplications []RelatedApplication `json:"related_applications,omitempty"`
PreferRelatedApplications bool `json:"prefer_related_applications,omitempty"`
Shortcuts []Shortcut `json:"shortcuts,omitempty"`
// Experimental features (uncomment if needed)
FileHandlers []FileHandler `json:"file_handlers,omitempty"`
ProtocolHandlers []ProtocolHandler `json:"protocol_handlers,omitempty"`
}
type FileHandler struct {
Action string `json:"action"`
Accept map[string][]string `json:"accept"`
}
type LaunchHandler struct {
Action string `json:"action"`
}
type IconDefinition struct {
Src string `json:"src"`
Sizes string `json:"sizes"`
Type string `json:"type,omitempty"`
Purpose string `json:"purpose,omitempty"`
}
type ProtocolHandler struct {
Scheme string `json:"scheme"`
URL string `json:"url"`
}
type RelatedApplication struct {
Platform string `json:"platform"`
URL string `json:"url,omitempty"`
ID string `json:"id,omitempty"`
}
type Shortcut struct {
Name string `json:"name"`
ShortName string `json:"short_name,omitempty"`
Description string `json:"description,omitempty"`
URL string `json:"url"`
Icons []IconDefinition `json:"icons,omitempty"`
}
type ServiceWorker struct {
Scope string `json:"scope"`
Src string `json:"src"`
UseCache bool `json:"use_cache"`
}
+54
View File
@@ -0,0 +1,54 @@
package config
import (
"reflect"
"strings"
"github.com/onsonr/sonr/pkg/common/models"
"github.com/onsonr/sonr/pkg/vault/types"
)
// DefaultSchema returns the default schema
func DefaultSchema() *types.Schema {
return &types.Schema{
Version: SchemaVersion,
Account: getSchema(&models.Account{}),
Asset: getSchema(&models.Asset{}),
Chain: getSchema(&models.Chain{}),
Credential: getSchema(&models.Credential{}),
Grant: getSchema(&models.Grant{}),
Keyshare: getSchema(&models.Keyshare{}),
Profile: getSchema(&models.Profile{}),
}
}
func getSchema(structType interface{}) string {
t := reflect.TypeOf(structType)
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
if t.Kind() != reflect.Struct {
return ""
}
var fields []string
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fieldName := toCamelCase(field.Name)
fields = append(fields, fieldName)
}
// Add "++" at the beginning, separated by a comma
return "++, " + strings.Join(fields, ", ")
}
func toCamelCase(s string) string {
if s == "" {
return s
}
if len(s) == 1 {
return strings.ToLower(s)
}
return strings.ToLower(s[:1]) + s[1:]
}
+50
View File
@@ -0,0 +1,50 @@
package config
import (
"encoding/json"
"github.com/ipfs/boxo/files"
"github.com/onsonr/sonr/app/gateway/config/internal"
"github.com/onsonr/sonr/pkg/vault/types"
)
const SchemaVersion = 1
const (
AppManifestFileName = "app.webmanifest"
DWNConfigFileName = "dwn.json"
IndexHTMLFileName = "index.html"
MainJSFileName = "main.js"
ServiceWorkerFileName = "sw.js"
)
// spawnVaultDirectory creates a new directory with the default files
func NewFS(cfg *types.Config) (files.Directory, error) {
manifestBz, err := newWebManifestBytes()
if err != nil {
return nil, err
}
cnfBz, err := json.Marshal(cfg)
if err != nil {
return nil, err
}
return files.NewMapDirectory(map[string]files.Node{
AppManifestFileName: files.NewBytesFile(manifestBz),
DWNConfigFileName: files.NewBytesFile(cnfBz),
IndexHTMLFileName: files.NewBytesFile(internal.IndexHTML),
MainJSFileName: files.NewBytesFile(internal.MainJS),
ServiceWorkerFileName: files.NewBytesFile(internal.WorkerJS),
}), nil
}
// GetVaultConfig returns the default vault config
func GetVaultConfig(addr string, ucanCID string) *types.Config {
return &types.Config{
MotrToken: ucanCID,
MotrAddress: addr,
IpfsGatewayUrl: "http://localhost:80",
SonrApiUrl: "http://localhost:1317",
SonrRpcUrl: "http://localhost:26657",
SonrChainId: "sonr-testnet-1",
VaultSchema: DefaultSchema(),
}
}
+49
View File
@@ -0,0 +1,49 @@
package handlers
import (
"github.com/labstack/echo/v4"
"github.com/onsonr/sonr/app/gateway/internal/pages/index"
"github.com/onsonr/sonr/app/gateway/internal/session"
"github.com/onsonr/sonr/pkg/common/response"
)
func HandleIndex(c echo.Context) error {
if isExpired(c) {
return response.TemplEcho(c, index.ReturningView())
}
return response.TemplEcho(c, index.InitialView())
}
// ╭─────────────────────────────────────────────────────────╮
// │ Utility Functions │
// ╰─────────────────────────────────────────────────────────╯
// Initial users have no authorization, user handle, or vault address
func isInitial(c echo.Context) bool {
sess, err := session.Get(c)
if err != nil {
return false
}
data := sess.Session()
return data.UserHandle == "" && data.VaultAddress == ""
}
// Expired users have either a user handle or vault address
func isExpired(c echo.Context) bool {
sess, err := session.Get(c)
if err != nil {
return false
}
data := sess.Session()
return data.UserHandle != "" || data.VaultAddress != ""
}
// Returning users have a valid authorization, and either a user handle or vault address
func isReturning(c echo.Context) bool {
sess, err := session.Get(c)
if err != nil {
return false
}
data := sess.Session()
return data.UserHandle != "" && data.VaultAddress != ""
}
+24
View File
@@ -0,0 +1,24 @@
package handlers
import "github.com/labstack/echo/v4"
func HandleLogin(c echo.Context) error {
return nil
}
//
// func LoginHandler(c echo.Context) error {
// options := PublicKeyCredentialRequestOptions{
// Challenge: "your-challenge-base64url",
// RpID: "yourdomain.com",
// Timeout: 60000,
// AllowCredentials: []CredentialDescriptor{
// {
// Type: "public-key",
// ID: "credential-id-base64url",
// },
// },
// }
//
// return GetCredentials(c, options)
// }
+98
View File
@@ -0,0 +1,98 @@
package handlers
import (
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"github.com/go-webauthn/webauthn/protocol"
"github.com/labstack/echo/v4"
"github.com/onsonr/sonr/app/gateway/internal/database"
"github.com/onsonr/sonr/app/gateway/internal/pages/register"
"github.com/onsonr/sonr/crypto/mpc"
"github.com/onsonr/sonr/pkg/common/response"
"github.com/onsonr/sonr/pkg/common/styles/forms"
)
func HandleRegisterView(c echo.Context) error {
dat := forms.CreateProfileData{
FirstNumber: 1,
LastNumber: 2,
}
return response.TemplEcho(c, register.ProfileFormView(dat))
}
func HandleRegisterStart(c echo.Context) error {
challenge, _ := protocol.CreateChallenge()
handle := c.FormValue("handle")
firstName := c.FormValue("first_name")
lastName := c.FormValue("last_name")
ks, err := mpc.NewKeyset()
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
dat := forms.RegisterPasskeyData{
Address: ks.Address(),
Handle: handle,
Name: fmt.Sprintf("%s %s", firstName, lastName),
Challenge: challenge.String(),
CreationBlock: "00001",
}
return response.TemplEcho(c, register.LinkCredentialView(dat))
}
func HandleRegisterFinish(c echo.Context) error {
// Get the raw credential JSON string
credentialJSON := c.FormValue("credential")
if credentialJSON == "" {
return echo.NewHTTPError(http.StatusBadRequest, "missing credential data")
}
cred := database.Credential{}
// Unmarshal the credential JSON
if err := json.Unmarshal([]byte(credentialJSON), &cred); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid credential format: %v", err))
}
// Validate required fields
if cred.ID == "" || cred.RawID == "" {
return echo.NewHTTPError(http.StatusBadRequest, "missing credential ID")
}
if cred.Type != "public-key" {
return echo.NewHTTPError(http.StatusBadRequest, "invalid credential type")
}
if cred.Response.AttestationObject == "" || cred.Response.ClientDataJSON == "" {
return echo.NewHTTPError(http.StatusBadRequest, "missing attestation data")
}
// Decode attestation object and client data
attestationObj, err := base64.RawURLEncoding.DecodeString(cred.Response.AttestationObject)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "invalid attestation object encoding")
}
clientData, err := base64.RawURLEncoding.DecodeString(cred.Response.ClientDataJSON)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "invalid client data encoding")
}
// Log detailed credential information
fmt.Printf("Credential Details:\n"+
"ID: %s\n"+
"Raw ID: %s\n"+
"Type: %s\n"+
"Authenticator Attachment: %s\n"+
"Transports: %v\n"+
"Attestation Object Size: %d bytes\n"+
"Client Data Size: %d bytes\n",
cred.ID,
cred.RawID,
cred.Type,
cred.AuthenticatorAttachment,
cred.Transports,
len(attestationObj),
len(clientData))
return response.TemplEcho(c, register.LoadingVaultView())
}
+33
View File
@@ -0,0 +1,33 @@
package handlers
//
// type SpawnVaultRequest struct {
// Name string `json:"name"`
// }
//
// func SpawnVault(c echo.Context) error {
// ks, err := mpc.NewKeyset()
// if err != nil {
// return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
// }
// src, err := mpc.NewSource(ks)
// if err != nil {
// return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
// }
// tk, err := src.OriginToken()
// if err != nil {
// return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
// }
// // Create the vault keyshare auth token
// kscid, err := tk.CID()
// if err != nil {
// return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
// }
// // Create the vault config
// dir, err := config.NewFS(config.GetVaultConfig(src.Address(), kscid.String()))
// path, err := clients.IPFSAdd(c, dir)
// if err != nil {
// return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
// }
// return c.Redirect(http.StatusFound, path)
// }
+57
View File
@@ -0,0 +1,57 @@
package database
import (
"net/http"
"github.com/labstack/echo/v4"
"gorm.io/gorm"
)
var (
ErrInvalidCredentials = echo.NewHTTPError(http.StatusUnauthorized, "Invalid credentials")
ErrInvalidSubject = echo.NewHTTPError(http.StatusBadRequest, "Invalid subject")
ErrInvalidUser = echo.NewHTTPError(http.StatusBadRequest, "Invalid user")
ErrUserAlreadyExists = echo.NewHTTPError(http.StatusConflict, "User already exists")
ErrUserNotFound = echo.NewHTTPError(http.StatusNotFound, "User not found")
)
// Define the credential structure matching our frontend data
type Credential struct {
ID string `json:"id"`
RawID string `json:"rawId"`
Type string `json:"type"`
AuthenticatorAttachment string `json:"authenticatorAttachment"`
Transports []string `json:"transports"`
ClientExtensionResults map[string]interface{} `json:"clientExtensionResults"`
Response struct {
AttestationObject string `json:"attestationObject"`
ClientDataJSON string `json:"clientDataJSON"`
} `json:"response"`
}
type User struct {
gorm.Model
Address string `json:"address"`
Handle string `json:"handle"`
Name string `json:"name"`
CID string `json:"cid"`
Credentials []*Credential `json:"credentials"`
}
type Session struct {
gorm.Model
ID string `json:"id" gorm:"primaryKey"`
BrowserName string `json:"browserName"`
BrowserVersion string `json:"browserVersion"`
UserArchitecture string `json:"userArchitecture"`
Platform string `json:"platform"`
PlatformVersion string `json:"platformVersion"`
DeviceModel string `json:"deviceModel"`
UserHandle string `json:"userHandle"`
FirstName string `json:"firstName"`
LastInitial string `json:"lastInitial"`
VaultAddress string `json:"vaultAddress"`
HumanSum int `json:"humanSum"`
Challenge string `json:"challenge"`
}
+43
View File
@@ -0,0 +1,43 @@
package database
import (
"os"
"path/filepath"
"github.com/onsonr/sonr/app/gateway/config"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
// InitDB initializes and returns a configured database connection
func InitDB(env config.Env) (*gorm.DB, error) {
path := formatDBPath(env.GetSqliteFile())
db, err := gorm.Open(sqlite.Open(path), &gorm.Config{})
if err != nil {
return nil, err
}
// Migrate the schema
db.AutoMigrate(&Session{})
db.AutoMigrate(&User{})
return db, nil
}
func formatDBPath(path string) string {
home := os.Getenv("HOME")
if home == "" {
home = os.Getenv("USERPROFILE")
}
if home == "" {
home = "."
}
configDir := filepath.Join(home, ".config", "hway")
if err := os.MkdirAll(configDir, 0o755); err != nil {
// If we can't create the directory, fall back to current directory
return path
}
return filepath.Join(configDir, path)
}
@@ -0,0 +1,29 @@
package index
import "github.com/onsonr/sonr/pkg/common/styles/layout"
templ NoWebauthnErrorView() {
@layout.Root("Error | Sonr.ID") {
<div class="flex fixed inset-0 z-[99] w-screen h-screen bg-white">
<div class="relative flex flex-wrap items-center w-full h-full px-8">
<div class="relative w-full max-w-sm mx-auto lg:mb-0">
<div class="flex flex-col items-center justify-center h-full">
<div class="flex flex-col items-center justify-center h-full">
<h1 class="text-3xl font-bold text-zinc-900 mb-4">
No WebAuthn Devices
</h1>
<p class="text-lg text-zinc-500">
You don't have any WebAuthn devices connected to your computer.
</p>
</div>
<div class="flex flex-col items-center justify-center h-full">
<button class="btn btn-sm text-zinc-100 bg-zinc-900 hover:bg-zinc-800 w-full shadow">
Get Started
</button>
</div>
</div>
</div>
</div>
</div>
}
}
@@ -0,0 +1,60 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.793
package index
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "github.com/onsonr/sonr/pkg/common/styles/layout"
func NoWebauthnErrorView() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"flex fixed inset-0 z-[99] w-screen h-screen bg-white\"><div class=\"relative flex flex-wrap items-center w-full h-full px-8\"><div class=\"relative w-full max-w-sm mx-auto lg:mb-0\"><div class=\"flex flex-col items-center justify-center h-full\"><div class=\"flex flex-col items-center justify-center h-full\"><h1 class=\"text-3xl font-bold text-zinc-900 mb-4\">No WebAuthn Devices</h1><p class=\"text-lg text-zinc-500\">You don't have any WebAuthn devices connected to your computer.</p></div><div class=\"flex flex-col items-center justify-center h-full\"><button class=\"btn btn-sm text-zinc-100 bg-zinc-900 hover:bg-zinc-800 w-full shadow\">Get Started</button></div></div></div></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
templ_7745c5c3_Err = layout.Root("Error | Sonr.ID").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
}
var _ = templruntime.GeneratedTemplate
@@ -0,0 +1,36 @@
package index
import (
"github.com/onsonr/sonr/pkg/common/styles/layout"
"github.com/onsonr/sonr/pkg/common/styles/text"
)
templ InitialView() {
@layout.Root("Sonr.ID") {
@layout.Container() {
@text.Header("Sonr.ID", "The decentralized identity layer for the web.")
<div class="pt-3 flex flex-col items-center justify-center h-full">
<sl-button hx-target="#container" hx-get="/register" hx-push-url="/register" type="button">
<sl-icon slot="prefix" library="sonr" name="sonr"></sl-icon>
Get Started
<sl-icon slot="suffix" library="sonr" name="arrow-right"></sl-icon>
</sl-button>
</div>
}
}
}
templ ReturningView() {
@layout.Root("Login | Sonr.ID") {
@layout.Container() {
@text.Header("Welcome Back!", "Continue with your existing Sonr.ID.")
<div class="pt-3 flex flex-col items-center justify-center h-full">
<sl-button hx-target="#container" hx-get="/register" type="button">
<sl-icon slot="prefix" library="sonr" name="sonr"></sl-icon>
Log back in
<sl-icon slot="suffix" library="sonr" name="arrow-right"></sl-icon>
</sl-button>
</div>
}
}
}
@@ -0,0 +1,154 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.793
package index
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
"github.com/onsonr/sonr/pkg/common/styles/layout"
"github.com/onsonr/sonr/pkg/common/styles/text"
)
func InitialView() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var3 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = text.Header("Sonr.ID", "The decentralized identity layer for the web.").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" <div class=\"pt-3 flex flex-col items-center justify-center h-full\"><sl-button hx-target=\"#container\" hx-get=\"/register\" hx-push-url=\"/register\" type=\"button\"><sl-icon slot=\"prefix\" library=\"sonr\" name=\"sonr\"></sl-icon> Get Started <sl-icon slot=\"suffix\" library=\"sonr\" name=\"arrow-right\"></sl-icon></sl-button></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
templ_7745c5c3_Err = layout.Container().Render(templ.WithChildren(ctx, templ_7745c5c3_Var3), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
templ_7745c5c3_Err = layout.Root("Sonr.ID").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
}
func ReturningView() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var4 := templ.GetChildren(ctx)
if templ_7745c5c3_Var4 == nil {
templ_7745c5c3_Var4 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var5 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var6 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = text.Header("Welcome Back!", "Continue with your existing Sonr.ID.").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" <div class=\"pt-3 flex flex-col items-center justify-center h-full\"><sl-button hx-target=\"#container\" hx-get=\"/register\" type=\"button\"><sl-icon slot=\"prefix\" library=\"sonr\" name=\"sonr\"></sl-icon> Log back in <sl-icon slot=\"suffix\" library=\"sonr\" name=\"arrow-right\"></sl-icon></sl-button></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
templ_7745c5c3_Err = layout.Container().Render(templ.WithChildren(ctx, templ_7745c5c3_Var6), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
templ_7745c5c3_Err = layout.Root("Login | Sonr.ID").Render(templ.WithChildren(ctx, templ_7745c5c3_Var5), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
}
var _ = templruntime.GeneratedTemplate
+24
View File
@@ -0,0 +1,24 @@
package login
type LoginRequest struct {
Subject string
Action string
Origin string
Status string
Ping string
BlockSpeed string
BlockHeight string
}
type PublicKeyCredentialRequestOptions struct {
Challenge string `json:"challenge"`
RpID string `json:"rpId"`
Timeout int `json:"timeout,omitempty"`
UserVerification string `json:"userVerification,omitempty"`
AllowCredentials []CredentialDescriptor `json:"allowCredentials,omitempty"`
}
type CredentialDescriptor struct {
Type string `json:"type"`
ID string `json:"id"`
}
@@ -0,0 +1,20 @@
package register
import (
"github.com/a-h/templ"
"github.com/go-webauthn/webauthn/protocol"
)
type LinkCredentialRequest struct {
Platform string `json:"platform"`
Handle string `json:"handle"`
DeviceModel string `json:"deviceModel"`
Architecture string `json:"architecture"`
Address string `json:"address"`
RegisterOptions protocol.PublicKeyCredentialCreationOptions `json:"registerOptions"`
}
func (r LinkCredentialRequest) GetCredentialOptions() string {
opts, _ := templ.JSONString(r.RegisterOptions)
return opts
}
@@ -0,0 +1,33 @@
package register
import (
"github.com/onsonr/sonr/pkg/common/styles/forms"
"github.com/onsonr/sonr/pkg/common/styles/layout"
"github.com/onsonr/sonr/pkg/common/styles/text"
)
templ ProfileFormView(data forms.CreateProfileData) {
@layout.Root("New Profile | Sonr.ID") {
@layout.Container() {
@text.Header("Create a Profile", "Enter some basic information about yourself.")
@forms.CreateProfile("/register/start", "POST", data)
}
}
}
templ LinkCredentialView(data forms.RegisterPasskeyData) {
@layout.Root("Register | Sonr.ID") {
@layout.Container() {
@text.Header("Link a PassKey", "This will be used to login to your vault.")
@forms.RegisterPasskey("/register/finish", "POST", data)
}
}
}
templ LoadingVaultView() {
@layout.Root("Loading... | Sonr.ID") {
@layout.Container() {
@text.Header("Loading Vault", "This will be used to login to your vault.")
}
}
}
@@ -0,0 +1,228 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.793
package register
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
"github.com/onsonr/sonr/pkg/common/styles/forms"
"github.com/onsonr/sonr/pkg/common/styles/layout"
"github.com/onsonr/sonr/pkg/common/styles/text"
)
func ProfileFormView(data forms.CreateProfileData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var3 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = text.Header("Create a Profile", "Enter some basic information about yourself.").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = forms.CreateProfile("/register/start", "POST", data).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
templ_7745c5c3_Err = layout.Container().Render(templ.WithChildren(ctx, templ_7745c5c3_Var3), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
templ_7745c5c3_Err = layout.Root("New Profile | Sonr.ID").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
}
func LinkCredentialView(data forms.RegisterPasskeyData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var4 := templ.GetChildren(ctx)
if templ_7745c5c3_Var4 == nil {
templ_7745c5c3_Var4 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var5 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var6 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = text.Header("Link a PassKey", "This will be used to login to your vault.").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = forms.RegisterPasskey("/register/finish", "POST", data).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
templ_7745c5c3_Err = layout.Container().Render(templ.WithChildren(ctx, templ_7745c5c3_Var6), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
templ_7745c5c3_Err = layout.Root("Register | Sonr.ID").Render(templ.WithChildren(ctx, templ_7745c5c3_Var5), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
}
func LoadingVaultView() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var7 := templ.GetChildren(ctx)
if templ_7745c5c3_Var7 == nil {
templ_7745c5c3_Var7 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var8 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var9 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = text.Header("Loading Vault", "This will be used to login to your vault.").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
templ_7745c5c3_Err = layout.Container().Render(templ.WithChildren(ctx, templ_7745c5c3_Var9), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
templ_7745c5c3_Err = layout.Root("Loading... | Sonr.ID").Render(templ.WithChildren(ctx, templ_7745c5c3_Var8), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
}
var _ = templruntime.GeneratedTemplate
+102
View File
@@ -0,0 +1,102 @@
package session
import (
"regexp"
"strings"
"github.com/labstack/echo/v4"
"github.com/onsonr/sonr/app/gateway/internal/database"
"github.com/onsonr/sonr/pkg/common"
"github.com/segmentio/ksuid"
)
// InitSession initializes or loads an existing session
func (s *HTTPContext) InitSession() error {
sessionID := s.getOrCreateSessionID()
// Try to load existing session
var sess database.Session
result := s.db.Where("id = ?", sessionID).First(&sess)
if result.Error != nil {
// Create new session if not found
bn, bv, arch, plat, platVer, model := extractBrowserInfo(s.Context)
sess = database.Session{
ID: sessionID,
BrowserName: bn,
BrowserVersion: bv,
UserArchitecture: arch,
Platform: plat,
PlatformVersion: platVer,
DeviceModel: model,
}
if err := s.db.Create(&sess).Error; err != nil {
return err
}
}
s.sess = &sess
return nil
}
func (s *HTTPContext) getOrCreateSessionID() string {
if ok := common.CookieExists(s.Context, common.SessionID); !ok {
sessionID := ksuid.New().String()
common.WriteCookie(s.Context, common.SessionID, sessionID)
return sessionID
}
sessionID, err := common.ReadCookie(s.Context, common.SessionID)
if err != nil {
sessionID = ksuid.New().String()
common.WriteCookie(s.Context, common.SessionID, sessionID)
}
return sessionID
}
func extractBrowserInfo(c echo.Context) (string, string, string, string, string, string) {
// Extract all relevant headers
browserName := common.HeaderRead(c, common.UserAgent)
arch := common.HeaderRead(c, common.Architecture)
platform := common.HeaderRead(c, common.Platform)
platformVer := common.HeaderRead(c, common.PlatformVersion)
model := common.HeaderRead(c, common.Model)
fullVersionList := common.HeaderRead(c, common.FullVersionList)
// Default values if headers are empty
if browserName == "" {
browserName = "N/A"
}
if arch == "" {
arch = "unknown"
}
if platform == "" {
platform = "unknown"
}
if platformVer == "" {
platformVer = "unknown"
}
if model == "" {
model = "unknown"
}
// Extract browser version from full version list
version := "-1"
if fullVersionList != "" {
entries := strings.Split(strings.TrimSpace(fullVersionList), ",")
for _, entry := range entries {
entry = strings.TrimSpace(entry)
re := regexp.MustCompile(`"([^"]+)";v="([^"]+)"`)
matches := re.FindStringSubmatch(entry)
if len(matches) == 3 {
browserName = matches[1]
version = matches[2]
if browserName != "Not.A/Brand" &&
browserName != "Chromium" {
break
}
}
}
}
return browserName, version, arch, platform, platformVer, model
}
+1
View File
@@ -0,0 +1 @@
package session
@@ -0,0 +1,53 @@
package session
import (
"net/http"
"github.com/labstack/echo/v4"
"github.com/onsonr/sonr/app/gateway/config"
"github.com/onsonr/sonr/app/gateway/internal/database"
"gorm.io/gorm"
)
// Middleware creates a new session middleware
func Middleware(db *gorm.DB, env config.Env) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
cc := NewHTTPContext(c, db)
if err := cc.InitSession(); err != nil {
return err
}
return next(cc)
}
}
}
// HTTPContext is the context for HTTP endpoints.
type HTTPContext struct {
echo.Context
db *gorm.DB
sess *database.Session
env config.Env
}
// Get returns the HTTPContext from the echo context
func Get(c echo.Context) (*HTTPContext, error) {
ctx, ok := c.(*HTTPContext)
if !ok {
return nil, echo.NewHTTPError(http.StatusInternalServerError, "Session Context not found")
}
return ctx, nil
}
// NewHTTPContext creates a new session context
func NewHTTPContext(c echo.Context, db *gorm.DB) *HTTPContext {
return &HTTPContext{
Context: c,
db: db,
}
}
// Session returns the current session
func (s *HTTPContext) Session() *database.Session {
return s.sess
}
+168
View File
@@ -0,0 +1,168 @@
package session
import (
"github.com/labstack/echo/v4"
"github.com/onsonr/sonr/app/gateway/internal/database"
)
// ╭───────────────────────────────────────────────────────╮
// │ DB Setter Functions │
// ╰───────────────────────────────────────────────────────╯
// SetUserHandle sets the user handle in the session
func SetUserHandle(c echo.Context, handle string) error {
sess, err := Get(c)
if err != nil {
return err
}
sess.Session().UserHandle = handle
return sess.db.Save(sess.Session()).Error
}
// SetFirstName sets the first name in the session
func SetFirstName(c echo.Context, name string) error {
sess, err := Get(c)
if err != nil {
return err
}
sess.Session().FirstName = name
return sess.db.Save(sess.Session()).Error
}
// SetLastInitial sets the last initial in the session
func SetLastInitial(c echo.Context, initial string) error {
sess, err := Get(c)
if err != nil {
return err
}
sess.Session().LastInitial = initial
return sess.db.Save(sess.Session()).Error
}
// SetVaultAddress sets the vault address in the session
func SetVaultAddress(c echo.Context, address string) error {
sess, err := Get(c)
if err != nil {
return err
}
sess.Session().VaultAddress = address
return sess.db.Save(sess.Session()).Error
}
// ╭───────────────────────────────────────────────────────╮
// │ DB Getter Functions │
// ╰───────────────────────────────────────────────────────╯
// GetID returns the session ID
func GetID(c echo.Context) (string, error) {
sess, err := Get(c)
if err != nil {
return "", err
}
return sess.Session().ID, nil
}
// GetBrowserName returns the browser name
func GetBrowserName(c echo.Context) (string, error) {
sess, err := Get(c)
if err != nil {
return "", err
}
return sess.Session().BrowserName, nil
}
// GetBrowserVersion returns the browser version
func GetBrowserVersion(c echo.Context) (string, error) {
sess, err := Get(c)
if err != nil {
return "", err
}
return sess.Session().BrowserVersion, nil
}
// GetUserArchitecture returns the user architecture
func GetUserArchitecture(c echo.Context) (string, error) {
sess, err := Get(c)
if err != nil {
return "", err
}
return sess.Session().UserArchitecture, nil
}
// GetPlatform returns the platform
func GetPlatform(c echo.Context) (string, error) {
sess, err := Get(c)
if err != nil {
return "", err
}
return sess.Session().Platform, nil
}
// GetPlatformVersion returns the platform version
func GetPlatformVersion(c echo.Context) (string, error) {
sess, err := Get(c)
if err != nil {
return "", err
}
return sess.Session().PlatformVersion, nil
}
// GetDeviceModel returns the device model
func GetDeviceModel(c echo.Context) (string, error) {
sess, err := Get(c)
if err != nil {
return "", err
}
return sess.Session().DeviceModel, nil
}
// GetUserHandle returns the user handle
func GetUserHandle(c echo.Context) (string, error) {
sess, err := Get(c)
if err != nil {
return "", err
}
return sess.Session().UserHandle, nil
}
// GetFirstName returns the first name
func GetFirstName(c echo.Context) (string, error) {
sess, err := Get(c)
if err != nil {
return "", err
}
return sess.Session().FirstName, nil
}
// GetLastInitial returns the last initial
func GetLastInitial(c echo.Context) (string, error) {
sess, err := Get(c)
if err != nil {
return "", err
}
return sess.Session().LastInitial, nil
}
// GetVaultAddress returns the vault address
func GetVaultAddress(c echo.Context) (string, error) {
sess, err := Get(c)
if err != nil {
return "", err
}
return sess.Session().VaultAddress, nil
}
// HandleExists checks if a handle already exists in any session
func HandleExists(c echo.Context, handle string) (bool, error) {
sess, err := Get(c)
if err != nil {
return false, err
}
var count int64
if err := sess.db.Model(&database.Session{}).Where("user_handle = ?", handle).Count(&count).Error; err != nil {
return false, err
}
return count > 0, nil
}
+32
View File
@@ -0,0 +1,32 @@
// Package gateway provides the default routes for the Sonr hway.
package gateway
import (
"github.com/labstack/echo/v4"
"github.com/onsonr/sonr/pkg/common/response"
"github.com/onsonr/sonr/pkg/gateway/config"
"github.com/onsonr/sonr/pkg/gateway/handlers"
"github.com/onsonr/sonr/pkg/gateway/internal/database"
"github.com/onsonr/sonr/pkg/gateway/internal/session"
)
func RegisterRoutes(e *echo.Echo, env config.Env) error {
// Custom error handler for gateway
e.HTTPErrorHandler = response.RedirectOnError("http://localhost:3000")
// Initialize database
db, err := database.InitDB(env)
if err != nil {
return err
}
// Inject session middleware with database connection
e.Use(session.Middleware(db, env))
// Register routes
e.GET("/", handlers.HandleIndex)
e.GET("/register", handlers.HandleRegisterView)
e.POST("/register/start", handlers.HandleRegisterStart)
e.POST("/register/finish", handlers.HandleRegisterFinish)
return nil
}
+16
View File
@@ -0,0 +1,16 @@
// Code generated from Pkl module `sonr.hway.Gate`. DO NOT EDIT.
package types
type Config struct {
ChainId string `pkl:"chainId" json:"chainId,omitempty"`
IpfsGatewayUrl string `pkl:"ipfsGatewayUrl" json:"ipfsGatewayUrl,omitempty"`
LandingUrl string `pkl:"landingUrl" json:"landingUrl,omitempty"`
AuthProxyUrl string `pkl:"authProxyUrl" json:"authProxyUrl,omitempty"`
SonrApiUrl string `pkl:"sonrApiUrl" json:"sonrApiUrl,omitempty"`
SonrRpcUrl string `pkl:"sonrRpcUrl" json:"sonrRpcUrl,omitempty"`
}
+5
View File
@@ -0,0 +1,5 @@
// Code generated from Pkl module `sonr.hway.Ctx`. DO NOT EDIT.
package types
type Ctx struct {
}
+8
View File
@@ -0,0 +1,8 @@
// Code generated from Pkl module `sonr.hway.Gate`. DO NOT EDIT.
package types
import "github.com/apple/pkl-go/pkl"
func init() {
pkl.RegisterMapping("sonr.hway.Gate#Config", Config{})
}