implement login with vue

* use vue router
* use pinia
* use tailwindcss
* use vite
* transform /api/login and /api/login/callback into JSON APIs
* add Access-Control-Allow-Credentials header
* add TODO about JSON errors
This commit is contained in:
ekzyis 2023-11-04 14:31:48 +01:00
parent d1b7786434
commit 4e343d49d0
22 changed files with 605 additions and 5164 deletions

View File

@ -30,7 +30,7 @@ func NewLNAuth() (*LNAuth, error) {
return nil, fmt.Errorf("rand.Read error: %w", err)
}
k1hex := hex.EncodeToString(k1)
url := []byte(fmt.Sprintf("https://%s/api/login?tag=login&k1=%s&action=login", env.PublicURL, k1hex))
url := []byte(fmt.Sprintf("https://%s/api/login/callback?tag=login&k1=%s&action=login", env.PublicURL, k1hex))
conv, err := bech32.ConvertBits(url, 8, 5, true)
if err != nil {
return nil, fmt.Errorf("bech32.ConvertBits error: %w", err)

View File

@ -37,6 +37,7 @@ func serveError(c echo.Context, code int) error {
c.Logger().Error(err)
return err
}
// TODO return errors in JSON
if err = c.Stream(code, "text/html", f); err != nil {
c.Logger().Error(err)
return err

View File

@ -34,11 +34,10 @@ func HandleLogin(sc context.ServerContext) echo.HandlerFunc {
return err
}
data = map[string]any{
"session": c.Get("session"),
"lnurl": lnAuth.LNURL,
"qr": qr,
"lnurl": lnAuth.LNURL,
"qr": qr,
}
return sc.Render(c, http.StatusOK, "login.html", data)
return c.JSON(http.StatusOK, data)
}
}

View File

@ -17,13 +17,16 @@ func HandleCheckSession(sc context.ServerContext) echo.HandlerFunc {
err error
)
if cookie, err = c.Cookie("session"); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, map[string]string{"reason": "cookie required"})
// return echo.NewHTTPError(http.StatusBadRequest, map[string]string{"reason": "cookie required"})
return c.JSON(http.StatusBadRequest, map[string]string{"reason": "cookie required"})
}
s = db.Session{SessionId: cookie.Value}
if err = sc.Db.FetchSession(&s); err == sql.ErrNoRows {
return echo.NewHTTPError(http.StatusNotFound, map[string]string{"reason": "session not found"})
// return echo.NewHTTPError(http.StatusNotFound, map[string]string{"reason": "session not found"})
return c.JSON(http.StatusBadRequest, map[string]string{"reason": "session not found"})
} else if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError)
// return echo.NewHTTPError(http.StatusInternalServerError)
return c.JSON(http.StatusInternalServerError, nil)
}
return c.JSON(http.StatusOK, map[string]string{"pubkey": s.Pubkey})
}

View File

@ -25,7 +25,6 @@ func mountMiddleware(e *echo.Echo, sc ServerContext) {
func addFrontendRoutes(e *echo.Echo, sc ServerContext) {
GET(e, sc, "/", handler.HandleIndex)
GET(e, sc, "/login", handler.HandleLogin)
POST(e, sc, "/logout", handler.HandleLogout)
GET(e, sc, "/user",
handler.HandleUser,
@ -43,7 +42,8 @@ func addFrontendRoutes(e *echo.Echo, sc ServerContext) {
}
func addBackendRoutes(e *echo.Echo, sc ServerContext) {
GET(e, sc, "/api/login", handler.HandleLoginCallback)
GET(e, sc, "/api/login", handler.HandleLogin)
GET(e, sc, "/api/login/callback", handler.HandleLoginCallback)
GET(e, sc, "/api/session", handler.HandleCheckSession)
GET(e, sc, "/api/invoice/:id",
handler.HandleInvoiceStatus,

View File

@ -25,6 +25,11 @@ func New(ctx ServerContext) *Server {
Format: "${time_custom} ${method} ${uri} ${status}\n",
CustomTimeFormat: "2006-01-02 15:04:05.00000-0700",
}))
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"http://localhost:4224", "https://delphi.market", "https://dev1.delphi.market"},
AllowCredentials: true,
AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept},
}))
e.HTTPErrorHandler = httpErrorHandler
s = &Server{e}

View File

@ -4,12 +4,13 @@
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
<link rel="icon" href="/favicon.ico">
<script type="module" src="/src/main.js"></script>
<title>delphi.market</title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
<strong>We're sorry but this site doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->

View File

@ -16,4 +16,4 @@
"scripthost"
]
}
}
}

View File

@ -3,27 +3,30 @@
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
"dev": "vite --port 4224",
"build": "vite build",
"serve": "vite preview"
},
"dependencies": {
"@vitejs/plugin-vue": "^4.4.0",
"core-js": "^3.8.3",
"pinia": "^2.1.7",
"register-service-worker": "^1.7.2",
"vue": "^3.2.13"
"vite": "^4.5.0",
"vue": "^3.2.13",
"vue-router": "4"
},
"devDependencies": {
"@babel/core": "^7.12.16",
"@babel/eslint-parser": "^7.12.16",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-plugin-pwa": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"@vue/eslint-config-standard": "^6.1.0",
"autoprefixer": "^10.4.16",
"eslint": "^7.32.0",
"eslint-plugin-import": "^2.25.3",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-vue": "^8.0.3"
"eslint-plugin-vue": "^8.0.3",
"postcss": "^8.4.31",
"tailwindcss": "^3.3.5"
}
}

