491 lines
11 KiB
Vue
491 lines
11 KiB
Vue
<template>
|
|
<div class="landing-page wide">
|
|
<div class="instance-group">
|
|
<div v-if="instance" class="instance-info">
|
|
<h1 class="instance-title">{{ instance.title }}</h1>
|
|
<div class="instance-description">
|
|
{{ instance.short_description }}
|
|
<br>
|
|
<router-link :to="{ name: 'about' }">Learn more <span class="arrow">>></span></router-link>
|
|
</div>
|
|
</div>
|
|
<div v-if="instance" class="login-form-group">
|
|
<form class="login-form">
|
|
<div v-if="isLoading" class="login-form-loader">
|
|
<loader></loader>
|
|
</div>
|
|
<div class="form-control" v-if="!isRegistered || loginType == 'password'">
|
|
<div class="input-group">
|
|
<input
|
|
id="username"
|
|
v-model="username"
|
|
required
|
|
placeholder="Username"
|
|
>
|
|
<div class="addon">@{{ instance.uri }}</div>
|
|
</div>
|
|
<div
|
|
v-if="!isRegistered"
|
|
class="form-message"
|
|
:class="{ error: !isUsernameValid() }"
|
|
>
|
|
Only lowercase letters, numbers and underscores are allowed.
|
|
</div>
|
|
</div>
|
|
<div class="form-control">
|
|
<input
|
|
id="password"
|
|
type="password"
|
|
v-model="password"
|
|
required
|
|
placeholder="Password"
|
|
>
|
|
</div>
|
|
<div class="form-control" v-if="!instance.registrations && !isRegistered">
|
|
<input
|
|
id="invite-token"
|
|
v-model="inviteCode"
|
|
required
|
|
placeholder="Enter the invite code"
|
|
>
|
|
</div>
|
|
<button
|
|
v-if="isRegistered"
|
|
type="submit"
|
|
:disabled="!isLoginFormValid()"
|
|
@click.prevent="login()"
|
|
>
|
|
Sign in
|
|
</button>
|
|
<button
|
|
v-else
|
|
type="submit"
|
|
:disabled="!isLoginFormValid()"
|
|
@click.prevent="register()"
|
|
>
|
|
Sign Up
|
|
</button>
|
|
<div class="error-message" v-if="loginErrorMessage" >{{ loginErrorMessage }}</div>
|
|
</form>
|
|
<div class="switch-mode">
|
|
<template v-if="isRegistered">Don't have an account?</template>
|
|
<template v-else>Already registered?</template>
|
|
 
