BIN
assets/images/help/settings/github-mobile-active-sessions.png
Normal file
BIN
assets/images/help/settings/github-mobile-active-sessions.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
BIN
assets/images/help/settings/revoke-mobile-session.png
Normal file
BIN
assets/images/help/settings/revoke-mobile-session.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
BIN
assets/images/help/settings/revoke-session.png
Normal file
BIN
assets/images/help/settings/revoke-session.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 55 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 53 KiB |
@@ -6,13 +6,14 @@ import { ArticleGridLayout } from './ArticleGridLayout'
|
||||
import { MiniTocs } from 'components/ui/MiniTocs'
|
||||
import { useAutomatedPageContext } from 'components/context/AutomatedPageContext'
|
||||
import { ClientSideHighlight } from 'components/ClientSideHighlight'
|
||||
import { Callout } from 'components/ui/Callout'
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export const AutomatedPage = ({ children }: Props) => {
|
||||
const { title, intro, renderedPage, miniTocItems } = useAutomatedPageContext()
|
||||
const { title, intro, renderedPage, miniTocItems, product } = useAutomatedPageContext()
|
||||
|
||||
return (
|
||||
<DefaultLayout>
|
||||
@@ -22,11 +23,21 @@ export const AutomatedPage = ({ children }: Props) => {
|
||||
<ArticleGridLayout
|
||||
topper={<ArticleTitle>{title}</ArticleTitle>}
|
||||
intro={
|
||||
intro && (
|
||||
<Lead data-testid="lead" data-search="lead">
|
||||
{intro}
|
||||
</Lead>
|
||||
)
|
||||
<>
|
||||
{intro && (
|
||||
<Lead data-testid="lead" data-search="lead">
|
||||
{intro}
|
||||
</Lead>
|
||||
)}
|
||||
|
||||
{product && (
|
||||
<Callout
|
||||
variant="success"
|
||||
className="mb-4"
|
||||
dangerouslySetInnerHTML={{ __html: product }}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
toc={miniTocItems.length > 1 && <MiniTocs miniTocItems={miniTocItems} />}
|
||||
>
|
||||
|
||||
@@ -6,6 +6,7 @@ export type AutomatedPageContextT = {
|
||||
intro: string
|
||||
renderedPage: string | JSX.Element[]
|
||||
miniTocItems: Array<MiniTocItem>
|
||||
product?: string
|
||||
}
|
||||
|
||||
export const AutomatedPageContext = createContext<AutomatedPageContextT | null>(null)
|
||||
@@ -30,5 +31,6 @@ export const getAutomatedPageContextFromRequest = (req: any): AutomatedPageConte
|
||||
intro: page.intro,
|
||||
renderedPage: req.context.renderedPage || '',
|
||||
miniTocItems: req.context.miniTocItems || [],
|
||||
product: page.product || '',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { ChildParameter } from './types'
|
||||
import styles from './ChildBodyParametersRows.module.scss'
|
||||
|
||||
type Props = {
|
||||
open: boolean
|
||||
slug: string
|
||||
childParamsGroups: ChildParameter[]
|
||||
parentName: string
|
||||
@@ -14,6 +15,7 @@ type Props = {
|
||||
}
|
||||
|
||||
export function ChildBodyParametersRows({
|
||||
open,
|
||||
slug,
|
||||
parentName,
|
||||
parentType,
|
||||
@@ -24,24 +26,27 @@ export function ChildBodyParametersRows({
|
||||
return (
|
||||
<tr className={cx(styles.childBodyParametersRows, 'color-bg-subtle border-top-0')}>
|
||||
<td colSpan={4} className="has-nested-table">
|
||||
<details className="box px-3 ml-1 mb-0">
|
||||
<details className="box px-3 ml-1 mb-0" open={open}>
|
||||
<summary role="button" aria-expanded="false" className="mb-2 keyboard-focus">
|
||||
<span id={`${slug}-${parentName}-${parentType}`}>Properties of {parentName}</span>
|
||||
<span id={`${slug}-${parentName}-${parentType}`}>
|
||||
Properties of <code>{parentName}</code>
|
||||
</span>
|
||||
</summary>
|
||||
<table id={`${parentName}-object`} className="mb-4 mt-2 color-bg-subtle">
|
||||
<table id={`${parentName}-object`} className="mb-4 color-bg-subtle">
|
||||
<thead className="visually-hidden">
|
||||
<tr>
|
||||
<th>{`${t('name')}, ${t('type')}, ${t('description')}`}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{childParamsGroups.map((childParam) => {
|
||||
{childParamsGroups.map((childParam, index) => {
|
||||
return (
|
||||
<ParameterRow
|
||||
rowParams={childParam}
|
||||
slug={slug}
|
||||
isChild={true}
|
||||
key={childParam.name}
|
||||
rowIndex={index}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import cx from 'classnames'
|
||||
|
||||
import { useTranslation } from 'components/hooks/useTranslation'
|
||||
import { KeyboardEventHandler } from 'react'
|
||||
import { ChildBodyParametersRows } from './ChildBodyParametersRows'
|
||||
import type { ChildParameter } from './types'
|
||||
|
||||
@@ -10,14 +11,38 @@ type Props = {
|
||||
numPreviews?: number
|
||||
isChild?: boolean
|
||||
rowIndex?: number
|
||||
bodyParamExpandCallback?: KeyboardEventHandler<HTMLButtonElement> | undefined
|
||||
clickedBodyParameterName?: string | undefined
|
||||
}
|
||||
|
||||
// Webhooks have these same properties in common that we describe separately in its
|
||||
// own section on the webhooks page:
|
||||
//
|
||||
// https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#webhook-payload-object-common-properties
|
||||
//
|
||||
// Since there's more details for these particular properties, we chose not
|
||||
// show their child properties for each webhook and we also don't grab this
|
||||
// information from the schema.
|
||||
//
|
||||
// We use this list of common properties to make sure we don't try and request
|
||||
// the child properties for these specific properties.
|
||||
const NO_CHILD_WEBHOOK_PROPERTIES = [
|
||||
'action',
|
||||
'enterprise',
|
||||
'installation',
|
||||
'organization',
|
||||
'repository',
|
||||
'sender',
|
||||
]
|
||||
|
||||
export function ParameterRow({
|
||||
rowParams,
|
||||
slug,
|
||||
numPreviews = 0,
|
||||
rowIndex = 0,
|
||||
isChild = false,
|
||||
rowIndex = 0,
|
||||
bodyParamExpandCallback = undefined,
|
||||
clickedBodyParameterName = undefined,
|
||||
}: Props) {
|
||||
const { t } = useTranslation(['parameter_table', 'products'])
|
||||
|
||||
@@ -97,8 +122,40 @@ export function ParameterRow({
|
||||
parentName={rowParams.name}
|
||||
parentType={rowParams.type}
|
||||
childParamsGroups={rowParams.childParamsGroups}
|
||||
open={rowParams.name === clickedBodyParameterName}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* These conditions tell us:
|
||||
|
||||
1. the param is an object or array AND:
|
||||
2. the param has no child param groups AND:
|
||||
3. the param isn't one of the common webhook properties
|
||||
|
||||
If all these are true, then that means we haven't yet loaded the
|
||||
nested parameters so we show a stub <details> element that triggers
|
||||
an API request to get the nested parameter data.
|
||||
*/}
|
||||
{(rowParams.type === 'object' || rowParams.type.includes('array of')) &&
|
||||
rowParams.childParamsGroups &&
|
||||
rowParams.childParamsGroups.length === 0 &&
|
||||
!NO_CHILD_WEBHOOK_PROPERTIES.includes(rowParams.name) && (
|
||||
<tr className="border-top-0">
|
||||
<td colSpan={4} className="has-nested-table">
|
||||
<details
|
||||
data-nested-param-id={rowParams.name}
|
||||
className="box px-3 ml-1 mb-0"
|
||||
onToggle={bodyParamExpandCallback}
|
||||
>
|
||||
<summary role="button" aria-expanded="false" className="mb-2 keyboard-focus">
|
||||
<span id={`${slug}-${rowParams.name}`}>
|
||||
Properties of <code>{rowParams.name}</code>
|
||||
</span>
|
||||
</summary>
|
||||
</details>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import cx from 'classnames'
|
||||
import { useTranslation } from 'components/hooks/useTranslation'
|
||||
import { KeyboardEventHandler } from 'react'
|
||||
|
||||
import { ParameterRow } from './ParameterRow'
|
||||
import { BodyParameter, ChildParameter, Parameter } from './types'
|
||||
@@ -8,20 +9,24 @@ import styles from './ParameterTable.module.scss'
|
||||
|
||||
type Props = {
|
||||
slug: string
|
||||
numPreviews: number
|
||||
heading: string
|
||||
headers: Array<ChildParameter>
|
||||
parameters: Array<Parameter>
|
||||
numPreviews?: number
|
||||
heading?: string
|
||||
headers?: Array<ChildParameter>
|
||||
parameters?: Array<Parameter>
|
||||
bodyParameters: Array<BodyParameter>
|
||||
bodyParamExpandCallback?: KeyboardEventHandler<HTMLButtonElement> | undefined
|
||||
clickedBodyParameterName?: string | undefined
|
||||
}
|
||||
|
||||
export function ParameterTable({
|
||||
slug,
|
||||
numPreviews,
|
||||
numPreviews = 0,
|
||||
heading = '',
|
||||
headers = [],
|
||||
parameters,
|
||||
parameters = [],
|
||||
bodyParameters,
|
||||
bodyParamExpandCallback = undefined,
|
||||
clickedBodyParameterName = '',
|
||||
}: Props) {
|
||||
const { t } = useTranslation(['parameter_table', 'products'])
|
||||
const queryParams = parameters.filter((param) => param.in === 'query')
|
||||
@@ -41,7 +46,11 @@ export function ParameterTable({
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th id="header" scope="col" className="text-bold pl-0">
|
||||
<th
|
||||
id="header"
|
||||
scope="col"
|
||||
className={cx(headers.length === 0 && 'visually-hidden', 'text-bold pl-0')}
|
||||
>
|
||||
{t('headers')}
|
||||
</th>
|
||||
</tr>
|
||||
@@ -122,7 +131,9 @@ export function ParameterTable({
|
||||
{bodyParameters.length > 0 && (
|
||||
<>
|
||||
<tr className="border-top-0">
|
||||
<th scope="colgroup" className="text-bold pl-0">
|
||||
{/* webhooks don't have a 'Parameters' table heading text so
|
||||
we adjust the size of the body params heading in that case */}
|
||||
<th scope="colgroup" className={cx(heading ? 'text-bold' : 'h4', 'pl-0')}>
|
||||
{t('body')}
|
||||
</th>
|
||||
</tr>
|
||||
@@ -131,7 +142,13 @@ export function ParameterTable({
|
||||
</tr>
|
||||
|
||||
{bodyParameters.map((param, index) => (
|
||||
<ParameterRow rowParams={param} slug={slug} key={`${index}-${param}`} />
|
||||
<ParameterRow
|
||||
bodyParamExpandCallback={bodyParamExpandCallback}
|
||||
clickedBodyParameterName={clickedBodyParameterName}
|
||||
rowParams={param}
|
||||
slug={slug}
|
||||
key={`${index}-${param}`}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
195
components/webhooks/Webhook.tsx
Normal file
195
components/webhooks/Webhook.tsx
Normal file
@@ -0,0 +1,195 @@
|
||||
import { ActionList, ActionMenu, Flash } from '@primer/react'
|
||||
import { useState, KeyboardEvent } from 'react'
|
||||
import useSWR from 'swr'
|
||||
import { slug } from 'github-slugger'
|
||||
import cx from 'classnames'
|
||||
|
||||
import { useMainContext } from 'components/context/MainContext'
|
||||
import { useVersion } from 'components/hooks/useVersion'
|
||||
import { LinkIconHeading } from 'components/article/LinkIconHeading'
|
||||
import { useTranslation } from 'components/hooks/useTranslation'
|
||||
import type { WebhookAction, WebhookData } from './types'
|
||||
import { ParameterTable } from 'components/parameter-table/ParameterTable'
|
||||
|
||||
import styles from './WebhookPayloadExample.module.scss'
|
||||
|
||||
type Props = {
|
||||
webhook: WebhookAction
|
||||
}
|
||||
|
||||
// fetcher passed to useSWR() to get webhook data using the given URL
|
||||
async function webhookFetcher(url: string) {
|
||||
const response = await fetch(url)
|
||||
if (!response.ok) {
|
||||
throw new Error(`${response.status} on ${url}`)
|
||||
}
|
||||
|
||||
return response.json()
|
||||
}
|
||||
|
||||
// We manually created decorated webhooks files for GHES versions older than
|
||||
// 3.7, returns whether the given version is one of these versions of GHES.
|
||||
//
|
||||
// TODO: once 3.7 is the oldest supported version of GHES, we won't need this
|
||||
// anymore.
|
||||
function isScrapedGhesVersion(version: ReturnType<typeof useVersion>) {
|
||||
const scrapedVersions = ['3.6', '3.5', '3.4', '3.3', '3.2']
|
||||
|
||||
if (!version.isEnterprise) return false
|
||||
|
||||
// getting the number part e.g. '3.6' from a version string like
|
||||
// 'enterprise-server@3.6'
|
||||
const versionNumber = version.currentVersion.split('@')[1]
|
||||
|
||||
return scrapedVersions.includes(versionNumber)
|
||||
}
|
||||
|
||||
export function Webhook({ webhook }: Props) {
|
||||
// Get version for requests to switch webhook action type
|
||||
const version = useVersion()
|
||||
const { t } = useTranslation('products')
|
||||
|
||||
const context = useMainContext()
|
||||
// Get more user friendly language for the different availability options in
|
||||
// the webhook schema (we can't change it directly in the schema). Note that
|
||||
// we specifically don't want to translate these strings with useTranslation()
|
||||
// like we usually do with strings from data/ui.yml.
|
||||
const rephraseAvailability = context.data.ui.products.webhooks.rephrase_availability
|
||||
|
||||
// The param that was clicked so we can expand its property <details> element
|
||||
const [clickedBodyParameterName, setClickedBodyParameterName] = useState<undefined | string>('')
|
||||
// The selected webhook action type the user selects via a dropdown
|
||||
const [selectedWebhookActionType, setSelectedWebhookActionType] = useState('')
|
||||
const webhookSlug = slug(webhook.data.category)
|
||||
const webhookFetchUrl = `/api/webhooks/v1?${new URLSearchParams({
|
||||
category: webhook.data.category,
|
||||
version: version.currentVersion,
|
||||
})}`
|
||||
|
||||
// callback for the action type dropdown -- besides setting the action type
|
||||
// state, we also want to clear the clicked body param so that no properties
|
||||
// are expanded when we re-render the webhook
|
||||
function handleActionTypeChange(type: string) {
|
||||
setClickedBodyParameterName('')
|
||||
setSelectedWebhookActionType(type)
|
||||
}
|
||||
|
||||
// callback to trigger useSWR() hook after a nested property is clicked
|
||||
function handleBodyParamExpansion(event: KeyboardEvent<HTMLElement>) {
|
||||
// need to cast it because 'closest' isn't necessarily available on
|
||||
// event.target
|
||||
const target = event.target as HTMLElement
|
||||
setClickedBodyParameterName(target.closest('details')?.dataset.nestedParamId)
|
||||
}
|
||||
|
||||
// fires when the webhook action type changes or someone clicks on a nested
|
||||
// body param for the first time. In either case, we now have all the data
|
||||
// for a webhook (i.e. all the data for each action type and all of their
|
||||
// nested parameters)
|
||||
const { data, error } = useSWR<WebhookData, Error>(
|
||||
clickedBodyParameterName || selectedWebhookActionType ? webhookFetchUrl : null,
|
||||
webhookFetcher,
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
}
|
||||
)
|
||||
|
||||
const currentWebhookActionType = selectedWebhookActionType || webhook.data.action
|
||||
const currentWebhookAction = (data && data[currentWebhookActionType]) || webhook.data
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 id={webhookSlug}>
|
||||
<LinkIconHeading slug={webhookSlug} />
|
||||
{currentWebhookAction.category}
|
||||
</h2>
|
||||
<div>
|
||||
<div dangerouslySetInnerHTML={{ __html: currentWebhookAction.summaryHtml }}></div>
|
||||
|
||||
<h3>{t('webhooks.availability')}</h3>
|
||||
<ul>
|
||||
{currentWebhookAction.availability.map((availability) => {
|
||||
// TODO: once 3.7 is the oldest supported version of GHES, we won't need this anymore.
|
||||
if (isScrapedGhesVersion(version)) {
|
||||
return (
|
||||
<li
|
||||
dangerouslySetInnerHTML={{ __html: availability }}
|
||||
key={`availability-${availability}`}
|
||||
></li>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<li key={`availability-${availability}`}>
|
||||
{rephraseAvailability[availability] ?? availability}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
})}
|
||||
</ul>
|
||||
<h3>{t('webhooks.webhook_payload_object')}</h3>
|
||||
{error && (
|
||||
<Flash className="mb-5" variant="danger">
|
||||
<p>{t('webhooks.action_type_switch_error')}</p>
|
||||
<p>
|
||||
<code className="f6" style={{ background: 'none' }}>
|
||||
{error.toString()}
|
||||
</code>
|
||||
</p>
|
||||
</Flash>
|
||||
)}
|
||||
{webhook.actionTypes.length > 1 && (
|
||||
<div className="mb-4">
|
||||
<h4 className="border-bottom pt-2 pb-2 mb-3">{t('webhooks.action_type')}</h4>
|
||||
<div className="mb-3">
|
||||
<ActionMenu>
|
||||
<ActionMenu.Button className="text-bold">
|
||||
{currentWebhookActionType}
|
||||
</ActionMenu.Button>
|
||||
<ActionMenu.Overlay>
|
||||
<ActionList>
|
||||
{webhook.actionTypes.map((type) => {
|
||||
return (
|
||||
<ActionList.Item
|
||||
disabled={type === currentWebhookActionType}
|
||||
key={`${webhook.name}-${type}`}
|
||||
onSelect={() => handleActionTypeChange(type)}
|
||||
>
|
||||
{type}
|
||||
</ActionList.Item>
|
||||
)
|
||||
})}
|
||||
</ActionList>
|
||||
</ActionMenu.Overlay>
|
||||
</ActionMenu>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="mb-4 f5 color-fg-muted"
|
||||
dangerouslySetInnerHTML={{ __html: currentWebhookAction.descriptionHtml }}
|
||||
></div>
|
||||
<div>
|
||||
<ParameterTable
|
||||
slug={slug(`${currentWebhookAction.category}-${selectedWebhookActionType}`)}
|
||||
bodyParameters={currentWebhookAction.bodyParameters || []}
|
||||
bodyParamExpandCallback={handleBodyParamExpansion}
|
||||
clickedBodyParameterName={clickedBodyParameterName}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{webhook.data.payloadExample && (
|
||||
<>
|
||||
<h3>{t('webhooks.webhook_payload_example')}</h3>
|
||||
<div
|
||||
className={cx(styles.payloadExample, 'border-top rounded-1 my-0')}
|
||||
style={{ maxHeight: '32rem' }}
|
||||
data-highlight={'json'}
|
||||
>
|
||||
<code>{JSON.stringify(webhook.data.payloadExample, null, 2)}</code>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
16
components/webhooks/WebhookPayloadExample.module.scss
Normal file
16
components/webhooks/WebhookPayloadExample.module.scss
Normal file
@@ -0,0 +1,16 @@
|
||||
@import "@primer/css/support/index.scss";
|
||||
|
||||
.payloadExample {
|
||||
overflow: auto;
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.45;
|
||||
background-color: var(--color-canvas-subtle);
|
||||
font-size: 90%;
|
||||
max-height: 32rem;
|
||||
|
||||
code {
|
||||
background-color: transparent;
|
||||
padding: 8px 8px 16px;
|
||||
white-space: pre;
|
||||
}
|
||||
}
|
||||
34
components/webhooks/types.ts
Normal file
34
components/webhooks/types.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Parameter, StatusCode, CodeSample, BodyParameter } from '../rest/types'
|
||||
|
||||
export interface WebhookT {
|
||||
actions: string[]
|
||||
webhookActions: WebhookAction[]
|
||||
}
|
||||
|
||||
interface WebhookActionData {
|
||||
verb: string
|
||||
title: string
|
||||
descriptionHtml: string
|
||||
summaryHtml: string
|
||||
previews: Array<string>
|
||||
requestPath: string
|
||||
serverUrl: string
|
||||
statusCodes: Array<StatusCode>
|
||||
parameters: Array<Parameter>
|
||||
bodyParameters: Array<BodyParameter>
|
||||
category: string
|
||||
subcategory: string
|
||||
codeExamples: Array<CodeSample>
|
||||
availability: Array<string>
|
||||
action: string
|
||||
payloadExample?: Object
|
||||
}
|
||||
export interface WebhookAction {
|
||||
name: string
|
||||
actionTypes: string[]
|
||||
data: WebhookActionData
|
||||
}
|
||||
|
||||
export type WebhookData = {
|
||||
[key: string]: WebhookActionData
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Viewing and managing your active SAML sessions
|
||||
intro: You can view and revoke your active SAML sessions in your security settings.
|
||||
intro: You can view and revoke your active SAML sessions in your settings.
|
||||
redirect_from:
|
||||
- /articles/viewing-and-managing-your-active-saml-sessions
|
||||
- /github/authenticating-to-github/viewing-and-managing-your-active-saml-sessions
|
||||
@@ -9,16 +9,24 @@ versions:
|
||||
ghec: '*'
|
||||
topics:
|
||||
- SSO
|
||||
type: how_to
|
||||
shortTitle: Active SAML sessions
|
||||
---
|
||||
|
||||
You can view a list of devices that have logged into your account, and revoke any SAML sessions that you don't recognize.
|
||||
|
||||
{% data reusables.user-settings.access_settings %}
|
||||
{% data reusables.user-settings.security %}
|
||||
3. Under "Sessions," you can see your active SAML sessions.
|
||||

|
||||
4. To see the session details, click **See more**.
|
||||

|
||||
5. To revoke a session, click **Revoke SAML**.
|
||||

|
||||
{% data reusables.user-settings.sessions %}
|
||||
1. Under "Web sessions," you can see your active SAML sessions.
|
||||
|
||||

|
||||
|
||||
1. To see the session details, click **See more**.
|
||||

|
||||
|
||||
1. To revoke a session, click **Revoke SAML**.
|
||||
|
||||

|
||||
|
||||
{% note %}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ children:
|
||||
- /githubs-ssh-key-fingerprints
|
||||
- /sudo-mode
|
||||
- /preventing-unauthorized-access
|
||||
- /viewing-and-managing-your-sessions
|
||||
shortTitle: Account security
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
title: Viewing and managing your sessions
|
||||
intro: You can view and revoke your active sessions in your settings.
|
||||
versions:
|
||||
feature: device-and-settings-management-page
|
||||
type: how_to
|
||||
topics:
|
||||
- SSO
|
||||
shortTitle: Viewing and managing sessions
|
||||
---
|
||||
|
||||
You can view a list of devices that have logged into your account, and revoke any sessions that you don't recognize.
|
||||
|
||||
{% data reusables.user-settings.access_settings %}
|
||||
{% data reusables.user-settings.sessions %}
|
||||
1. Under "Web sessions", you can see your active web sessions.
|
||||
|
||||

|
||||
{% ifversion fpt or ghec %}
|
||||
Under "{% data variables.product.prodname_mobile %} sessions", you can see a list of devices that have logged into your account via the {% data variables.product.prodname_mobile %} app.
|
||||
|
||||
{% endif %}
|
||||
|
||||
1. To see the web session details, click **See more**.
|
||||
|
||||

|
||||
|
||||
1. To revoke a web session, click **Revoke session**.
|
||||
|
||||

|
||||
|
||||
{% ifversion fpt or ghec %}
|
||||
1. Optionally, to revoke a {% data variables.product.prodname_mobile %} session, go back to the Sessions overview page and click **Revoke** next to the device you want to revoke.
|
||||
|
||||
{% note %}
|
||||
|
||||
**Note:** Revoking a mobile session signs you out of the {% data variables.product.prodname_mobile %} application on that device and removes it as a second-factor option.
|
||||
|
||||
{% endnote %}
|
||||
|
||||

|
||||
|
||||
{% endif %}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
7
data/features/device-and-settings-management-page.yml
Normal file
7
data/features/device-and-settings-management-page.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
# Reference: #8482.
|
||||
# Device and session management settings page
|
||||
versions:
|
||||
fpt: '*'
|
||||
ghec: '*'
|
||||
ghes: '>=3.8'
|
||||
ghae: '>= 3.8'
|
||||
1
data/reusables/user-settings/sessions.md
Normal file
1
data/reusables/user-settings/sessions.md
Normal file
@@ -0,0 +1 @@
|
||||
1. In the "Access" section of the sidebar, click **{% octicon "broadcast" aria-label="The broadcast icon" %} Sessions**.
|
||||
20
data/ui.yml
20
data/ui.yml
@@ -101,6 +101,7 @@ parameter_table:
|
||||
see_preview_notice: See preview notice
|
||||
see_preview_notices: See preview notices
|
||||
type: Type
|
||||
single_enum_description: Value
|
||||
products:
|
||||
graphql:
|
||||
reference:
|
||||
@@ -141,6 +142,25 @@ products:
|
||||
preview_notice_to_change: This API is under preview and subject to change
|
||||
works_with: Works with
|
||||
api_reference: REST API reference
|
||||
enum_description_title: Can be one of
|
||||
required: Required
|
||||
headers: Headers
|
||||
query: Query parameters
|
||||
path: Path parameters
|
||||
body: Body parameters
|
||||
webhooks:
|
||||
action_type_switch_error: There was an error switching webhook action types.
|
||||
action_type: Action type
|
||||
availability: Availability
|
||||
webhook_payload_object: Webhook payload object
|
||||
webhook_payload_example: Webhook payload example
|
||||
rephrase_availability:
|
||||
repository: Repositories
|
||||
organization: Organizations
|
||||
app: GitHub Apps
|
||||
business: Enterprises
|
||||
marketplace: GitHub Marketplace
|
||||
sponsors_listing: Sponsored accounts
|
||||
footer:
|
||||
all_rights_reserved: All rights reserved
|
||||
terms: Terms
|
||||
|
||||
@@ -3,9 +3,92 @@ import path from 'path'
|
||||
import fs from 'fs'
|
||||
import walk from 'walk-sync'
|
||||
import { set } from 'lodash-es'
|
||||
|
||||
import { allVersions } from '../all-versions.js'
|
||||
import { readCompressedJsonFileFallback } from '../read-json-file.js'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const staticDir = path.join(__dirname, 'static')
|
||||
const schemasPath = path.join(__dirname, 'static/decorated')
|
||||
|
||||
// cache for webhook data per version
|
||||
const webhooksCache = new Map()
|
||||
// cache for webhook data for when you first visit the webhooks page where we
|
||||
// show all webhooks for the current version but only 1 action type per webhook
|
||||
// and also no nested parameters
|
||||
const initialWebhooksCache = new Map()
|
||||
|
||||
// return the webhoook data as described for `initialWebhooksCache` for the given
|
||||
// version
|
||||
export async function getInitialPageWebhooks(version) {
|
||||
if (initialWebhooksCache.has(version)) {
|
||||
return initialWebhooksCache.get(version)
|
||||
}
|
||||
const allWebhooks = await getWebhooks(version)
|
||||
const initialWebhooks = []
|
||||
|
||||
// The webhooks page shows all webhooks but for each webhook only a single
|
||||
// webhook action type at a time. We pick the first webhook type from each
|
||||
// webhook's set of action types to show.
|
||||
for (const [key, webhook] of Object.entries(allWebhooks)) {
|
||||
const actionTypes = Object.keys(webhook)
|
||||
const defaultAction = actionTypes ? actionTypes[0] : null
|
||||
|
||||
const initialWebhook = {
|
||||
name: key,
|
||||
actionTypes,
|
||||
data: webhook[defaultAction],
|
||||
}
|
||||
|
||||
// remove all nested params for the initial webhooks page, we'll load
|
||||
// them by request
|
||||
if (initialWebhook.data.bodyParameters) {
|
||||
initialWebhook.data.bodyParameters.forEach((bodyParam) => {
|
||||
if (bodyParam.childParamsGroups) {
|
||||
bodyParam.childParamsGroups = []
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
initialWebhooks.push({ ...initialWebhook })
|
||||
}
|
||||
initialWebhooksCache.set(version, initialWebhooks)
|
||||
return initialWebhooks
|
||||
}
|
||||
|
||||
// returns the webhook data for the given version and webhook category (e.g.
|
||||
// `check_run`) -- this includes all the data per webhook action type and all
|
||||
// nested parameters
|
||||
export async function getWebhook(version, webhookCategory) {
|
||||
const webhooks = await getWebhooks(version)
|
||||
return webhooks[webhookCategory]
|
||||
}
|
||||
|
||||
// returns all the webhook data for the given version
|
||||
export async function getWebhooks(version) {
|
||||
const openApiVersion = getOpenApiVersion(version)
|
||||
if (!webhooksCache.has(openApiVersion)) {
|
||||
const filename = `${openApiVersion}.json`
|
||||
|
||||
// The `readCompressedJsonFileFallback()` function
|
||||
// will check for both a .br and .json extension.
|
||||
webhooksCache.set(
|
||||
openApiVersion,
|
||||
readCompressedJsonFileFallback(path.join(schemasPath, filename))
|
||||
)
|
||||
}
|
||||
|
||||
return webhooksCache.get(openApiVersion)
|
||||
}
|
||||
|
||||
function getOpenApiVersion(version) {
|
||||
if (!(version in allVersions)) {
|
||||
throw new Error(`Unrecognized version '${version}'. Not found in ${Object.keys(allVersions)}`)
|
||||
}
|
||||
return allVersions[version].openApiVersionName
|
||||
}
|
||||
|
||||
// TODO: docs-eng#1937: delete this function
|
||||
export default function getWebhookPayloads() {
|
||||
// Compile contents of individual .payload.json files into a single
|
||||
// object, with versions as top-level keys.
|
||||
@@ -40,6 +123,7 @@ export default function getWebhookPayloads() {
|
||||
return payloads
|
||||
}
|
||||
|
||||
// TODO: docs-eng#1937: delete this function
|
||||
function formatAsJsonCodeBlock(payloadObj) {
|
||||
// Note the use of `data-highlight="json"`. This is important because
|
||||
// done like this, it tells the rehype processor to NOT bother syntax
|
||||
|
||||
@@ -3,10 +3,12 @@ import { createProxyMiddleware } from 'http-proxy-middleware'
|
||||
|
||||
import events from './events.js'
|
||||
import search from './search.js'
|
||||
import webhooks from './webhooks.js'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
router.use('/events', events)
|
||||
router.use('/webhooks', webhooks)
|
||||
|
||||
// The purpose of this is for convenience to everyone who runs this code
|
||||
// base locally but don't have an Elasticsearch server locally.
|
||||
|
||||
42
middleware/api/webhooks.js
Normal file
42
middleware/api/webhooks.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import express from 'express'
|
||||
import { getWebhook } from '../../lib/webhooks/index.js'
|
||||
import { allVersions } from '../../lib/all-versions.js'
|
||||
import { defaultCacheControl } from '../cache-control.js'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
// Returns a webhook for the given category and version
|
||||
//
|
||||
// Example request:
|
||||
//
|
||||
// /api/webhooks/v1?category=check_run&version=free-pro-team%40latest
|
||||
router.get('/v1', async function webhooks(req, res, next) {
|
||||
if (!req.query.category) {
|
||||
return res.status(400).json({ error: "Missing 'category' in query string" })
|
||||
}
|
||||
if (!req.query.version) {
|
||||
return res.status(400).json({ error: "Missing 'version' in query string" })
|
||||
}
|
||||
|
||||
const webhookVersion = Object.values(allVersions).find(
|
||||
(version) => version.version === req.query.version
|
||||
)?.version
|
||||
const notFoundError = 'No webhook found for given category and version'
|
||||
|
||||
if (!webhookVersion) {
|
||||
return res.status(404).json({ error: notFoundError })
|
||||
}
|
||||
|
||||
const webhook = await getWebhook(webhookVersion, req.query.category)
|
||||
|
||||
if (webhook) {
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
defaultCacheControl(res)
|
||||
}
|
||||
return res.status(200).send(webhook)
|
||||
} else {
|
||||
res.status(404).json({ error: notFoundError })
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -5,6 +5,7 @@ import { allVersions } from '../../lib/all-versions.js'
|
||||
|
||||
let webhookPayloads = null
|
||||
|
||||
// TODO: docs-eng#1937: webhooks-delete-1937: delete this file
|
||||
export default function webhooksContext(req, res, next) {
|
||||
const currentVersionObj = allVersions[req.context.currentVersion]
|
||||
// ignore requests to non-webhook reference paths
|
||||
|
||||
@@ -38,6 +38,7 @@ import triggerError from './trigger-error.js'
|
||||
import ghesReleaseNotes from './contextualizers/ghes-release-notes.js'
|
||||
import ghaeReleaseNotes from './contextualizers/ghae-release-notes.js'
|
||||
import whatsNewChangelog from './contextualizers/whats-new-changelog.js'
|
||||
// TODO: docs-eng#1937: delete this line
|
||||
import webhooks from './contextualizers/webhooks.js'
|
||||
import layout from './contextualizers/layout.js'
|
||||
import currentProductTree from './contextualizers/current-product-tree.js'
|
||||
@@ -261,6 +262,7 @@ export default function (app) {
|
||||
// *** Preparation for render-page: contextualizers ***
|
||||
app.use(asyncMiddleware(instrument(ghesReleaseNotes, './contextualizers/ghes-release-notes')))
|
||||
app.use(asyncMiddleware(instrument(ghaeReleaseNotes, './contextualizers/ghae-release-notes')))
|
||||
// TODO: docs-eng#1937: delete this line
|
||||
app.use(instrument(webhooks, './contextualizers/webhooks'))
|
||||
app.use(asyncMiddleware(instrument(whatsNewChangelog, './contextualizers/whats-new-changelog')))
|
||||
app.use(instrument(layout, './contextualizers/layout'))
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { GetServerSideProps } from 'next'
|
||||
import { getInitialPageWebhooks } from 'lib/webhooks'
|
||||
import { getMainContext, MainContext, MainContextT } from 'components/context/MainContext'
|
||||
import {
|
||||
getAutomatedPageContextFromRequest,
|
||||
AutomatedPageContext,
|
||||
AutomatedPageContextT,
|
||||
} from 'components/context/AutomatedPageContext'
|
||||
import { WebhookAction } from 'components/webhooks/types'
|
||||
import { Webhook } from 'components/webhooks/Webhook'
|
||||
import { getAutomatedPageMiniTocItems } from 'lib/get-mini-toc-items'
|
||||
import { AutomatedPage } from 'components/article/AutomatedPage'
|
||||
|
||||
type Props = {
|
||||
mainContext: MainContextT
|
||||
automatedPageContext: AutomatedPageContextT
|
||||
webhooks: WebhookAction[]
|
||||
}
|
||||
|
||||
export default function WebhooksEventsAndPayloads({
|
||||
mainContext,
|
||||
automatedPageContext,
|
||||
webhooks,
|
||||
}: Props) {
|
||||
const content = webhooks.map((webhook: WebhookAction, index) => {
|
||||
return (
|
||||
<div key={`${webhook.data.requestPath}-${index}`}>
|
||||
<Webhook webhook={webhook} />
|
||||
</div>
|
||||
)
|
||||
})
|
||||
return (
|
||||
<MainContext.Provider value={mainContext}>
|
||||
<AutomatedPageContext.Provider value={automatedPageContext}>
|
||||
<AutomatedPage>{content}</AutomatedPage>
|
||||
</AutomatedPageContext.Provider>
|
||||
</MainContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps<Props> = async (context) => {
|
||||
const req = context.req as object
|
||||
const res = context.res as object
|
||||
const currentVersion = context.query.versionId as string
|
||||
const mainContext = await getMainContext(req, res)
|
||||
const { miniTocItems } = getAutomatedPageContextFromRequest(req)
|
||||
|
||||
// Get data for initial webhooks page (i.e. only 1 action type per webhook and
|
||||
// no nested parameters)
|
||||
const webhooks = (await getInitialPageWebhooks(currentVersion)) as WebhookAction[]
|
||||
|
||||
// Build the minitocs for the webhooks page which is based on the webhook
|
||||
// categories in addition to the Markdown in the webhook-events-and-payloads.md
|
||||
// content file
|
||||
const webhooksMiniTocs = await getAutomatedPageMiniTocItems(
|
||||
webhooks.map((webhook) => webhook.data.category),
|
||||
context
|
||||
)
|
||||
webhooksMiniTocs && miniTocItems.push(...webhooksMiniTocs)
|
||||
|
||||
return {
|
||||
props: {
|
||||
webhooks,
|
||||
mainContext,
|
||||
automatedPageContext: getAutomatedPageContextFromRequest(req),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import { difference } from 'lodash-es'
|
||||
import { getJSON } from '../helpers/e2etest.js'
|
||||
import { get, getJSON } from '../helpers/e2etest.js'
|
||||
import { SURROGATE_ENUMS } from '../../middleware/set-fastly-surrogate-key.js'
|
||||
import { latest } from '../../lib/enterprise-server-releases.js'
|
||||
import { allVersions } from '../../lib/all-versions.js'
|
||||
import getWebhookPayloads from '../../lib/webhooks'
|
||||
import { jest } from '@jest/globals'
|
||||
import { describe, expect, jest } from '@jest/globals'
|
||||
|
||||
const allVersionValues = Object.values(allVersions)
|
||||
|
||||
@@ -22,6 +23,66 @@ const ghaePayloadVersion = allVersionValues.find(
|
||||
(version) => version.plan === 'github-ae'
|
||||
).miscVersionName
|
||||
|
||||
describe('webhooks middleware', () => {
|
||||
test('basic get webhook', async () => {
|
||||
const sp = new URLSearchParams()
|
||||
// Based on live data which isn't ideal but it should rarely change at least.
|
||||
// Just check that we find the webhook and that the result has the `category`
|
||||
// field which all webhook types should have.
|
||||
sp.set('category', 'branch_protection_rule')
|
||||
sp.set('version', 'free-pro-team@latest')
|
||||
const res = await get('/api/webhooks/v1?' + sp)
|
||||
expect(res.statusCode).toBe(200)
|
||||
const results = JSON.parse(res.text)
|
||||
const actionTypes = Object.keys(results)
|
||||
expect(actionTypes.length).toBeGreaterThan(2)
|
||||
expect(Object.keys(results[actionTypes[0]]).includes('category')).toBeTruthy()
|
||||
|
||||
// Check that it can be cached at the CDN
|
||||
expect(res.headers['set-cookie']).toBeUndefined()
|
||||
expect(res.headers['cache-control']).toContain('public')
|
||||
expect(res.headers['cache-control']).toMatch(/max-age=[1-9]/)
|
||||
expect(res.headers['surrogate-control']).toContain('public')
|
||||
expect(res.headers['surrogate-control']).toMatch(/max-age=[1-9]/)
|
||||
expect(res.headers['surrogate-key']).toBe(SURROGATE_ENUMS.DEFAULT)
|
||||
})
|
||||
|
||||
test('get non-fpt version webhook', async () => {
|
||||
const sp = new URLSearchParams()
|
||||
sp.set('category', 'branch_protection_rule')
|
||||
sp.set('version', 'enterprise-cloud@latest')
|
||||
const res = await get('/api/webhooks/v1?' + sp)
|
||||
expect(res.statusCode).toBe(200)
|
||||
const results = JSON.parse(res.text)
|
||||
const actionTypes = Object.keys(results)
|
||||
expect(actionTypes.length).toBeGreaterThan(2)
|
||||
expect(Object.keys(results[actionTypes[0]]).includes('category')).toBeTruthy()
|
||||
|
||||
expect(res.statusCode).toBe(200)
|
||||
})
|
||||
|
||||
test('unknown webhook category', async () => {
|
||||
const sp = new URLSearchParams()
|
||||
sp.set('category', 'no-such-category')
|
||||
sp.set('version', 'free-pro-team@latest')
|
||||
const res = await get('/api/webhooks/v1?' + sp)
|
||||
|
||||
expect(res.statusCode).toBe(404)
|
||||
expect(JSON.parse(res.text).error).toBeTruthy()
|
||||
})
|
||||
|
||||
test('unknown version', async () => {
|
||||
const sp = new URLSearchParams()
|
||||
sp.set('category', 'branch_protection_rule')
|
||||
sp.set('version', 'no-such-version')
|
||||
const res = await get('/api/webhooks/v1?' + sp)
|
||||
|
||||
expect(res.statusCode).toBe(404)
|
||||
expect(JSON.parse(res.text).error).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
// TODO: docs-eng#1937: delete this test suite
|
||||
describe('webhook payloads', () => {
|
||||
jest.setTimeout(3 * 60 * 1000)
|
||||
|
||||
|
||||
@@ -1,11 +1,65 @@
|
||||
import { jest } from '@jest/globals'
|
||||
import { getDOM } from '../helpers/e2etest.js'
|
||||
import { allVersions } from '../../lib/all-versions.js'
|
||||
import { getWebhooks } from '../../lib/webhooks/index.js'
|
||||
|
||||
describe('webhooks events and payloads', () => {
|
||||
jest.setTimeout(300 * 1000)
|
||||
|
||||
describe('rendering', () => {
|
||||
test('loads webhook schema data for all versions', async () => {
|
||||
for (const version in allVersions) {
|
||||
const webhooks = await getWebhooks(version)
|
||||
const webhookNames = Object.keys(webhooks)
|
||||
const $ = await getDOM(
|
||||
`/en/${version}/developers/webhooks-and-events/webhooks/webhook-events-and-payloads`
|
||||
)
|
||||
const domH2Ids = $('h2')
|
||||
.map((i, h2) => $(h2).attr('id'))
|
||||
.get()
|
||||
|
||||
webhookNames.forEach((webhookName) => {
|
||||
expect(domH2Ids.includes(webhookName))
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
test('Non-GHES versions do not load GHES only webhook', async () => {
|
||||
// available since 3.4, only in GHES (technically also GHAE which is based
|
||||
// off of GHES)
|
||||
const ghesOnlyWebhook = 'cache_sync'
|
||||
|
||||
for (const version in allVersions) {
|
||||
if (!version.includes('enterprise-server') && !version.includes('github-ae')) {
|
||||
const $ = await getDOM(
|
||||
`/en/${version}/developers/webhooks-and-events/webhooks/webhook-events-and-payloads`
|
||||
)
|
||||
const domH2Ids = $('h2')
|
||||
.map((i, h2) => $(h2).attr('id'))
|
||||
.get()
|
||||
|
||||
expect(domH2Ids.length).toBeGreaterThan(0)
|
||||
expect(domH2Ids.includes(ghesOnlyWebhook)).toBe(false)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test('Webhooks events and payloads page has DOM markers needed for extracting search content', async () => {
|
||||
const $ = await getDOM(
|
||||
'/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads'
|
||||
)
|
||||
const rootSelector = '[data-search=article-body]'
|
||||
const $root = $(rootSelector)
|
||||
expect($root.length).toBe(1)
|
||||
|
||||
// on the webhooks page the lead is separate from the article body (unlike
|
||||
// the REST pages for example)
|
||||
const leadSelector = '[data-search=lead] p'
|
||||
const $lead = $(leadSelector)
|
||||
expect($lead.length).toBe(1)
|
||||
})
|
||||
|
||||
// All webhook types don't yet have examples in the schema.
|
||||
describe.skip('rendering', () => {
|
||||
test('every webhook event has at least one payload example', async () => {
|
||||
const versions = Object.values(allVersions).map((value) => value.version)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user