6
vue/postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
}

View File

@ -1,26 +1,41 @@
<template>
<img alt="Vue logo" src="./assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js App" />
<NavBar />
<div id="container">
<router-view />
</div>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
import NavBar from './components/NavBar'
export default {
name: 'App',
components: {
HelloWorld
}
components: { NavBar }
}
</script>
<script setup>
import { useSession } from './stores/session';
const session = useSession()
session.init()
</script>
<style>
html,
body {
background-color: #091833;
color: #ffffff;
}
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
color: #ffffff;
margin-top: 1em;
}
#container {
margin: 1em;
}
</style>

View File

@ -1,66 +0,0 @@
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<p>
For a guide and recipes on how to configure / customize this project,<br>
check out the
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
</p>
<h3>Installed CLI Plugins</h3>
<ul>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank"
rel="noopener">babel</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-pwa" target="_blank"
rel="noopener">pwa</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank"
rel="noopener">eslint</a></li>
</ul>
<h3>Essential Links</h3>
<ul>
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
</ul>
<h3>Ecosystem</h3>
<ul>
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a>
</li>
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
</ul>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>

View File

@ -0,0 +1,5 @@
<template>
<div>home</div>
</template>
<script></script>

View File

@ -0,0 +1,25 @@
<template>
<a v-if="lnurl" :href="'lightning:' + lnurl">
<img v-if="qr" :src="'data:image/png;base64,' + qr" />
</a>
</template>
<script setup>
import { ref } from 'vue';
import { useSession } from '@/stores/session';
let qr = ref(null)
let lnurl = ref(null)
const session = useSession()
await (async () => {
try {
if (session.isAuthenticated) return
const s = await session.login()
qr = s.qr
lnurl = s.lnurl
} catch (err) {
console.error("error:", err.reason || err)
}
})()
</script>

View File

@ -0,0 +1,25 @@
<template>
<!-- eslint-disable -->
<pre>
_ _
| | ___ __ _(_)_ __
| |/ _ \ / _` | | '_ \
| | (_) | (_| | | | | |
|_|\___/ \__, |_|_| |_|
|___/ </pre>
<!-- eslint-enable -->
<Suspense>
<LoginQRCode class="flex justify-center m-3" />
</Suspense>
</template>
<script setup>
import LoginQRCode from './LoginQRCode.vue';
</script>
<style scoped>
pre {
font-family: monospace;
font-weight: bold;
}
</style>

View File

@ -0,0 +1,40 @@
<template>
<header class="flex flex-row text-center justify-center pt-1">
<nav>
<router-link to="/">home</router-link>
<router-link to="/user" v-if="session.isAuthenticated">user</router-link>
<router-link to="/login" v-else href="/login">login</router-link>
</nav>
</header>
</template>
<script setup>
import { useSession } from '@/stores/session'
const session = useSession()
</script>
<style scoped>
nav {
display: flex;
justify-content: center;
}
a {
color: #8787a4;
text-decoration: underline;
}
a:hover {
color: #ffffff;
background: #8787A4;
}
a.selected {
color: #ffffff;
background: #8787A4;
}
nav>a {
margin: 0 3px;
}
</style>

3
vue/src/index.css Normal file
View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@ -1,5 +1,29 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import * as VueRouter from 'vue-router'
import App from './App.vue'
import './registerServiceWorker'
import './index.css'
createApp(App).mount('#app')
import HomeView from '@/components/HomeView'
import LoginView from '@/components/LoginView'
const routes = [
{
path: '/', component: HomeView
},
{
path: '/login', component: LoginView
}
]
const router = VueRouter.createRouter({
history: VueRouter.createWebHashHistory(),
routes
})
const pinia = createPinia()
const app = createApp(App)
app.use(router)
app.use(pinia)
app.mount('#app')

40
vue/src/stores/session.js Normal file
View File

@ -0,0 +1,40 @@
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
export const useSession = defineStore('session', () => {
let pubkey = ref(null)
// eslint-disable-next-line vue/no-ref-as-operand
const isAuthenticated = computed(() => !!pubkey)
async function init () {
try {
const { pubkey } = await checkSession()
if (pubkey) {
console.log('authenticated as', pubkey)
return
}
console.log('unauthenticated')
} catch (err) {
console.error('error:', err.reason || err)
}
}
function checkSession () {
const url = window.origin + '/api/session'
return fetch(url, {
credentials: 'include'
})
.then(r => {
const body = r.json()
pubkey = body.pubkey
return body
})
}
function login () {
const url = window.origin + '/api/login'
return fetch(url, { credentials: 'include' }).then(r => r.json())
}
return { pubkey, isAuthenticated, init, checkSession, login }
})

10
vue/tailwind.config.js Normal file
View File

@ -0,0 +1,10 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./src/**/*.{html,js,vue}'
],
theme: {
extend: {}
},
plugins: []
}

14
vue/vite.config.js Normal file
View File

@ -0,0 +1,14 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'],
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})

File diff suppressed because it is too large Load Diff