|
|
<button @click.prevent="isRegistered = !isRegistered; loginErrorMessage = null">
|
|
<template v-if="isRegistered">Sign Up</template>
|
|
<template v-else>Sign In</template>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { watch } from "vue"
|
|
import { $, $$, $ref } from "vue/macros"
|
|
import { useRouter } from "vue-router"
|
|
|
|
import {
|
|
createUser,
|
|
getAccessToken,
|
|
getCurrentUser,
|
|
} from "@/api/users"
|
|
import Loader from "@/components/Loader.vue"
|
|
import { useInstanceInfo } from "@/composables/instance"
|
|
import { useCurrentUser } from "@/composables/user"
|
|
import {
|
|
createEip4361_SignedMessage,
|
|
getWallet,
|
|
hasEthereumWallet,
|
|
} from "@/utils/ethereum"
|
|
|
|
const router = useRouter()
|
|
const { setCurrentUser, setAuthToken } = useCurrentUser()
|
|
const { getBlockchainInfo, instance } = $(useInstanceInfo())
|
|
|
|
const isRegistered = $ref(true)
|
|
const username = $ref("")
|
|
const password = $ref<string | null>(null)
|
|
const inviteCode = $ref<string | null>(null)
|
|
let loginType = $ref<"password" | "eip4361">("password")
|
|
let isLoading = $ref(false)
|
|
let loginErrorMessage = $ref<string | null>(null)
|
|
|
|
function isWalletRequired(): boolean {
|
|
return false
|
|
}
|
|
|
|
watch($$(instance), () => {
|
|
if (hasEthereumWallet() || isWalletRequired()) {
|
|
// Switch to EIP-4361 if wallet is present or
|
|
// if registration is token-gated
|
|
loginType = "eip4361"
|
|
}
|
|
}, { immediate: true })
|
|
|
|
function isUsernameValid(): boolean {
|
|
if (!username) {
|
|
return true
|
|
}
|
|
return /^[a-z0-9_]+$/.test(username)
|
|
}
|
|
|
|
function isLoginFormValid(): boolean {
|
|
if (!instance) {
|
|
return false
|
|
}
|
|
if (isRegistered) {
|
|
if (loginType === "password") {
|
|
return Boolean(username) && Boolean(password)
|
|
} else {
|
|
return true
|
|
}
|
|
} else {
|
|
const inviteCodeValid = instance.registrations ? true : Boolean(inviteCode)
|
|
if (!username || !isUsernameValid()) {
|
|
return false
|
|
}
|
|
if (loginType === "password") {
|
|
return Boolean(password) && inviteCodeValid
|
|
} else {
|
|
return inviteCodeValid
|
|
}
|
|
}
|
|
}
|
|
|
|
async function register() {
|
|
loginErrorMessage = null
|
|
if (!instance) {
|
|
return
|
|
}
|
|
let userData
|
|
let loginData
|
|
if (loginType === "password") {
|
|
userData = {
|
|
username,
|
|
password,
|
|
message: null,
|
|
signature: null,
|
|
invite_code: inviteCode,
|
|
}
|
|
loginData = { username, password, message: null, signature: null }
|
|
} else {
|
|
const signer = await getWallet()
|
|
if (!signer) {
|
|
loginErrorMessage = "wallet not found"
|
|
return
|
|
}
|
|
const { message, signature } = await createEip4361_SignedMessage(
|
|
signer,
|
|
instance.uri,
|
|
instance.login_message,
|
|
)
|
|
userData = {
|
|
username,
|
|
password: null,
|
|
message,
|
|
signature,
|
|
invite_code: inviteCode,
|
|
}
|
|
loginData = { username: null, password: null, message, signature }
|
|
}
|
|
isLoading = true
|
|
let user
|
|
let authToken
|
|
try {
|
|
user = await createUser(loginType, userData)
|
|
authToken = await getAccessToken(loginType, loginData)
|
|
} catch (error: any) {
|
|
isLoading = false
|
|
loginErrorMessage = error.message
|
|
return
|
|
}
|
|
setCurrentUser(user)
|
|
setAuthToken(authToken)
|
|
isLoading = false
|
|
router.push({ name: "home" })
|
|
}
|
|
|
|
async function login() {
|
|
loginErrorMessage = null
|
|
if (!instance) {
|
|
return
|
|
}
|
|
let loginData
|
|
if (loginType === "password") {
|
|
loginData = { username, password, message: null, signature: null }
|
|
} else {
|
|
const signer = await getWallet()
|
|
if (!signer) {
|
|
loginErrorMessage = "wallet not found"
|
|
return
|
|
}
|
|
const { message, signature } = await createEip4361_SignedMessage(
|
|
signer,
|
|
instance.uri,
|
|
instance.login_message,
|
|
)
|
|
loginData = { username: null, password: null, message, signature }
|
|
}
|
|
isLoading = true
|
|
let user
|
|
let authToken
|
|
try {
|
|
authToken = await getAccessToken(loginType, loginData)
|
|
user = await getCurrentUser(authToken)
|
|
} catch (error: any) {
|
|
isLoading = false
|
|
loginErrorMessage = error.message
|
|
return
|
|
}
|
|
setCurrentUser(user)
|
|
setAuthToken(authToken)
|
|
isLoading = false
|
|
router.push({ name: "home" })
|
|
}
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
@import "../styles/layout";
|
|
@import "../styles/theme";
|
|
|
|
$landing-text-color: #fff;
|
|
|
|
.landing-page {
|
|
background-color: #000;
|
|
background-image: url("../assets/startpage.png");
|
|
background-repeat: no-repeat;
|
|
background-size: cover;
|
|
box-sizing: border-box;
|
|
color: $landing-text-color;
|
|
min-height: 100vh;
|
|
padding-top: 20vh;
|
|
}
|
|
|
|
.instance-group {
|
|
align-items: flex-start;
|
|
display: flex;
|
|
flex-direction: row;
|
|
gap: $content-gap;
|
|
justify-content: space-between;
|
|
margin: 0 auto;
|
|
max-width: $wide-content-width + $content-gap + $wide-sidebar-width;
|
|
}
|
|
|
|
.instance-info {
|
|
max-width: $wide-content-width;
|
|
min-width: 0;
|
|
}
|
|
|
|
.instance-title {
|
|
font-size: 60px;
|
|
font-weight: bold;
|
|
margin-bottom: 20px;
|
|
//text-transform: uppercase;
|
|
word-wrap: break-word;
|
|
}
|
|
|
|
.instance-description {
|
|
font-size: 24px;
|
|
line-height: 1.75;
|
|
|
|
a {
|
|
color: $landing-text-color;
|
|
}
|
|
|
|
.arrow {
|
|
color: #7DFF54;
|
|
|
|
&:hover {
|
|
color: $landing-text-color;
|
|
}
|
|
}
|
|
}
|
|
|
|
.login-form-group {
|
|
background-color: #1A1818;
|
|
border-radius: 10px;
|
|
box-sizing: border-box;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 15px;
|
|
max-width: 100%;
|
|
min-width: $wide-sidebar-width - 50px;
|
|
padding: 30px;
|
|
width: $wide-sidebar-width;
|
|
}
|
|
|
|
.login-type {
|
|
border-radius: 10px;
|
|
display: flex;
|
|
|
|
button {
|
|
border: 1px solid #3D3D3D;
|
|
color: #fff;
|
|
padding: 10px;
|
|
width: 100%;
|
|
|
|
&:first-child {
|
|
border-bottom-left-radius: 10px;
|
|
border-top-left-radius: 10px;
|
|
}
|
|
|
|
&:last-child {
|
|
border-bottom-right-radius: 10px;
|
|
border-top-right-radius: 10px;
|
|
}
|
|
|
|
&.active {
|
|
background-color: #3D3D3D;
|
|
}
|
|
}
|
|
}
|
|
|
|
.login-form {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 15px;
|
|
position: relative;
|
|
|
|
input,
|
|
.addon {
|
|
background-color: #2E2C2C;
|
|
border: none;
|
|
line-height: 18px;
|
|
padding: 15px;
|
|
}
|
|
|
|
input {
|
|
border-radius: 10px;
|
|
color: $landing-text-color;
|
|
min-width: 100px;
|
|
|
|
&::placeholder {
|
|
color: #B3B3B3;
|
|
}
|
|
}
|
|
|
|
.input-group {
|
|
display: flex;
|
|
flex-direction: row;
|
|
|
|
input {
|
|
border-radius: 10px 0 0 10px;
|
|
min-width: 0;
|
|
}
|
|
|
|
.addon {
|
|
border-radius: 0 10px 10px 0;
|
|
color: #B3B3B3;
|
|
flex-shrink: 0;
|
|
max-width: 40%;
|
|
overflow: hidden;
|
|
padding-left: 0;
|
|
text-align: right;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
}
|
|
|
|
.form-message {
|
|
font-size: 12px;
|
|
margin-top: 3px;
|
|
padding: 0 15px;
|
|
|
|
&.error {
|
|
color: $error-color;
|
|
}
|
|
}
|
|
|
|
button[type="submit"] {
|
|
background: linear-gradient(to right, #FF5959, #FF5EAD, #D835FE, #D963FF);
|
|
background-color: #000;
|
|
border: none;
|
|
border-radius: 10px;
|
|
box-shadow: 0 2px 16px -5px #BB5CC7;
|
|
color: $landing-text-color;
|
|
cursor: pointer;
|
|
display: block;
|
|
font-size: 20px;
|
|
font-weight: bold;
|
|
height: 48px;
|
|
padding: 10px 60px;
|
|
|
|
&:not([disabled]):hover {
|
|
background: linear-gradient(to right, #FF7373, #FF78BA, #DD4FFE, #DF7DFF);
|
|
}
|
|
}
|
|
|
|
.error-message {
|
|
color: $error-color;
|
|
margin-top: 10px;
|
|
text-align: center;
|
|
}
|
|
|
|
.wallet-required {
|
|
align-items: center;
|
|
display: flex;
|
|
flex-direction: row;
|
|
gap: 0.4em;
|
|
justify-content: center;
|
|
|
|
img {
|
|
filter: var(--btn-text-colorizer);
|
|
height: 1em;
|
|
width: 1em;
|
|
}
|
|
|
|
a {
|
|
color: $landing-text-color;
|
|
text-decoration: underline;
|
|
}
|
|
}
|
|
}
|
|
|
|
.login-form-loader {
|
|
bottom: 0;
|
|
display: flex;
|
|
left: 0;
|
|
position: absolute;
|
|
right: 0;
|
|
top: 0;
|
|
z-index: 1;
|
|
|
|
.loader {
|
|
margin: auto;
|
|
}
|
|
}
|
|
|
|
.switch-mode {
|
|
text-align: center;
|
|
|
|
button {
|
|
color: $landing-text-color;
|
|
text-decoration: underline;
|
|
}
|
|
}
|
|
|
|
@media screen and (max-width: $screen-breakpoint-medium) {
|
|
.login-form-group {
|
|
padding: 25px;
|
|
}
|
|
}
|
|
|
|
@media screen and (max-width: $screen-breakpoint-small) {
|
|
.landing-page {
|
|
padding-top: $content-gap;
|
|
}
|
|
|
|
.instance-group {
|
|
flex-wrap: wrap;
|
|
justify-content: flex-start;
|
|
}
|
|
|
|
.login-form-group {
|
|
margin-right: auto;
|
|
min-width: auto;
|
|
}
|
|
}
|
|
</style>
|