вход переехал на доверенный заголовок Authelia вместо собственного OIDC
- пришедшего называет заголовок Remote-User от прокси, и верят ему только с адреса из перечня trusted_proxies; своего входа у сервиса не осталось — ни корня /auth, ни кук, ни срока сессии, ни секрета клиента в конфиге и в базе - учётная запись заводится первым обращением: EnsureUser в пакете хранилища, шаг схемы 202608220001 с колонкой provider_login и снятыми правилами users - cmd/oidcstub заменён на cmd/devtools с подкомандой proxy; заодно закрыт унаследованный DL3066 — пользователь образа назван числом
This commit is contained in:
+15
-10
@@ -17,14 +17,15 @@ interface ErrorBody {
|
||||
}
|
||||
|
||||
/**
|
||||
* Исход запроса. Отсутствие сессии — свой исход, а не разновидность неудачи:
|
||||
* экран уводит ко входу только на нём. Всякий прочий отказ ко входу не ведёт,
|
||||
* иначе отказ сервиса или ограничителя частоты замкнул бы круг «приложение →
|
||||
* вход → приложение».
|
||||
* Исход запроса. «Сервис вас не узнал» — свой исход, а не разновидность
|
||||
* неудачи: человек по нему делает не то, что по «сервис не отвечает». Прежде
|
||||
* этот исход уводил ко входу; уводить стало некуда — своего входа у сервиса
|
||||
* нет, а заголовок ставит обратный прокси, — и различие сохраняется ради
|
||||
* текста, а не ради перехода.
|
||||
*/
|
||||
export type ApiResult<T> =
|
||||
| { status: 'ok'; value: T }
|
||||
| { status: 'unauthorized' }
|
||||
| { status: 'unrecognized'; message: string }
|
||||
| { status: 'failed'; message: string }
|
||||
|
||||
const noConnectionMessage = 'Связи нет'
|
||||
@@ -54,7 +55,11 @@ async function request<T>(path: string): Promise<ApiResult<T>> {
|
||||
}
|
||||
|
||||
if (response.status === 401) {
|
||||
return { status: 'unauthorized' }
|
||||
// Текст берётся у сервера, а не сочиняется здесь: единая форма отказа —
|
||||
// обязанность API, и второй словарь на клиенте разошёлся бы с первым.
|
||||
// Свой исход у этого кода всё равно есть: «сервис вас не узнал» и «сервис
|
||||
// не отвечает» ведут человека к разному.
|
||||
return { status: 'unrecognized', message: await failureMessage(response) }
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -68,10 +73,10 @@ async function request<T>(path: string): Promise<ApiResult<T>> {
|
||||
}
|
||||
}
|
||||
|
||||
/** Спрашивает сервис, кто вошёл. Куку сессии браузер шлёт сам. */
|
||||
/**
|
||||
* Спрашивает сервис, кто пришёл. Заголовок ставит обратный прокси, и приложение
|
||||
* его не видит вовсе — этот адрес единственный способ узнать вошедшего.
|
||||
*/
|
||||
export function fetchMe(): Promise<ApiResult<Me>> {
|
||||
return request<Me>('/me')
|
||||
}
|
||||
|
||||
/** Адрес, которым сервис уводит человека ко входу. */
|
||||
export const loginPath = '/auth/login'
|
||||
|
||||
@@ -4,6 +4,10 @@ import HomeScreen from './HomeScreen.vue'
|
||||
|
||||
// Экран судится по тому, что видит человек, а не по внутреннему состоянию:
|
||||
// переписанная реализация обязана остаться зелёной.
|
||||
//
|
||||
// Переход к адресу входа проверяется тем, что его **нет**: своего входа у
|
||||
// сервиса не осталось, и экран, снова начавший куда-то уводить, обязан упасть
|
||||
// здесь, а не обнаружиться пустой страницей у человека.
|
||||
|
||||
const assign = vi.fn()
|
||||
|
||||
@@ -23,7 +27,7 @@ afterEach(() => {
|
||||
})
|
||||
|
||||
describe('экран показывает вошедшего', () => {
|
||||
it('показывает имя, когда сессия есть', async () => {
|
||||
it('показывает имя, когда сервис узнал пришедшего', async () => {
|
||||
answerWith({
|
||||
ok: true,
|
||||
status: 200,
|
||||
@@ -37,16 +41,43 @@ describe('экран показывает вошедшего', () => {
|
||||
expect(assign).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('ведёт ко входу, когда сессии нет', async () => {
|
||||
answerWith({ ok: false, status: 401, json: () => Promise.resolve({}) })
|
||||
it('показывает текст сервера, когда тот не узнал пришедшего, и никуда не уводит', async () => {
|
||||
answerWith({
|
||||
ok: false,
|
||||
status: 401,
|
||||
json: () => Promise.resolve({ error_code: 'unauthorized', message: 'Сервис вас не узнал' }),
|
||||
})
|
||||
|
||||
mount(HomeScreen)
|
||||
const screen = mount(HomeScreen)
|
||||
await flushPromises()
|
||||
|
||||
expect(assign).toHaveBeenCalledWith('/auth/login')
|
||||
expect(screen.text()).toContain('Сервис вас не узнал')
|
||||
expect(assign).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('на прочий отказ показывает строку и ко входу не ведёт', async () => {
|
||||
it('на неузнавание без текста сервера показывает общее, а не пустоту', async () => {
|
||||
answerWith({ ok: false, status: 401, json: () => Promise.resolve({}) })
|
||||
|
||||
const screen = mount(HomeScreen)
|
||||
await flushPromises()
|
||||
|
||||
expect(screen.text()).toContain('Сервис не отвечает как надо')
|
||||
})
|
||||
|
||||
it('на неразобранное тело показывает строку, а не падает', async () => {
|
||||
answerWith({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: () => Promise.reject(new SyntaxError('not json')),
|
||||
})
|
||||
|
||||
const screen = mount(HomeScreen)
|
||||
await flushPromises()
|
||||
|
||||
expect(screen.text()).toContain('Сервис не отвечает как надо')
|
||||
})
|
||||
|
||||
it('прочий отказ отличается от неузнавания', async () => {
|
||||
answerWith({
|
||||
ok: false,
|
||||
status: 500,
|
||||
@@ -70,7 +101,7 @@ describe('экран показывает вошедшего', () => {
|
||||
expect(assign).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('учётная запись без имени: вход выполнен, почты на экране нет', async () => {
|
||||
it('учётная запись без имени: человек узнан, почты на экране нет', async () => {
|
||||
answerWith({
|
||||
ok: true,
|
||||
status: 200,
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { fetchMe, loginPath } from '../api'
|
||||
import { fetchMe } from '../api'
|
||||
|
||||
// Состояние экрана живёт в экране: общего между экранами пока нет.
|
||||
const state = ref<'loading' | 'signed-in' | 'failed'>('loading')
|
||||
const state = ref<'loading' | 'signed-in' | 'unrecognized' | 'failed'>('loading')
|
||||
const name = ref('')
|
||||
const failure = ref('')
|
||||
const unrecognized = ref('')
|
||||
|
||||
async function load() {
|
||||
const result = await fetchMe()
|
||||
|
||||
if (result.status === 'unauthorized') {
|
||||
// Единственная ветка, уводящая ко входу. Прочие отказы ведут сюда же
|
||||
// только через круг «приложение → вход → приложение», поэтому их здесь нет.
|
||||
window.location.assign(loginPath)
|
||||
if (result.status === 'unrecognized') {
|
||||
// Уводить некуда: своего входа у сервиса нет, а имя пришедшего ставит
|
||||
// обратный прокси. Показываем строкой — той, что прислал сервер, — и
|
||||
// отдельной от прочей неудачи: «вас не узнали» и «сервис не отвечает»
|
||||
// ведут человека к разному.
|
||||
unrecognized.value = result.message
|
||||
state.value = 'unrecognized'
|
||||
return
|
||||
}
|
||||
|
||||
@@ -36,6 +40,8 @@ onMounted(load)
|
||||
|
||||
<p v-if="state === 'loading'">Загружается…</p>
|
||||
|
||||
<p v-else-if="state === 'unrecognized'">{{ unrecognized }}</p>
|
||||
|
||||
<p v-else-if="state === 'failed'">{{ failure }}</p>
|
||||
|
||||
<!-- Имени у учётной записи может не быть вовсе: тогда говорим, что вход
|
||||
|
||||
Reference in New Issue
Block a user