mirror of
https://github.com/m1k1o/neko.git
synced 2026-11-03 00:11:48 -06:00
feat(auth): add OIDC-only member authentication (#686)
* feat(auth): add OIDC member authentication * docs(auth): document OIDC member login * fix(auth): address OAuth review feedback * refactor(auth): move OAuth flow into member provider
This commit is contained in:
@@ -12,7 +12,8 @@
|
||||
backgroundColor: Background(seed),
|
||||
}"
|
||||
>
|
||||
{{ seed.substring(0, 2).toUpperCase() }}
|
||||
<img v-if="imageURL" :src="imageURL" :alt="seed" @error="imageFailed = true" />
|
||||
<template v-else>{{ seed.substring(0, 2).toUpperCase() }}</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -25,19 +26,47 @@
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
border-radius: 50%;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { Vue, Component, Prop } from 'vue-property-decorator'
|
||||
import { Vue, Component, Prop, Watch } from 'vue-property-decorator'
|
||||
|
||||
@Component({
|
||||
name: 'neko-avatar',
|
||||
})
|
||||
export default class extends Vue {
|
||||
@Prop(String) readonly seed: string | undefined
|
||||
@Prop(String) readonly avatar: string | undefined
|
||||
@Prop(Number) readonly size: number | undefined
|
||||
|
||||
private imageFailed: boolean = false
|
||||
|
||||
get imageURL(): string {
|
||||
if (this.imageFailed || !this.avatar) {
|
||||
return ''
|
||||
}
|
||||
|
||||
try {
|
||||
const avatar = new URL(this.avatar)
|
||||
return avatar.protocol === 'http:' || avatar.protocol === 'https:' ? avatar.toString() : ''
|
||||
} catch (_) {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
@Watch('avatar')
|
||||
onAvatarChanged() {
|
||||
this.imageFailed = false
|
||||
}
|
||||
|
||||
Background(seed: string) {
|
||||
let a = 0,
|
||||
b = 0,
|
||||
|
||||
@@ -11,7 +11,12 @@
|
||||
}"
|
||||
>
|
||||
<div class="author" @contextmenu.stop.prevent="onContext($event, { member: member(message.id) })">
|
||||
<neko-avatar class="avatar" :seed="member(message.id).displayname" :size="40" />
|
||||
<neko-avatar
|
||||
class="avatar"
|
||||
:seed="member(message.id).displayname"
|
||||
:avatar="member(message.id).avatar"
|
||||
:size="40"
|
||||
/>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="content-head">
|
||||
|
||||
@@ -8,11 +8,30 @@
|
||||
<form class="message" v-if="!connecting" @submit.stop.prevent="connect">
|
||||
<span v-if="!autoPassword">{{ $t('connect.login_title') }}</span>
|
||||
<span v-else>{{ $t('connect.invitation_title') }}</span>
|
||||
<input type="text" :placeholder="$t('connect.displayname')" v-model="displayname" autofocus />
|
||||
<input type="password" :placeholder="$t('connect.password')" v-model="password" v-if="!autoPassword" />
|
||||
<button type="submit" @click.stop.prevent="login">
|
||||
<input
|
||||
v-if="passwordLoginEnabled"
|
||||
type="text"
|
||||
:placeholder="$t('connect.displayname')"
|
||||
v-model="displayname"
|
||||
autofocus
|
||||
/>
|
||||
<input
|
||||
v-if="passwordLoginEnabled && !autoPassword"
|
||||
type="password"
|
||||
:placeholder="$t('connect.password')"
|
||||
v-model="password"
|
||||
/>
|
||||
<button v-if="passwordLoginEnabled" type="submit" @click.stop.prevent="login">
|
||||
{{ $t('connect.connect') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="oauthEnabled && !autoPassword"
|
||||
type="button"
|
||||
class="oauth-login"
|
||||
@click.stop.prevent="loginWithOAuth"
|
||||
>
|
||||
{{ oauthName }} Login
|
||||
</button>
|
||||
</form>
|
||||
<div class="loader" v-if="connecting">
|
||||
<div class="bounce1"></div>
|
||||
@@ -101,6 +120,11 @@
|
||||
line-height: 30px;
|
||||
margin: 5px 0;
|
||||
border: none;
|
||||
|
||||
&.oauth-login {
|
||||
background: $background-tertiary;
|
||||
border: 1px solid $style-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,8 +179,16 @@
|
||||
|
||||
private displayname: string = ''
|
||||
private password: string = ''
|
||||
private oauthEnabled: boolean = false
|
||||
private oauthName: string = 'OAuth'
|
||||
private oauthLoginURL: string = ''
|
||||
private passwordLoginEnabled: boolean = true
|
||||
|
||||
async mounted() {
|
||||
if (await this.loadOAuthConfig()) {
|
||||
return
|
||||
}
|
||||
|
||||
mounted() {
|
||||
// auto-password fill
|
||||
let password = this.$accessor.password
|
||||
if (this.autoPassword !== null) {
|
||||
@@ -182,6 +214,36 @@
|
||||
return this.$accessor.connecting
|
||||
}
|
||||
|
||||
async loadOAuthConfig(): Promise<boolean> {
|
||||
try {
|
||||
const response = await this.$http.get('api/oauth/config')
|
||||
const config = response.data
|
||||
this.oauthEnabled = config.enabled === true
|
||||
this.oauthName = config.name || 'OAuth'
|
||||
this.oauthLoginURL = config.login_url || ''
|
||||
this.passwordLoginEnabled = config.password_login_enabled !== false
|
||||
|
||||
if (this.oauthEnabled && !this.passwordLoginEnabled) {
|
||||
try {
|
||||
await this.$http.get('api/whoami')
|
||||
this.$accessor.login({ displayname: '', password: '' })
|
||||
} catch (_) {
|
||||
// No authenticated session yet; leave the OAuth button visible.
|
||||
}
|
||||
return true
|
||||
}
|
||||
} catch (_) {
|
||||
// An unauthenticated OAuth user remains on the OAuth login screen.
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
loginWithOAuth() {
|
||||
if (this.oauthLoginURL !== '') {
|
||||
window.location.assign(this.oauthLoginURL)
|
||||
}
|
||||
}
|
||||
|
||||
removeUrlParam(param: string) {
|
||||
let url = document.location.href
|
||||
let urlparts = url.split('?')
|
||||
|
||||
@@ -3,7 +3,12 @@
|
||||
<template slot-scope="child" v-if="child.data">
|
||||
<li class="header">
|
||||
<div class="user">
|
||||
<neko-avatar class="avatar" :seed="child.data.member.displayname" :size="25" />
|
||||
<neko-avatar
|
||||
class="avatar"
|
||||
:seed="child.data.member.displayname"
|
||||
:avatar="child.data.member.avatar"
|
||||
:size="25"
|
||||
/>
|
||||
<strong>{{ child.data.member.displayname }}</strong>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<ul class="members-list">
|
||||
<li v-if="member">
|
||||
<div :class="[{ host: member.id === host }, 'self', 'member']">
|
||||
<neko-avatar class="avatar" :seed="member.displayname" :size="50" />
|
||||
<neko-avatar class="avatar" :seed="member.displayname" :avatar="member.avatar" :size="50" />
|
||||
</div>
|
||||
</li>
|
||||
<template v-for="(member, index) in members">
|
||||
@@ -17,7 +17,7 @@
|
||||
:class="[{ host: member.id === host, admin: member.admin }, 'member']"
|
||||
@contextmenu.stop.prevent="onContext($event, { member })"
|
||||
>
|
||||
<neko-avatar class="avatar" :seed="member.displayname" :size="50" />
|
||||
<neko-avatar class="avatar" :seed="member.displayname" :avatar="member.avatar" :size="50" />
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export interface Member {
|
||||
id: string
|
||||
displayname: string
|
||||
avatar?: string
|
||||
admin: boolean
|
||||
muted: boolean
|
||||
connected?: boolean
|
||||
|
||||
Reference in New Issue
Block a user