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:
parent
d1b7786434
commit
4e343d49d0
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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})
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 -->
|
|
@ -16,4 +16,4 @@
|
|||
"scripthost"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<div>home</div>
|
||||
</template>
|
||||
|
||||
<script></script>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,3 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
|
@ -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')
|
||||
|
|
|
@ -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 }
|
||||
})
|
|
@ -0,0 +1,10 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
'./src/**/*.{html,js,vue}'
|
||||
],
|
||||
theme: {
|
||||
extend: {}
|
||||
},
|
||||
plugins: []
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
})
|
5418
vue/yarn.lock
5418
vue/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue