forked from mirrors/misskey
feat: oidc
This commit is contained in:
parent
2c814ecd83
commit
eb83f40ef2
25 changed files with 777 additions and 154 deletions
|
|
@ -293,7 +293,7 @@ fulltextSearch:
|
|||
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
|
||||
# ID SETTINGS AFTER THAT!
|
||||
|
||||
id: 'aidx'
|
||||
id: "aidx"
|
||||
|
||||
# ┌────────────────┐
|
||||
#───┘ Error tracking └──────────────────────────────────────────
|
||||
|
|
@ -402,3 +402,33 @@ proxyBypassHosts:
|
|||
# # Disable query truncation. If set to true, the full text of the query will be output to the log.
|
||||
# # default: false
|
||||
# disableQueryTruncation: false
|
||||
|
||||
# ┌─────────────────────────────────────────┐
|
||||
#────┘ External login via OpenID Connect (SSO) └───────────────────────────────
|
||||
# Allow signing in to Misskey through an external OpenID Connect Identity
|
||||
# Provider (e.g. Authentik, Keycloak, Auth0). Misskey acts as the OIDC
|
||||
# Relying Party (client).
|
||||
#
|
||||
# Register Misskey as an application/client on your IdP and set its
|
||||
# redirect URI to: {url}/sso/oidc/callback
|
||||
# (where {url} is the `url:` value at the top of this file)
|
||||
#
|
||||
# NOTE: clientSecret is a credential. Do NOT commit a filled-in value.
|
||||
#sso:
|
||||
# oidc:
|
||||
# # Whether OIDC login is enabled. default: true (when this block exists)
|
||||
# enabled: true
|
||||
# # Display name shown on the login button. default: null ("Log in with SSO")
|
||||
# name: Authentik
|
||||
# # The issuer URL. Its {issuer}/.well-known/openid-configuration must resolve.
|
||||
# issuer: https://authentik.example.com/application/o/misskey/
|
||||
# clientId: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
# clientSecret: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
# # Requested scopes. default: ['openid', 'profile', 'email']
|
||||
# scopes: ['openid', 'profile', 'email']
|
||||
# # Automatically create a Misskey account on first login if none is linked.
|
||||
# # When false, only pre-linked accounts may sign in. default: false
|
||||
# autoProvision: false
|
||||
# # The id_token claim used as the username on auto-provision.
|
||||
# # default: 'preferred_username'
|
||||
# usernameClaim: preferred_username
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
## Unreleased
|
||||
|
||||
### General
|
||||
-
|
||||
- Feat: 外部 OpenID Connect プロバイダー (Authentik 等) を用いた SSO ログインに対応 (設定ファイルの `sso.oidc`)
|
||||
|
||||
### Client
|
||||
-
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
_lang_: "English"
|
||||
headlineMisskey: "A network connected by notes"
|
||||
introMisskey: "Welcome! Misskey is an open source, decentralized microblogging service.\nCreate \"notes\" to share your thoughts with everyone around you. 📡\nWith \"reactions\", you can also quickly express your feelings about everyone's notes. 👍\nLet's explore a new world! 🚀"
|
||||
poweredByMisskeyDescription: "{name} is one of the services powered by the open source platform <b>Misskey</b> (referred to as a \"Misskey instance\")."
|
||||
poweredByMisskeyDescription: '{name} is one of the services powered by the open source platform <b>Misskey</b> (referred to as a "Misskey instance").'
|
||||
monthAndDay: "{month}/{day}"
|
||||
search: "Search"
|
||||
reset: "Reset"
|
||||
|
|
@ -81,7 +81,7 @@ import: "Import"
|
|||
export: "Export"
|
||||
files: "Files"
|
||||
download: "Download"
|
||||
driveFileDeleteConfirm: "Are you sure you want to delete \"{name}\"? All notes with this file attached will also be deleted."
|
||||
driveFileDeleteConfirm: 'Are you sure you want to delete "{name}"? All notes with this file attached will also be deleted.'
|
||||
unfollowConfirm: "Are you sure you want to unfollow {name}?"
|
||||
cancelFollowRequestConfirm: "Are you sure that you want to cancel your follow request to {name}?"
|
||||
rejectFollowRequestConfirm: "Are you sure that you want to reject the follow request from {name}?"
|
||||
|
|
@ -138,7 +138,7 @@ pinnedEmojisSettingDescription: "Set the emojis to be pinned and displayed when
|
|||
emojiPickerDisplay: "Emoji picker display"
|
||||
overwriteFromPinnedEmojisForReaction: "Override from reaction settings"
|
||||
overwriteFromPinnedEmojis: "Override from general settings"
|
||||
reactionSettingDescription2: "Drag to reorder, click to delete, press \"+\" to add."
|
||||
reactionSettingDescription2: 'Drag to reorder, click to delete, press "+" to add.'
|
||||
rememberNoteVisibility: "Remember note visibility settings"
|
||||
attachCancel: "Remove attachment"
|
||||
deleteFile: "Delete file"
|
||||
|
|
@ -287,8 +287,8 @@ announcements: "Announcements"
|
|||
imageUrl: "Image URL"
|
||||
remove: "Delete"
|
||||
removed: "Successfully deleted"
|
||||
removeAreYouSure: "Are you sure that you want to remove \"{x}\"?"
|
||||
deleteAreYouSure: "Are you sure that you want to delete \"{x}\"?"
|
||||
removeAreYouSure: 'Are you sure that you want to remove "{x}"?'
|
||||
deleteAreYouSure: 'Are you sure that you want to delete "{x}"?'
|
||||
resetAreYouSure: "Really reset?"
|
||||
areYouSure: "Are you sure?"
|
||||
saved: "Saved"
|
||||
|
|
@ -331,7 +331,7 @@ dark: "Dark"
|
|||
lightThemes: "Light themes"
|
||||
darkThemes: "Dark themes"
|
||||
syncDeviceDarkMode: "Sync Dark Mode with your device settings"
|
||||
switchDarkModeManuallyWhenSyncEnabledConfirm: "\"{x}\" is turned on. Would you like to turn off synchronization and switch modes manually?"
|
||||
switchDarkModeManuallyWhenSyncEnabledConfirm: '"{x}" is turned on. Would you like to turn off synchronization and switch modes manually?'
|
||||
drive: "Drive"
|
||||
fileName: "Filename"
|
||||
selectFile: "Select a file"
|
||||
|
|
@ -399,7 +399,7 @@ bannerUrl: "Banner image URL"
|
|||
backgroundImageUrl: "Background image URL"
|
||||
basicInfo: "Basic info"
|
||||
pinnedUsers: "Pinned users"
|
||||
pinnedUsersDescription: "List usernames separated by line breaks to be pinned in the \"Explore\" tab."
|
||||
pinnedUsersDescription: 'List usernames separated by line breaks to be pinned in the "Explore" tab.'
|
||||
pinnedPages: "Pinned Pages"
|
||||
pinnedPagesDescription: "Enter the paths of the Pages you want to pin to the top page of this instance, separated by line breaks."
|
||||
pinnedClipId: "ID of the clip to pin"
|
||||
|
|
@ -475,7 +475,7 @@ unregister: "Unregister"
|
|||
passwordLessLogin: "Password-less login"
|
||||
passwordLessLoginDescription: "Allows password-less login using a security- or passkey only"
|
||||
resetPassword: "Reset password"
|
||||
newPasswordIs: "The new password is \"{password}\""
|
||||
newPasswordIs: 'The new password is "{password}"'
|
||||
reduceUiAnimation: "Reduce UI animations"
|
||||
share: "Share"
|
||||
notFound: "Not found"
|
||||
|
|
@ -576,7 +576,7 @@ objectStorageUseSSL: "Use SSL"
|
|||
objectStorageUseSSLDesc: "Turn this off if you are not going to use HTTPS for API connections"
|
||||
objectStorageUseProxy: "Connect over Proxy"
|
||||
objectStorageUseProxyDesc: "Turn this off if you are not going to use a Proxy for API connections"
|
||||
objectStorageSetPublicRead: "Set \"public-read\" on upload"
|
||||
objectStorageSetPublicRead: 'Set "public-read" on upload'
|
||||
s3ForcePathStyleDesc: "If s3ForcePathStyle is enabled, the bucket name has to included in the path of the URL as opposed to the hostname of the URL. You may need to enable this setting when using services such as a self-hosted Minio instance."
|
||||
serverLogs: "Server logs"
|
||||
deleteAll: "Delete all"
|
||||
|
|
@ -703,7 +703,7 @@ regexpError: "Regular Expression error"
|
|||
regexpErrorDescription: "An error occurred in the regular expression on line {line} of your {tab} word mutes:"
|
||||
instanceMute: "Instance Mutes"
|
||||
userSaysSomething: "{name} said something"
|
||||
userSaysSomethingAbout: "{name} said something about \"{word}\""
|
||||
userSaysSomethingAbout: '{name} said something about "{word}"'
|
||||
makeActive: "Activate"
|
||||
display: "Display"
|
||||
copy: "Copy"
|
||||
|
|
@ -752,7 +752,7 @@ createNew: "Create new"
|
|||
optional: "Optional"
|
||||
createNewClip: "Create new clip"
|
||||
unclip: "Unclip"
|
||||
confirmToUnclipAlreadyClippedNote: "This note is already part of the \"{name}\" clip. Do you want to remove it from this clip instead?"
|
||||
confirmToUnclipAlreadyClippedNote: 'This note is already part of the "{name}" clip. Do you want to remove it from this clip instead?'
|
||||
removeFromAntenna: "Remove from this antenna"
|
||||
removeNoteFromAntennaConfirm: "Are you sure you want to remove this note from {name}?"
|
||||
public: "Public"
|
||||
|
|
@ -777,7 +777,7 @@ driveFilesCount: "Number of Drive files"
|
|||
driveUsage: "Drive space usage"
|
||||
noCrawle: "Reject crawler indexing"
|
||||
noCrawleDescription: "Ask search engines to not index your profile page, notes, Pages, etc."
|
||||
lockedAccountInfo: "Unless you set your note visiblity to \"Followers only\", your notes will be visible to anyone, even if you require followers to be manually approved."
|
||||
lockedAccountInfo: 'Unless you set your note visiblity to "Followers only", your notes will be visible to anyone, even if you require followers to be manually approved.'
|
||||
alwaysMarkSensitive: "Mark as sensitive by default"
|
||||
loadRawImages: "Load original images instead of showing thumbnails"
|
||||
disableShowingAnimatedImages: "Don't play animated images"
|
||||
|
|
@ -796,8 +796,8 @@ experimentalFeatures: "Experimental features"
|
|||
experimental: "Experimental"
|
||||
thisIsExperimentalFeature: "This is an experimental feature. Its functionality is subject to change, and it may not operate as intended."
|
||||
developer: "Developer"
|
||||
makeExplorable: "Make account visible in \"Explore\""
|
||||
makeExplorableDescription: "If you turn this off, your account will not show up in the \"Explore\" section."
|
||||
makeExplorable: 'Make account visible in "Explore"'
|
||||
makeExplorableDescription: 'If you turn this off, your account will not show up in the "Explore" section.'
|
||||
duplicate: "Duplicate"
|
||||
left: "Left"
|
||||
center: "Center"
|
||||
|
|
@ -852,7 +852,7 @@ unlikeConfirm: "Really remove your like?"
|
|||
fullView: "Full view"
|
||||
quitFullView: "Exit full view"
|
||||
addDescription: "Add description"
|
||||
userPagePinTip: "You can display notes here by selecting \"Pin to profile\" from the menu of individual notes."
|
||||
userPagePinTip: 'You can display notes here by selecting "Pin to profile" from the menu of individual notes.'
|
||||
notSpecifiedMentionWarning: "This note contains mentions of users not included as recipients"
|
||||
info: "About"
|
||||
userInfo: "User information"
|
||||
|
|
@ -942,7 +942,7 @@ continueThread: "View thread continuation"
|
|||
deleteAccountConfirm: "This will irreversibly delete your account. Proceed?"
|
||||
incorrectPassword: "Incorrect password."
|
||||
incorrectTotp: "The one-time password is incorrect or has expired."
|
||||
voteConfirm: "Confirm your vote for \"{choice}\"?"
|
||||
voteConfirm: 'Confirm your vote for "{choice}"?'
|
||||
hide: "Hide"
|
||||
useDrawerReactionPickerForMobile: "Display reaction picker as drawer on mobile"
|
||||
welcomeBackWithName: "Welcome back, {name}"
|
||||
|
|
@ -1098,7 +1098,7 @@ nonSensitiveOnlyForLocalLikeOnlyForRemote: "Non-sensitive only (Only likes from
|
|||
rolesAssignedToMe: "Roles assigned to me"
|
||||
resetPasswordConfirm: "Really reset your password?"
|
||||
sensitiveWords: "Sensitive words"
|
||||
sensitiveWordsDescription: "The visibility of all notes containing any of the configured words will be set to \"Home\" automatically. You can list multiple by separating them via line breaks."
|
||||
sensitiveWordsDescription: 'The visibility of all notes containing any of the configured words will be set to "Home" automatically. You can list multiple by separating them via line breaks.'
|
||||
sensitiveWordsDescription2: "Using spaces will create AND expressions and surrounding keywords with slashes will turn them into a regular expression."
|
||||
prohibitedWords: "Prohibited words"
|
||||
prohibitedWordsDescription: "Enables an error when attempting to post a note containing the set word(s). Multiple words can be set, separated by a new line."
|
||||
|
|
@ -1117,7 +1117,7 @@ retryAllQueuesConfirmText: "This will temporarily increase the server load."
|
|||
enableChartsForRemoteUser: "Generate remote user data charts"
|
||||
enableChartsForFederatedInstances: "Generate remote instance data charts"
|
||||
enableStatsForFederatedInstances: "Receive remote server stats"
|
||||
showClipButtonInNoteFooter: "Add \"Clip\" to note action menu"
|
||||
showClipButtonInNoteFooter: 'Add "Clip" to note action menu'
|
||||
reactionsDisplaySize: "Reaction display size"
|
||||
limitWidthOfReaction: "Limit the maximum width of reactions and display them in reduced size."
|
||||
noteIdOrUrl: "Note ID or URL"
|
||||
|
|
@ -1161,7 +1161,7 @@ displayOfNote: "Note display"
|
|||
initialAccountSetting: "Profile setup"
|
||||
youFollowing: "Followed"
|
||||
preventAiLearning: "Reject usage in Machine Learning (Generative AI)"
|
||||
preventAiLearningDescription: "Requests crawlers to not use posted text or image material etc. in machine learning (Predictive / Generative AI) data sets. This is achieved by adding a \"noai\" HTML-Response flag to the respective content. A complete prevention can however not be achieved through this flag, as it may simply be ignored."
|
||||
preventAiLearningDescription: 'Requests crawlers to not use posted text or image material etc. in machine learning (Predictive / Generative AI) data sets. This is achieved by adding a "noai" HTML-Response flag to the respective content. A complete prevention can however not be achieved through this flag, as it may simply be ignored.'
|
||||
options: "Options"
|
||||
specifyUser: "Specific user"
|
||||
lookupConfirm: "Do you want to look up?"
|
||||
|
|
@ -1202,7 +1202,7 @@ used: "Used"
|
|||
expired: "Expired"
|
||||
doYouAgree: "Agree?"
|
||||
beSureToReadThisAsItIsImportant: "Please read this important information."
|
||||
iHaveReadXCarefullyAndAgree: "I have read the text \"{x}\" and agree."
|
||||
iHaveReadXCarefullyAndAgree: 'I have read the text "{x}" and agree.'
|
||||
dialog: "Dialog"
|
||||
icon: "Icon"
|
||||
forYou: "For you"
|
||||
|
|
@ -1261,7 +1261,7 @@ refreshing: "Refreshing..."
|
|||
pullDownToRefresh: "Pull down to refresh"
|
||||
useGroupedNotifications: "Display grouped notifications"
|
||||
emailVerificationFailedError: "A problem occurred while verifying your email address. The link may have expired."
|
||||
cwNotationRequired: "If \"Hide content\" is enabled, a description must be provided."
|
||||
cwNotationRequired: 'If "Hide content" is enabled, a description must be provided.'
|
||||
doReaction: "Add reaction"
|
||||
code: "Code"
|
||||
reloadRequiredToApplySettings: "Reloading is required to apply the settings."
|
||||
|
|
@ -1313,6 +1313,7 @@ modified: "Modified"
|
|||
discard: "Discard"
|
||||
thereAreNChanges: "There are {n} change(s)"
|
||||
signinWithPasskey: "Sign in with Passkey"
|
||||
signinWithSso: "Sign in with SSO"
|
||||
unknownWebAuthnKey: "Unknown Passkey"
|
||||
passkeyVerificationFailed: "Passkey verification has failed."
|
||||
passkeyVerificationSucceededButPasswordlessLoginDisabled: "Passkey verification has succeeded but password-less login is disabled."
|
||||
|
|
@ -1333,7 +1334,7 @@ federationDisabled: "Federation is disabled on this server. You cannot interact
|
|||
draft: "Drafts"
|
||||
draftsAndScheduledNotes: "Drafts and scheduled notes"
|
||||
confirmOnReact: "Confirm when reacting"
|
||||
reactAreYouSure: "Would you like to add a \"{emoji}\" reaction?"
|
||||
reactAreYouSure: 'Would you like to add a "{emoji}" reaction?'
|
||||
markAsSensitiveConfirm: "Do you want to set this media as sensitive?"
|
||||
unmarkAsSensitiveConfirm: "Do you want to remove the sensitive designation for this media?"
|
||||
preferences: "Preferences"
|
||||
|
|
@ -1385,7 +1386,7 @@ unmuteX: "Unmute {x}"
|
|||
abort: "Abort"
|
||||
tip: "Tips & Tricks"
|
||||
redisplayAllTips: "Show all “Tips & Tricks” again"
|
||||
hideAllTips: "Hide all \"Tips & Tricks\""
|
||||
hideAllTips: 'Hide all "Tips & Tricks"'
|
||||
defaultImageCompressionLevel: "Default image compression level"
|
||||
defaultImageCompressionLevel_description: "Lower level preserves image quality but increases file size.<br>Higher level reduce file size, but reduce image quality."
|
||||
defaultCompressionLevel: "Default compression level"
|
||||
|
|
@ -1573,7 +1574,7 @@ _settings:
|
|||
_preferencesProfile:
|
||||
profileName: "Profile name"
|
||||
profileNameDescription: "Set a name that identifies this device."
|
||||
profileNameDescription2: "Example: \"Main PC\", \"Smartphone\""
|
||||
profileNameDescription2: 'Example: "Main PC", "Smartphone"'
|
||||
manageProfiles: "Manage Profiles"
|
||||
shareSameProfileBetweenDevicesIsNotRecommended: "We do not recommend sharing the same profile across multiple devices."
|
||||
useSyncBetweenDevicesOptionIfYouWantToSyncSetting: "If there are settings you wish to synchronize across multiple devices, enable the “Synchronize across multiple devices” option individually for each device."
|
||||
|
|
@ -1636,11 +1637,11 @@ _announcement:
|
|||
forExistingUsers: "Existing users only"
|
||||
forExistingUsersDescription: "This announcement will only be shown to users existing at the point of publishment if enabled. If disabled, those newly signing up after it has been posted will also see it."
|
||||
needConfirmationToRead: "Require separate read confirmation"
|
||||
needConfirmationToReadDescription: "A separate prompt to confirm marking this announcement as read will be displayed if enabled. This announcement will also be excluded from any \"Mark all as read\" functionality."
|
||||
needConfirmationToReadDescription: 'A separate prompt to confirm marking this announcement as read will be displayed if enabled. This announcement will also be excluded from any "Mark all as read" functionality.'
|
||||
end: "Archive announcement"
|
||||
tooManyActiveAnnouncementDescription: "Having too many active announcements may worsen the user experience. Please consider archiving announcements that have become obsolete."
|
||||
readConfirmTitle: "Mark as read?"
|
||||
readConfirmText: "This will mark the contents of \"{title}\" as read."
|
||||
readConfirmText: 'This will mark the contents of "{title}" as read.'
|
||||
shouldNotBeUsedToPresentPermanentInfo: "It's best to use announcements to publish fresh and time-bound information, not for information that will be relevant in the long term."
|
||||
dialogAnnouncementUxWarn: "Having two or more dialog-style notifications simultaneously can significantly impact the user experience, so please use them carefully."
|
||||
silence: "No notification"
|
||||
|
|
@ -1652,7 +1653,7 @@ _initialAccountSetting:
|
|||
profileSetting: "Profile settings"
|
||||
privacySetting: "Privacy settings"
|
||||
theseSettingsCanEditLater: "You can always change these settings later."
|
||||
youCanEditMoreSettingsInSettingsPageLater: "There are many more settings you can configure from the \"Settings\" page. Be sure to visit it later."
|
||||
youCanEditMoreSettingsInSettingsPageLater: 'There are many more settings you can configure from the "Settings" page. Be sure to visit it later.'
|
||||
followUsers: "Try following some users that interest you to build up your timeline."
|
||||
pushNotificationDescription: "Enabling push notifications will allow you to receive notifications from {name} directly on your device."
|
||||
initialAccountSettingCompleted: "Profile setup complete!"
|
||||
|
|
@ -1706,18 +1707,18 @@ _initialTutorial:
|
|||
localOnly: "Posting with this flag will not federate the note to other servers. Users on other servers will not be able to view these notes directly, regardless of the display settings above."
|
||||
_cw:
|
||||
title: "Content Warning"
|
||||
description: "Instead of the body, the content written in 'comments' field will be displayed. Pressing \"read more\" will reveal the body."
|
||||
description: 'Instead of the body, the content written in ''comments'' field will be displayed. Pressing "read more" will reveal the body.'
|
||||
_exampleNote:
|
||||
cw: "This will surely make you hungry!"
|
||||
note: "Just had a chocolate-glazed donut 🍩😋"
|
||||
useCases: "This is used when following the server guidelines, for necessary notes, or for self-restriction of spoiler or sensitive text."
|
||||
_howToMakeAttachmentsSensitive:
|
||||
title: "How to Mark Attachments as Sensitive?"
|
||||
description: "For attachments that are required by server guidelines or that should not be left intact, add a \"sensitive\" flag."
|
||||
description: 'For attachments that are required by server guidelines or that should not be left intact, add a "sensitive" flag.'
|
||||
tryThisFile: "Try marking the image attached in this form as sensitive!"
|
||||
_exampleNote:
|
||||
note: "Oops, messed up opening the natto lid..."
|
||||
method: "To mark an attachment as sensitive, click the file thumbnail, open the menu, and click \"Mark as Sensitive.\""
|
||||
method: 'To mark an attachment as sensitive, click the file thumbnail, open the menu, and click "Mark as Sensitive."'
|
||||
sensitiveSucceeded: "When attaching files, please set sensitivities in accordance with the server guidelines."
|
||||
doItToContinue: "Mark the attachment file as sensitive to proceed."
|
||||
_done:
|
||||
|
|
@ -1952,7 +1953,7 @@ _achievements:
|
|||
description: "Look at your list of achievements for at least 3 minutes"
|
||||
_iLoveMisskey:
|
||||
title: "I Love Misskey"
|
||||
description: "Post \"I ❤ #Misskey\""
|
||||
description: 'Post "I ❤ #Misskey"'
|
||||
flavor: "Misskey's development team greatly appreciates your support!"
|
||||
_foundTreasure:
|
||||
title: "Treasure Hunt"
|
||||
|
|
@ -1961,7 +1962,7 @@ _achievements:
|
|||
title: "Short break"
|
||||
description: "Keep Misskey opened for at least 30 minutes"
|
||||
_client60min:
|
||||
title: "No \"Miss\" in Misskey"
|
||||
title: 'No "Miss" in Misskey'
|
||||
description: "Keep Misskey opened for at least 60 minutes"
|
||||
_noteDeletedWithin1min:
|
||||
title: "Nevermind"
|
||||
|
|
@ -1985,7 +1986,7 @@ _achievements:
|
|||
description: "View your instance's charts"
|
||||
_outputHelloWorldOnScratchpad:
|
||||
title: "Hello, world!"
|
||||
description: "Output \"hello world\" in the Scratchpad"
|
||||
description: 'Output "hello world" in the Scratchpad'
|
||||
_open3windows:
|
||||
title: "Multi-Window"
|
||||
description: "Have at least 3 windows open at the same time"
|
||||
|
|
@ -2003,7 +2004,7 @@ _achievements:
|
|||
description: "Has a chance to be obtained with a probability of 0.005% every 10 seconds"
|
||||
_setNameToSyuilo:
|
||||
title: "God Complex"
|
||||
description: "Set your name to \"syuilo\""
|
||||
description: 'Set your name to "syuilo"'
|
||||
_passedSinceAccountCreated1:
|
||||
title: "One Year Anniversary"
|
||||
description: "One year has passed since your account was created"
|
||||
|
|
@ -2132,7 +2133,7 @@ _role:
|
|||
isBot: "Bot Users"
|
||||
isSuspended: "Suspended user"
|
||||
isLocked: "Private accounts"
|
||||
isExplorable: "Effective user of \"make an account discoverable\""
|
||||
isExplorable: 'Effective user of "make an account discoverable"'
|
||||
createdLessThan: "Less than X has passed since account creation"
|
||||
createdMoreThan: "More than X has passed since account creation"
|
||||
followersLessThanOrEq: "Has X or fewer followers"
|
||||
|
|
@ -2211,12 +2212,12 @@ _preferencesBackups:
|
|||
save: "Save changes"
|
||||
inputName: "Please enter a name for this backup"
|
||||
cannotSave: "Saving failed"
|
||||
nameAlreadyExists: "A backup called \"{name}\" already exists. Please enter a different name."
|
||||
applyConfirm: "Do you really want to apply the \"{name}\" backup to this device? Existing settings of this device will be overwritten."
|
||||
nameAlreadyExists: 'A backup called "{name}" already exists. Please enter a different name.'
|
||||
applyConfirm: 'Do you really want to apply the "{name}" backup to this device? Existing settings of this device will be overwritten.'
|
||||
saveConfirm: "Save backup as {name}?"
|
||||
deleteConfirm: "Delete the {name} backup?"
|
||||
renameConfirm: "Rename this backup from \"{old}\" to \"{new}\"?"
|
||||
noBackups: "No backups exist. You may backup your client settings on this server by using \"Create new backup\"."
|
||||
renameConfirm: 'Rename this backup from "{old}" to "{new}"?'
|
||||
noBackups: 'No backups exist. You may backup your client settings on this server by using "Create new backup".'
|
||||
createdAt: "Created at: {date} {time}"
|
||||
updatedAt: "Updated at: {date} {time}"
|
||||
cannotLoad: "Loading failed"
|
||||
|
|
@ -2502,7 +2503,7 @@ _permissions:
|
|||
"read:chat": "Browse Chat"
|
||||
_auth:
|
||||
shareAccessTitle: "Granting application permissions"
|
||||
shareAccess: "Would you like to authorize \"{name}\" to access this account?"
|
||||
shareAccess: 'Would you like to authorize "{name}" to access this account?'
|
||||
shareAccessAsk: "Are you sure you want to authorize this application to access your account?"
|
||||
permission: "{name} requests the following permissions"
|
||||
permissionAsk: "This application requests the following permissions"
|
||||
|
|
@ -2821,7 +2822,7 @@ _notification:
|
|||
exportOfXCompleted: "Export of {x} has been completed"
|
||||
login: "Someone logged in"
|
||||
createToken: "An access token has been created"
|
||||
createTokenDescription: "If you have no idea, delete the access token through \"{text}\"."
|
||||
createTokenDescription: 'If you have no idea, delete the access token through "{text}".'
|
||||
_types:
|
||||
all: "All"
|
||||
note: "New notes"
|
||||
|
|
@ -2868,9 +2869,9 @@ _deck:
|
|||
deleteProfile: "Delete profile"
|
||||
introduction: "Create the perfect interface for you by arranging columns freely!"
|
||||
introduction2: "Click on the + on the right of the screen to add new columns whenever you want."
|
||||
widgetsIntroduction: "Please select \"Edit widgets\" in the column menu and add a widget."
|
||||
widgetsIntroduction: 'Please select "Edit widgets" in the column menu and add a widget.'
|
||||
useSimpleUiForNonRootPages: "Use simple UI for navigated pages"
|
||||
usedAsMinWidthWhenFlexible: "Minimum width will be used for this when the \"Auto-adjust width\" option is enabled"
|
||||
usedAsMinWidthWhenFlexible: 'Minimum width will be used for this when the "Auto-adjust width" option is enabled'
|
||||
flexible: "Auto-adjust width"
|
||||
enableSyncBetweenDevicesForProfiles: "Enable profile information sync between devices"
|
||||
showHowToUse: ""
|
||||
|
|
@ -3184,8 +3185,8 @@ _customEmojisManager:
|
|||
_register:
|
||||
uploadSettingTitle: "Upload settings"
|
||||
uploadSettingDescription: "On this screen, you can configure the behavior when uploading Emojis."
|
||||
directoryToCategoryLabel: "Enter the directory name in the \"category\" field"
|
||||
directoryToCategoryCaption: "When you drag and drop a directory, enter the directory name in the \"category\" field."
|
||||
directoryToCategoryLabel: 'Enter the directory name in the "category" field'
|
||||
directoryToCategoryCaption: 'When you drag and drop a directory, enter the directory name in the "category" field.'
|
||||
confirmRegisterEmojisDescription: "Register the Emojis from the list as new custom Emojis. Are you sure to continue? (To avoid overload, only {count} Emoji(s) can be registered in a single operation)"
|
||||
confirmClearEmojisDescription: "Discard the edits and clear the Emojis from the list. Are you sure to continue?"
|
||||
confirmUploadEmojisDescription: "Upload the dragged and dropped {count} file(s) to the drive. Are you sure to continue?"
|
||||
|
|
@ -3205,7 +3206,7 @@ _embedCodeGen:
|
|||
codeGeneratedDescription: "Paste the generated code into your website to embed the content."
|
||||
_selfXssPrevention:
|
||||
warning: "WARNING"
|
||||
title: "\"Paste something on this screen\" is all a scam."
|
||||
title: '"Paste something on this screen" is all a scam.'
|
||||
description1: "If you paste something here, a malicious user could hijack your account or steal your personal information."
|
||||
description2: "If you do not understand exactly what you are trying to paste, %cstop working right now and close this window."
|
||||
description3: "For more information, please refer to this. {link}"
|
||||
|
|
@ -3290,7 +3291,7 @@ _serverSetupWizard:
|
|||
largeScaleServerAdvice: "Large servers may require advanced infrastructure knowledge, such as load balancing and database replication."
|
||||
doYouConnectToFediverse: "Do you want to connect to the Fediverse?"
|
||||
doYouConnectToFediverse_description1: "When connected to a network of distributed servers (Fediverse) content can be exchanged with other servers."
|
||||
doYouConnectToFediverse_description2: "Connecting with the Fediverse is also called \"federation\""
|
||||
doYouConnectToFediverse_description2: 'Connecting with the Fediverse is also called "federation"'
|
||||
youCanConfigureMoreFederationSettingsLater: "Advanced settings such as specifying federated servers can be configured later."
|
||||
remoteContentsCleaning: "Automatic cleanup of received contents"
|
||||
remoteContentsCleaning_description: "Federation may result in a continuous inflow of content. Enabling automatic cleanup will remove outdated and unreferenced content from the server to save storage."
|
||||
|
|
|
|||
|
|
@ -1313,6 +1313,7 @@ modified: "変更あり"
|
|||
discard: "破棄"
|
||||
thereAreNChanges: "{n}件の変更があります"
|
||||
signinWithPasskey: "パスキーでログイン"
|
||||
signinWithSso: "SSOでログイン"
|
||||
unknownWebAuthnKey: "登録されていないパスキーです。"
|
||||
passkeyVerificationFailed: "パスキーの検証に失敗しました。"
|
||||
passkeyVerificationSucceededButPasswordlessLoginDisabled: "パスキーの検証に成功しましたが、パスワードレスログインが無効になっています。"
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ driveFileDeleteConfirm: "‘{name}’ 파일을 삭제하시겠습니까? 이
|
|||
unfollowConfirm: "{name}님을 언팔로우하시겠습니까?"
|
||||
cancelFollowRequestConfirm: "{name}(으)로의 팔로우 신청을 취소하시겠습니까?"
|
||||
rejectFollowRequestConfirm: "{name}(으)로부터의 팔로우 신청을 거부하시겠습니까?"
|
||||
exportRequested: "내보내기를 요청하였습니다. 이 작업은 시간이 걸릴 수 있습니다. 내보내기가 완료되면 \"드라이브\"에 추가됩니다."
|
||||
exportRequested: '내보내기를 요청하였습니다. 이 작업은 시간이 걸릴 수 있습니다. 내보내기가 완료되면 "드라이브"에 추가됩니다.'
|
||||
importRequested: "가져오기를 요청하였습니다. 이 작업에는 시간이 걸릴 수 있습니다."
|
||||
lists: "리스트"
|
||||
noLists: "리스트가 없습니다"
|
||||
|
|
@ -287,8 +287,8 @@ announcements: "공지사항"
|
|||
imageUrl: "이미지 URL"
|
||||
remove: "삭제"
|
||||
removed: "삭제했습니다"
|
||||
removeAreYouSure: "\"{x}\" 을(를) 삭제하시겠습니까?"
|
||||
deleteAreYouSure: "\"{x}\" 을(를) 삭제하시겠습니까?"
|
||||
removeAreYouSure: '"{x}" 을(를) 삭제하시겠습니까?'
|
||||
deleteAreYouSure: '"{x}" 을(를) 삭제하시겠습니까?'
|
||||
resetAreYouSure: "초기화 하시겠습니까?"
|
||||
areYouSure: "계속 진행하시겠습니까?"
|
||||
saved: "저장했습니다"
|
||||
|
|
@ -399,7 +399,7 @@ bannerUrl: "배너 이미지 URL"
|
|||
backgroundImageUrl: "배경 이미지 URL"
|
||||
basicInfo: "기본 정보"
|
||||
pinnedUsers: "고정한 유저"
|
||||
pinnedUsersDescription: "\"발견하기\" 페이지 등에 고정하고 싶은 유저를 한 줄에 한 명씩 적습니다."
|
||||
pinnedUsersDescription: '"발견하기" 페이지 등에 고정하고 싶은 유저를 한 줄에 한 명씩 적습니다.'
|
||||
pinnedPages: "고정한 페이지"
|
||||
pinnedPagesDescription: "서버의 대문에 고정하고 싶은 페이지의 경로를 한 줄에 하나씩 적습니다."
|
||||
pinnedClipId: "고정할 클립의 ID"
|
||||
|
|
@ -475,7 +475,7 @@ unregister: "등록 해제"
|
|||
passwordLessLogin: "비밀번호 없이 로그인"
|
||||
passwordLessLoginDescription: "비밀번호 없이 보안 키 또는 패스키만 사용해서 로그인합니다."
|
||||
resetPassword: "비밀번호 재설정"
|
||||
newPasswordIs: "새로운 비밀번호는 \"{password}\" 입니다"
|
||||
newPasswordIs: '새로운 비밀번호는 "{password}" 입니다'
|
||||
reduceUiAnimation: "UI의 애니메이션을 줄이기"
|
||||
share: "공유"
|
||||
notFound: "찾을 수 없습니다"
|
||||
|
|
@ -703,7 +703,7 @@ regexpError: "정규 표현식 오류"
|
|||
regexpErrorDescription: "{tab}단어 뮤트 {line}행의 정규 표현식에 오류가 발생했습니다:"
|
||||
instanceMute: "서버 뮤트"
|
||||
userSaysSomething: "{name}님이 무언가를 말했습니다"
|
||||
userSaysSomethingAbout: "{name}님이 \"{word}\"를 언급했습니다."
|
||||
userSaysSomethingAbout: '{name}님이 "{word}"를 언급했습니다.'
|
||||
makeActive: "활성화"
|
||||
display: "보기"
|
||||
copy: "복사"
|
||||
|
|
@ -797,7 +797,7 @@ experimental: "실험실"
|
|||
thisIsExperimentalFeature: "이 기능은 실험적인 기능입니다. 사양이 변경되거나 정상적으로 동작하지 않을 가능성이 있습니다."
|
||||
developer: "개발자"
|
||||
makeExplorable: "계정을 쉽게 발견하도록 하기"
|
||||
makeExplorableDescription: "비활성화하면 \"발견하기\"에 나의 계정을 표시하지 않습니다."
|
||||
makeExplorableDescription: '비활성화하면 "발견하기"에 나의 계정을 표시하지 않습니다.'
|
||||
duplicate: "복제"
|
||||
left: "왼쪽"
|
||||
center: "가운데"
|
||||
|
|
@ -942,7 +942,7 @@ continueThread: "글타래 더 보기"
|
|||
deleteAccountConfirm: "계정이 삭제되고 되돌릴 수 없게 됩니다. 계속하시겠습니까? "
|
||||
incorrectPassword: "비밀번호가 올바르지 않습니다."
|
||||
incorrectTotp: "OTP 번호가 틀렸거나 유효기간이 만료되어 있을 수 있습니다."
|
||||
voteConfirm: "\"{choice}\"에 투표하시겠습니까?"
|
||||
voteConfirm: '"{choice}"에 투표하시겠습니까?'
|
||||
hide: "숨기기"
|
||||
useDrawerReactionPickerForMobile: "모바일에서 드로어 메뉴로 표시"
|
||||
welcomeBackWithName: "{name}님, 환영합니다."
|
||||
|
|
@ -1202,7 +1202,7 @@ used: "사용됨"
|
|||
expired: "만료됨"
|
||||
doYouAgree: "동의하십니까?"
|
||||
beSureToReadThisAsItIsImportant: "중요하므로 반드시 읽어주십시오."
|
||||
iHaveReadXCarefullyAndAgree: "\"{x}\"의 내용을 읽고 동의합니다."
|
||||
iHaveReadXCarefullyAndAgree: '"{x}"의 내용을 읽고 동의합니다.'
|
||||
dialog: "다이얼로그"
|
||||
icon: "아바타"
|
||||
forYou: "나에게"
|
||||
|
|
@ -1313,6 +1313,7 @@ modified: "변경 있음"
|
|||
discard: "파기"
|
||||
thereAreNChanges: "{n}건 변경이 있습니다."
|
||||
signinWithPasskey: "패스키로 로그인"
|
||||
signinWithSso: "SSO로 로그인"
|
||||
unknownWebAuthnKey: "등록되지 않은 패스키입니다."
|
||||
passkeyVerificationFailed: "패스키 검증을 실패했습니다."
|
||||
passkeyVerificationSucceededButPasswordlessLoginDisabled: "입력된 패스키는 정상적이나, 비밀번호 없이 로그인 하는 기능이 비활성화 되어있습니다."
|
||||
|
|
@ -1333,7 +1334,7 @@ federationDisabled: "이 서버는 연합을 하지 않고 있습니다. 리모
|
|||
draft: "초안"
|
||||
draftsAndScheduledNotes: "초안과 예약 게시물"
|
||||
confirmOnReact: "리액션할 때 확인"
|
||||
reactAreYouSure: "\" {emoji} \"로 리액션하시겠습니까?"
|
||||
reactAreYouSure: '" {emoji} "로 리액션하시겠습니까?'
|
||||
markAsSensitiveConfirm: "이 미디어를 민감한 미디어로 설정하시겠습니까?"
|
||||
unmarkAsSensitiveConfirm: "이 미디어의 민감한 미디어 지정을 해제하시겠습니까?"
|
||||
preferences: "환경설정"
|
||||
|
|
@ -1952,7 +1953,7 @@ _achievements:
|
|||
description: "도전 과제 목록을 3분 이상 쳐다봤다"
|
||||
_iLoveMisskey:
|
||||
title: "I Love Misskey"
|
||||
description: "\"I ❤ #Misskey\"를 게시했다"
|
||||
description: '"I ❤ #Misskey"를 게시했다'
|
||||
flavor: "Misskey를 이용해 주셔서 감사합니다! ― 개발 팀"
|
||||
_foundTreasure:
|
||||
title: "보물찾기"
|
||||
|
|
@ -1961,7 +1962,7 @@ _achievements:
|
|||
title: "잠시 쉬어요"
|
||||
description: "클라이언트를 시작하고 30분이 경과했다"
|
||||
_client60min:
|
||||
title: "No \"Miss\" in Misskey"
|
||||
title: 'No "Miss" in Misskey'
|
||||
description: "클라이언트를 시작하고 60분이 경과했다"
|
||||
_noteDeletedWithin1min:
|
||||
title: "있었는데요 없었습니다"
|
||||
|
|
@ -2211,12 +2212,12 @@ _preferencesBackups:
|
|||
save: "현재 설정으로 덮어쓰기"
|
||||
inputName: "백업 이름을 입력하세요"
|
||||
cannotSave: "저장하지 못했습니다"
|
||||
nameAlreadyExists: "\"{name}\" 백업이 이미 존재합니다. 다른 이름을 설정하여 주십시오."
|
||||
applyConfirm: "\"{name}\" 백업을 현재 기기에 적용하시겠습니까? 현재 설정은 덮어 씌워집니다."
|
||||
nameAlreadyExists: '"{name}" 백업이 이미 존재합니다. 다른 이름을 설정하여 주십시오.'
|
||||
applyConfirm: '"{name}" 백업을 현재 기기에 적용하시겠습니까? 현재 설정은 덮어 씌워집니다.'
|
||||
saveConfirm: "{name} 백업을 덮어쓰시겠습니까?"
|
||||
deleteConfirm: "{name} 백업을 삭제하시겠습니까?"
|
||||
renameConfirm: "‘{old}’ 백업을 ‘{new}’ 백업으로 바꾸시겠습니까?"
|
||||
noBackups: "저장된 백업이 없습니다. \"새 백업 만들기\"를 눌러 현재 클라이언트 설정을 서버에 백업할 수 있습니다."
|
||||
noBackups: '저장된 백업이 없습니다. "새 백업 만들기"를 눌러 현재 클라이언트 설정을 서버에 백업할 수 있습니다.'
|
||||
createdAt: "만든 날짜: {date} {time}"
|
||||
updatedAt: "고친 날짜: {date} {time}"
|
||||
cannotLoad: "가져오기에 실패했습니다"
|
||||
|
|
@ -2868,7 +2869,7 @@ _deck:
|
|||
deleteProfile: "프로파일 삭제"
|
||||
introduction: "칼럼을 조합해서 나만의 인터페이스를 구성해 보아요!"
|
||||
introduction2: "나중에라도 화면 우측의 + 버튼을 눌러 새 칼럼을 추가할 수 있습니다."
|
||||
widgetsIntroduction: "칼럼 메뉴의 \"위젯 편집\"에서 위젯을 추가해 주세요"
|
||||
widgetsIntroduction: '칼럼 메뉴의 "위젯 편집"에서 위젯을 추가해 주세요'
|
||||
useSimpleUiForNonRootPages: "루트 이외의 페이지로 접속한 경우 UI 간략화하기"
|
||||
usedAsMinWidthWhenFlexible: "'폭 자동 조정'이 활성화된 경우 최소 폭으로 사용됩니다"
|
||||
flexible: "폭 자동 조정"
|
||||
|
|
@ -3184,8 +3185,8 @@ _customEmojisManager:
|
|||
_register:
|
||||
uploadSettingTitle: "업로드 설정"
|
||||
uploadSettingDescription: "여기서 이모지를 업로드 할 때의 동작을 설정할 수 있습니다."
|
||||
directoryToCategoryLabel: "디렉토리 이름을 \"category\"로 입력하기"
|
||||
directoryToCategoryCaption: "디렉토리를 드래그 앤 드롭한 경우, 디렉토리 이름을 \"category\"로 입력합니다."
|
||||
directoryToCategoryLabel: '디렉토리 이름을 "category"로 입력하기'
|
||||
directoryToCategoryCaption: '디렉토리를 드래그 앤 드롭한 경우, 디렉토리 이름을 "category"로 입력합니다.'
|
||||
confirmRegisterEmojisDescription: "리스트에 표시되어진 이모지를 새로운 커스텀 이모지로 등록합니다. 실행할까요? (부하를 피하기 위해, 한 번에 등록할 수 있는 이모지는 {count}건까지 입니다.)"
|
||||
confirmClearEmojisDescription: "편집 내용을 지우고, 목록에 표시되어진 이모지를 지웁니다. 실행할까요?"
|
||||
confirmUploadEmojisDescription: "드래그 앤 드롭한 {count}개의 파일을 드라이브에 업로드 합니다. 실행할까요?"
|
||||
|
|
@ -3205,7 +3206,7 @@ _embedCodeGen:
|
|||
codeGeneratedDescription: "만들어진 코드를 웹 사이트에 붙여서 사용하세요."
|
||||
_selfXssPrevention:
|
||||
warning: "경고"
|
||||
title: "“이 화면에 뭔가를 붙여넣어라\"는 것은 모두 사기입니다."
|
||||
title: '“이 화면에 뭔가를 붙여넣어라"는 것은 모두 사기입니다.'
|
||||
description1: "여기에 무언가를 붙여넣으면 악의적인 유저에게 계정을 탈취당하거나 개인정보를 도용당할 수 있습니다."
|
||||
description2: "붙여 넣으려는 항목이 무엇인지 정확히 이해하지 못하는 경우, %c지금 바로 작업을 중단하고 이 창을 닫으십시오."
|
||||
description3: "자세한 내용은 여기를 확인해 주세요. {link}"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class AddUserSsoIdentity1782199402503 {
|
||||
name = 'AddUserSsoIdentity1782199402503'
|
||||
|
||||
/**
|
||||
* @param {QueryRunner} queryRunner
|
||||
*/
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`CREATE TABLE "user_sso_identity" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "lastUsedAt" TIMESTAMP WITH TIME ZONE, "issuer" character varying(512) NOT NULL, "sub" character varying(512) NOT NULL, "userId" character varying(32) NOT NULL, CONSTRAINT "PK_c860890d77720817a8068ea7a24" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_9712cbad45d26350f12b14670d" ON "user_sso_identity" ("issuer") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_f1ecf493e8a2bb4599dd77a16b" ON "user_sso_identity" ("userId") `);
|
||||
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_6e108c01a468d54613cc02021d" ON "user_sso_identity" ("issuer", "sub") `);
|
||||
await queryRunner.query(`ALTER TABLE "user_sso_identity" ADD CONSTRAINT "FK_f1ecf493e8a2bb4599dd77a16b3" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {QueryRunner} queryRunner
|
||||
*/
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "user_sso_identity" DROP CONSTRAINT "FK_f1ecf493e8a2bb4599dd77a16b3"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_6e108c01a468d54613cc02021d"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_f1ecf493e8a2bb4599dd77a16b"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_9712cbad45d26350f12b14670d"`);
|
||||
await queryRunner.query(`DROP TABLE "user_sso_identity"`);
|
||||
}
|
||||
}
|
||||
|
|
@ -120,6 +120,7 @@
|
|||
"node-html-parser": "7.1.0",
|
||||
"nodemailer": "8.0.10",
|
||||
"nsfwjs": "4.3.0",
|
||||
"openid-client": "6.8.4",
|
||||
"os-utils": "0.0.14",
|
||||
"otpauth": "9.5.1",
|
||||
"pg": "8.21.0",
|
||||
|
|
|
|||
|
|
@ -115,6 +115,30 @@ type Source = {
|
|||
enableQueryParamLogging?: boolean,
|
||||
}
|
||||
}
|
||||
|
||||
sso?: {
|
||||
oidc?: {
|
||||
enabled?: boolean;
|
||||
name?: string;
|
||||
issuer: string;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
scopes?: string[];
|
||||
autoProvision?: boolean;
|
||||
usernameClaim?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type SsoOidcConfig = {
|
||||
enabled: boolean;
|
||||
name: string | null;
|
||||
issuer: string;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
scopes: string[];
|
||||
autoProvision: boolean;
|
||||
usernameClaim: string;
|
||||
};
|
||||
|
||||
export type Config = {
|
||||
|
|
@ -212,6 +236,7 @@ export type Config = {
|
|||
perUserNotificationsMaxCount: number;
|
||||
deactivateAntennaThreshold: number;
|
||||
pidFile: string;
|
||||
ssoOidc: SsoOidcConfig | null;
|
||||
};
|
||||
|
||||
export type FulltextSearchProvider = 'sqlLike' | 'sqlPgroonga' | 'meilisearch';
|
||||
|
|
@ -340,6 +365,25 @@ export function loadConfig(): Config {
|
|||
deactivateAntennaThreshold: config.deactivateAntennaThreshold ?? (1000 * 60 * 60 * 24 * 7),
|
||||
pidFile: config.pidFile,
|
||||
logging: config.logging,
|
||||
ssoOidc: normalizeSsoOidc(config.sso?.oidc),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeSsoOidc(source: NonNullable<NonNullable<Source['sso']>['oidc']> | undefined): SsoOidcConfig | null {
|
||||
if (source == null) return null;
|
||||
if (!source.issuer || !source.clientId || !source.clientSecret) {
|
||||
throw new Error('sso.oidc requires issuer, clientId and clientSecret');
|
||||
}
|
||||
|
||||
return {
|
||||
enabled: source.enabled ?? true,
|
||||
name: source.name ?? null,
|
||||
issuer: source.issuer,
|
||||
clientId: source.clientId,
|
||||
clientSecret: source.clientSecret,
|
||||
scopes: source.scopes ?? ['openid', 'profile', 'email'],
|
||||
autoProvision: source.autoProvision ?? false,
|
||||
usernameClaim: source.usernameClaim ?? 'preferred_username',
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -135,6 +135,8 @@ export class MetaEntityService {
|
|||
noteSearchableScope: (this.config.fulltextSearch?.provider === 'meilisearch' && this.config.meilisearch?.scope === 'local') ? 'local' : 'global',
|
||||
maxFileSize: this.config.maxFileSize,
|
||||
federation: this.meta.federation,
|
||||
ssoOidcEnabled: this.config.ssoOidc?.enabled === true,
|
||||
ssoOidcName: this.config.ssoOidc?.name ?? null,
|
||||
};
|
||||
|
||||
return packed;
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ export const DI = {
|
|||
userKeypairsRepository: Symbol('userKeypairsRepository'),
|
||||
userPendingsRepository: Symbol('userPendingsRepository'),
|
||||
userSecurityKeysRepository: Symbol('userSecurityKeysRepository'),
|
||||
userSsoIdentitiesRepository: Symbol('userSsoIdentitiesRepository'),
|
||||
userPublickeysRepository: Symbol('userPublickeysRepository'),
|
||||
userListsRepository: Symbol('userListsRepository'),
|
||||
userListFavoritesRepository: Symbol('userListFavoritesRepository'),
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ import {
|
|||
MiUserProfile,
|
||||
MiUserPublickey,
|
||||
MiUserSecurityKey,
|
||||
MiUserSsoIdentity,
|
||||
MiWebhook,
|
||||
MiChatMessage,
|
||||
MiChatRoom,
|
||||
|
|
@ -190,6 +191,12 @@ const $userPublickeysRepository: Provider = {
|
|||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $userSsoIdentitiesRepository: Provider = {
|
||||
provide: DI.userSsoIdentitiesRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiUserSsoIdentity).extend(miRepository as MiRepository<MiUserSsoIdentity>),
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $userListsRepository: Provider = {
|
||||
provide: DI.userListsRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiUserList).extend(miRepository as MiRepository<MiUserList>),
|
||||
|
|
@ -564,6 +571,7 @@ const $reversiGamesRepository: Provider = {
|
|||
$userPendingsRepository,
|
||||
$userSecurityKeysRepository,
|
||||
$userPublickeysRepository,
|
||||
$userSsoIdentitiesRepository,
|
||||
$userListsRepository,
|
||||
$userListFavoritesRepository,
|
||||
$userListMembershipsRepository,
|
||||
|
|
@ -642,6 +650,7 @@ const $reversiGamesRepository: Provider = {
|
|||
$userPendingsRepository,
|
||||
$userSecurityKeysRepository,
|
||||
$userPublickeysRepository,
|
||||
$userSsoIdentitiesRepository,
|
||||
$userListsRepository,
|
||||
$userListFavoritesRepository,
|
||||
$userListMembershipsRepository,
|
||||
|
|
|
|||
52
packages/backend/src/models/UserSsoIdentity.ts
Normal file
52
packages/backend/src/models/UserSsoIdentity.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { PrimaryColumn, Entity, Index, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||
import { id } from './util/id.js';
|
||||
import { MiUser } from './User.js';
|
||||
|
||||
@Entity('user_sso_identity')
|
||||
@Index(['issuer', 'sub'], { unique: true })
|
||||
export class MiUserSsoIdentity {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Column('timestamp with time zone')
|
||||
public createdAt: Date;
|
||||
|
||||
@Column('timestamp with time zone', {
|
||||
nullable: true,
|
||||
})
|
||||
public lastUsedAt: Date | null;
|
||||
|
||||
/**
|
||||
* The OIDC issuer this identity belongs to.
|
||||
*/
|
||||
@Index()
|
||||
@Column('varchar', {
|
||||
length: 512,
|
||||
})
|
||||
public issuer: string;
|
||||
|
||||
/**
|
||||
* The stable subject identifier (`sub` claim) at the issuer.
|
||||
*/
|
||||
@Column('varchar', {
|
||||
length: 512,
|
||||
})
|
||||
public sub: string;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
})
|
||||
public userId: MiUser['id'];
|
||||
|
||||
@ManyToOne(() => MiUser, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public user: MiUser | null;
|
||||
}
|
||||
|
|
@ -83,6 +83,7 @@ import { MiUserPending } from '@/models/UserPending.js';
|
|||
import { MiUserProfile } from '@/models/UserProfile.js';
|
||||
import { MiUserPublickey } from '@/models/UserPublickey.js';
|
||||
import { MiUserSecurityKey } from '@/models/UserSecurityKey.js';
|
||||
import { MiUserSsoIdentity } from '@/models/UserSsoIdentity.js';
|
||||
import { MiWebhook } from '@/models/Webhook.js';
|
||||
import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js';
|
||||
|
||||
|
|
@ -157,6 +158,7 @@ export {
|
|||
MiUserProfile,
|
||||
MiUserPublickey,
|
||||
MiUserSecurityKey,
|
||||
MiUserSsoIdentity,
|
||||
MiWebhook,
|
||||
MiSystemWebhook,
|
||||
MiChannel,
|
||||
|
|
@ -237,6 +239,7 @@ export type UserPendingsRepository = Repository<MiUserPending> & MiRepository<Mi
|
|||
export type UserProfilesRepository = Repository<MiUserProfile> & MiRepository<MiUserProfile>;
|
||||
export type UserPublickeysRepository = Repository<MiUserPublickey> & MiRepository<MiUserPublickey>;
|
||||
export type UserSecurityKeysRepository = Repository<MiUserSecurityKey> & MiRepository<MiUserSecurityKey>;
|
||||
export type UserSsoIdentitiesRepository = Repository<MiUserSsoIdentity> & MiRepository<MiUserSsoIdentity>;
|
||||
export type WebhooksRepository = Repository<MiWebhook> & MiRepository<MiWebhook>;
|
||||
export type SystemWebhooksRepository = Repository<MiSystemWebhook> & MiRepository<MiWebhook>;
|
||||
export type ChannelsRepository = Repository<MiChannel> & MiRepository<MiChannel>;
|
||||
|
|
|
|||
|
|
@ -309,6 +309,14 @@ export const packedMetaLiteSchema = {
|
|||
enum: ['all', 'specified', 'none'],
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
ssoOidcEnabled: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
ssoOidcName: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ import { MiUserPending } from '@/models/UserPending.js';
|
|||
import { MiUserProfile } from '@/models/UserProfile.js';
|
||||
import { MiUserPublickey } from '@/models/UserPublickey.js';
|
||||
import { MiUserSecurityKey } from '@/models/UserSecurityKey.js';
|
||||
import { MiUserSsoIdentity } from '@/models/UserSsoIdentity.js';
|
||||
import { MiWebhook } from '@/models/Webhook.js';
|
||||
import { MiSystemWebhook } from '@/models/SystemWebhook.js';
|
||||
import { MiChannel } from '@/models/Channel.js';
|
||||
|
|
@ -195,6 +196,7 @@ export const entities = [
|
|||
MiUserListMembership,
|
||||
MiUserNotePining,
|
||||
MiUserSecurityKey,
|
||||
MiUserSsoIdentity,
|
||||
MiUsedUsername,
|
||||
MiFollowing,
|
||||
MiFollowRequest,
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import { FeedService } from './web/FeedService.js';
|
|||
import { UrlPreviewService } from './web/UrlPreviewService.js';
|
||||
import { ClientLoggerService } from './web/ClientLoggerService.js';
|
||||
import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
|
||||
import { OidcClientService } from './sso/OidcClientService.js';
|
||||
|
||||
import MainStreamConnection from '@/server/api/stream/Connection.js';
|
||||
import { MainChannel } from './api/stream/channels/main.js';
|
||||
|
|
@ -102,6 +103,7 @@ import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.j
|
|||
NoteStreamingHidingService,
|
||||
OpenApiServerService,
|
||||
OAuth2ProviderService,
|
||||
OidcClientService,
|
||||
],
|
||||
exports: [
|
||||
ServerService,
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import { HealthServerService } from './HealthServerService.js';
|
|||
import { ClientServerService } from './web/ClientServerService.js';
|
||||
import { OpenApiServerService } from './api/openapi/OpenApiServerService.js';
|
||||
import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
|
||||
import { OidcClientService } from './sso/OidcClientService.js';
|
||||
|
||||
const _dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
|
||||
|
|
@ -68,6 +69,7 @@ export class ServerService implements OnApplicationShutdown {
|
|||
private globalEventService: GlobalEventService,
|
||||
private loggerService: LoggerService,
|
||||
private oauth2ProviderService: OAuth2ProviderService,
|
||||
private oidcClientService: OidcClientService,
|
||||
) {
|
||||
this.logger = this.loggerService.getLogger('server', 'gray');
|
||||
}
|
||||
|
|
@ -148,6 +150,7 @@ export class ServerService implements OnApplicationShutdown {
|
|||
fastify.register(this.wellKnownServerService.createServer);
|
||||
fastify.register(this.oauth2ProviderService.createServer, { prefix: '/oauth' });
|
||||
fastify.register(this.oauth2ProviderService.createTokenServer, { prefix: '/oauth/token' });
|
||||
fastify.register(this.oidcClientService.createServer, { prefix: '/sso/oidc' });
|
||||
fastify.register(this.healthServerService.createServer, { prefix: '/healthz' });
|
||||
|
||||
fastify.get<{ Params: { path: string }; Querystring: { static?: any; badge?: any; }; }>('/emoji/:path(.*)', async (request, reply) => {
|
||||
|
|
|
|||
|
|
@ -33,8 +33,16 @@ export class SigninService {
|
|||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the side effects shared by every successful signin (history record,
|
||||
* login notification, new-login email) and return the user's native token.
|
||||
*
|
||||
* This is the redirect-flow-friendly core of {@link signin}: it does not
|
||||
* touch the reply, so callers that respond with a redirect (e.g. OIDC SSO)
|
||||
* can reuse it without being tied to the XHR/JSON response shape.
|
||||
*/
|
||||
@bindThis
|
||||
public signin(request: FastifyRequest, reply: FastifyReply, user: MiLocalUser) {
|
||||
public finalizeSignin(request: FastifyRequest, user: MiLocalUser): string {
|
||||
setImmediate(async () => {
|
||||
this.notificationService.createNotification(user.id, 'login', {});
|
||||
|
||||
|
|
@ -56,11 +64,18 @@ export class SigninService {
|
|||
}
|
||||
});
|
||||
|
||||
return user.token!;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public signin(request: FastifyRequest, reply: FastifyReply, user: MiLocalUser) {
|
||||
const token = this.finalizeSignin(request, user);
|
||||
|
||||
reply.code(200);
|
||||
return {
|
||||
finished: true,
|
||||
id: user.id,
|
||||
i: user.token!,
|
||||
i: token,
|
||||
} satisfies Misskey.entities.SigninFlowResponse;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
299
packages/backend/src/server/sso/OidcClientService.ts
Normal file
299
packages/backend/src/server/sso/OidcClientService.ts
Normal file
|
|
@ -0,0 +1,299 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as Redis from 'ioredis';
|
||||
import { IsNull } from 'typeorm';
|
||||
import * as oidc from 'openid-client';
|
||||
import type { Config, SsoOidcConfig } from '@/config.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { UsersRepository, UserSsoIdentitiesRepository } from '@/models/_.js';
|
||||
import type { MiLocalUser } from '@/models/User.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { SignupService } from '@/core/SignupService.js';
|
||||
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import Logger from '@/logger.js';
|
||||
import { SigninService } from '@/server/api/SigninService.js';
|
||||
import type { FastifyInstance, FastifyReply } from 'fastify';
|
||||
|
||||
// state (CSRF nonce + PKCE verifier) lifetime: 5min, single-use
|
||||
const STATE_TTL = 60 * 5;
|
||||
// one-time token handoff code lifetime: 2min, single-use
|
||||
const HANDOFF_TTL = 60 * 2;
|
||||
|
||||
type OidcStateData = {
|
||||
verifier: string;
|
||||
nonce: string;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class OidcClientService {
|
||||
private logger: Logger;
|
||||
#oidcConfigPromise: Promise<oidc.Configuration> | null = null;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.userSsoIdentitiesRepository)
|
||||
private userSsoIdentitiesRepository: UserSsoIdentitiesRepository,
|
||||
|
||||
private idService: IdService,
|
||||
private signupService: SignupService,
|
||||
private signinService: SigninService,
|
||||
private loggerService: LoggerService,
|
||||
) {
|
||||
this.logger = this.loggerService.getLogger('oidc-client');
|
||||
}
|
||||
|
||||
private get oidcConfig(): SsoOidcConfig | null {
|
||||
const c = this.config.ssoOidc;
|
||||
return c != null && c.enabled ? c : null;
|
||||
}
|
||||
|
||||
private get callbackUrl(): string {
|
||||
return `${this.config.url}/sso/oidc/callback`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazily run OIDC issuer discovery so that an unreachable IdP at boot does
|
||||
* not prevent the server from starting. The discovered Configuration is
|
||||
* memoized; on failure the promise is cleared so the next request retries.
|
||||
*/
|
||||
@bindThis
|
||||
private async getConfiguration(): Promise<oidc.Configuration> {
|
||||
const oidcConf = this.oidcConfig;
|
||||
if (oidcConf == null) {
|
||||
throw new Error('OIDC SSO is not configured');
|
||||
}
|
||||
|
||||
if (this.#oidcConfigPromise == null) {
|
||||
this.#oidcConfigPromise = oidc.discovery(
|
||||
new URL(oidcConf.issuer),
|
||||
oidcConf.clientId,
|
||||
oidcConf.clientSecret,
|
||||
).catch((err) => {
|
||||
this.#oidcConfigPromise = null;
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
return this.#oidcConfigPromise;
|
||||
}
|
||||
|
||||
private replyError(reply: FastifyReply, status: number, message: string): void {
|
||||
reply.header('Cache-Control', 'no-store');
|
||||
reply.code(status);
|
||||
reply.header('Content-Type', 'text/plain; charset=utf-8');
|
||||
reply.send(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive a valid, unused local username from an OIDC claim for
|
||||
* auto-provisioning. Local usernames must match /^\w{1,20}$/.
|
||||
*/
|
||||
private async resolveProvisionUsername(claimValue: unknown): Promise<string> {
|
||||
const base = (typeof claimValue === 'string' ? claimValue : '')
|
||||
.replace(/[^\w]/g, '_')
|
||||
.slice(0, 20)
|
||||
.replace(/^_+|_+$/g, '');
|
||||
const seed = base.length > 0 ? base : 'user';
|
||||
|
||||
// Try the sanitized value first, then fall back to suffixed variants.
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const candidate = i === 0
|
||||
? seed.slice(0, 20)
|
||||
: `${seed.slice(0, 20 - 5)}_${secureRndstr(4, { chars: '0123456789abcdefghijklmnopqrstuvwxyz' })}`;
|
||||
const exists = await this.usersRepository.exists({
|
||||
where: { usernameLower: candidate.toLowerCase(), host: IsNull() },
|
||||
});
|
||||
if (!exists) return candidate;
|
||||
}
|
||||
|
||||
throw new Error('Could not allocate a unique username for auto-provisioning');
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async createServer(fastify: FastifyInstance): Promise<void> {
|
||||
fastify.get('/login', async (_request, reply) => {
|
||||
const oidcConf = this.oidcConfig;
|
||||
if (oidcConf == null) {
|
||||
return this.replyError(reply, 404, 'OIDC SSO is not enabled.');
|
||||
}
|
||||
|
||||
let configuration: oidc.Configuration;
|
||||
try {
|
||||
configuration = await this.getConfiguration();
|
||||
} catch (err) {
|
||||
this.logger.error('OIDC issuer discovery failed', { err });
|
||||
return this.replyError(reply, 502, 'Failed to reach the identity provider.');
|
||||
}
|
||||
|
||||
const verifier = oidc.randomPKCECodeVerifier();
|
||||
const challenge = await oidc.calculatePKCECodeChallenge(verifier);
|
||||
const state = oidc.randomState();
|
||||
const nonce = oidc.randomNonce();
|
||||
|
||||
await this.redisClient.setex(
|
||||
`oidc:state:${state}`,
|
||||
STATE_TTL,
|
||||
JSON.stringify({ verifier, nonce } satisfies OidcStateData),
|
||||
);
|
||||
|
||||
const authorizationUrl = oidc.buildAuthorizationUrl(configuration, {
|
||||
redirect_uri: this.callbackUrl,
|
||||
scope: oidcConf.scopes.join(' '),
|
||||
code_challenge: challenge,
|
||||
code_challenge_method: 'S256',
|
||||
state,
|
||||
nonce,
|
||||
});
|
||||
|
||||
reply.header('Cache-Control', 'no-store');
|
||||
return reply.redirect(authorizationUrl.href);
|
||||
});
|
||||
|
||||
fastify.get('/callback', async (request, reply) => {
|
||||
const oidcConf = this.oidcConfig;
|
||||
if (oidcConf == null) {
|
||||
return this.replyError(reply, 404, 'OIDC SSO is not enabled.');
|
||||
}
|
||||
|
||||
const query = request.query as Record<string, string | undefined>;
|
||||
const state = query.state;
|
||||
if (!state) {
|
||||
return this.replyError(reply, 400, 'Missing state parameter.');
|
||||
}
|
||||
|
||||
// getdel = atomic read + delete, so a state can only be consumed once.
|
||||
const raw = await this.redisClient.getdel(`oidc:state:${state}`);
|
||||
if (!raw) {
|
||||
return this.replyError(reply, 403, 'Invalid or expired login session. Please try again.');
|
||||
}
|
||||
const { verifier, nonce } = JSON.parse(raw) as OidcStateData;
|
||||
|
||||
let configuration: oidc.Configuration;
|
||||
try {
|
||||
configuration = await this.getConfiguration();
|
||||
} catch (err) {
|
||||
this.logger.error('OIDC issuer discovery failed', { err });
|
||||
return this.replyError(reply, 502, 'Failed to reach the identity provider.');
|
||||
}
|
||||
|
||||
let claims: oidc.IDToken | undefined;
|
||||
let issuer: string;
|
||||
let sub: string;
|
||||
try {
|
||||
const currentUrl = new URL(request.url, this.config.url);
|
||||
const tokens = await oidc.authorizationCodeGrant(configuration, currentUrl, {
|
||||
pkceCodeVerifier: verifier,
|
||||
expectedNonce: nonce,
|
||||
expectedState: state,
|
||||
idTokenExpected: true,
|
||||
});
|
||||
claims = tokens.claims();
|
||||
if (claims == null) {
|
||||
throw new Error('id_token has no claims');
|
||||
}
|
||||
issuer = claims.iss;
|
||||
sub = claims.sub;
|
||||
} catch (err) {
|
||||
this.logger.warn('OIDC code exchange / id_token validation failed', { err });
|
||||
return this.replyError(reply, 403, 'Authentication with the identity provider failed.');
|
||||
}
|
||||
|
||||
let identity = await this.userSsoIdentitiesRepository.findOneBy({ issuer, sub });
|
||||
let user: MiLocalUser | null = null;
|
||||
|
||||
if (identity != null) {
|
||||
user = await this.usersRepository.findOneBy({
|
||||
id: identity.userId,
|
||||
host: IsNull(),
|
||||
}) as MiLocalUser | null;
|
||||
if (user == null) {
|
||||
this.logger.warn(`Dangling SSO identity for issuer=${issuer} sub=${sub}; user missing`);
|
||||
return this.replyError(reply, 403, 'The linked account no longer exists.');
|
||||
}
|
||||
} else {
|
||||
if (!oidcConf.autoProvision) {
|
||||
return this.replyError(reply, 403, 'No Misskey account is linked to this identity, and auto-provisioning is disabled.');
|
||||
}
|
||||
|
||||
let username: string;
|
||||
try {
|
||||
username = await this.resolveProvisionUsername(claims[oidcConf.usernameClaim]);
|
||||
} catch (err) {
|
||||
this.logger.error('Failed to resolve username for auto-provisioning', { err });
|
||||
return this.replyError(reply, 500, 'Could not create an account automatically.');
|
||||
}
|
||||
|
||||
try {
|
||||
const { account } = await this.signupService.signup({ username, password: null });
|
||||
user = account as MiLocalUser;
|
||||
} catch (err) {
|
||||
this.logger.error('Auto-provisioning signup failed', { err });
|
||||
return this.replyError(reply, 500, 'Could not create an account automatically.');
|
||||
}
|
||||
|
||||
identity = await this.userSsoIdentitiesRepository.insertOne({
|
||||
id: this.idService.gen(),
|
||||
createdAt: new Date(),
|
||||
lastUsedAt: null,
|
||||
issuer,
|
||||
sub,
|
||||
userId: user.id,
|
||||
});
|
||||
this.logger.info(`Auto-provisioned user ${user.id} (@${username}) for issuer=${issuer} sub=${sub}`);
|
||||
}
|
||||
|
||||
if (user.isSuspended) {
|
||||
return this.replyError(reply, 403, 'This account has been suspended.');
|
||||
}
|
||||
|
||||
await this.userSsoIdentitiesRepository.update({ id: identity.id }, { lastUsedAt: new Date() });
|
||||
|
||||
const token = this.signinService.finalizeSignin(request, user);
|
||||
|
||||
// One-time handoff code: the SPA exchanges it for the native token,
|
||||
// avoiding leaking the token via the redirect URL itself.
|
||||
const handoffCode = secureRndstr(32);
|
||||
await this.redisClient.setex(`oidc:handoff:${handoffCode}`, HANDOFF_TTL, token);
|
||||
|
||||
reply.header('Cache-Control', 'no-store');
|
||||
return reply.redirect(`/sso/oidc/redirect?session=${handoffCode}`);
|
||||
});
|
||||
|
||||
fastify.post<{ Body: { session?: string } }>('/exchange', async (request, reply) => {
|
||||
reply.header('Cache-Control', 'no-store');
|
||||
|
||||
const session = request.body?.session;
|
||||
if (!session || typeof session !== 'string') {
|
||||
reply.code(400);
|
||||
return { error: 'Missing session' };
|
||||
}
|
||||
|
||||
const token = await this.redisClient.getdel(`oidc:handoff:${session}`);
|
||||
if (!token) {
|
||||
reply.code(401);
|
||||
return { error: 'Invalid or expired session' };
|
||||
}
|
||||
|
||||
reply.code(200);
|
||||
return { token };
|
||||
});
|
||||
|
||||
// NOTE: intentionally no catch-all here. Unmatched paths under this
|
||||
// prefix (e.g. `/sso/oidc/redirect`) must fall through to the SPA
|
||||
// handler so the frontend redirect page can render.
|
||||
}
|
||||
}
|
||||
|
|
@ -48,6 +48,18 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<i class="ti ti-device-usb" style="font-size: medium;"></i>{{ i18n.ts.signinWithPasskey }}
|
||||
</MkButton>
|
||||
</div>
|
||||
|
||||
<!-- 外部IdP(OIDC)ログイン -->
|
||||
<template v-if="instance.ssoOidcEnabled">
|
||||
<div :class="$style.orHr">
|
||||
<p :class="$style.orMsg">{{ i18n.ts.or }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<MkButton type="button" style="margin: auto auto;" large rounded @click="onSsoOidcClick">
|
||||
<i class="ti ti-login" style="font-size: medium;"></i>{{ instance.ssoOidcName ? i18n.tsx.signinWith({ x: instance.ssoOidcName }) : i18n.ts.signinWithSso }}
|
||||
</MkButton>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -60,6 +72,7 @@ import { query, extractDomain } from '@@/js/url.js';
|
|||
import { host as configHost } from '@@/js/config.js';
|
||||
import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import * as os from '@/os.js';
|
||||
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
|
|
@ -85,6 +98,11 @@ const host = toUnicode(configHost);
|
|||
|
||||
const username = ref(props.initialUsername ?? '');
|
||||
|
||||
function onSsoOidcClick(): void {
|
||||
// Full-page redirect into the OIDC authorization flow.
|
||||
window.location.href = '/sso/oidc/login';
|
||||
}
|
||||
|
||||
//#region Open on remote
|
||||
function openRemote(options: OpenOnRemoteOptions, targetHost?: string): void {
|
||||
switch (options.type) {
|
||||
|
|
|
|||
92
packages/frontend/src/pages/sso-oidc-redirect.vue
Normal file
92
packages/frontend/src/pages/sso-oidc-redirect.vue
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div :class="$style.root">
|
||||
<MkLoading v-if="state === 'loading'"/>
|
||||
<div v-else-if="state === 'error'" class="_gaps_m" :class="$style.error">
|
||||
<i class="ti ti-circle-x" :class="$style.errorIcon"></i>
|
||||
<div>{{ i18n.ts.signinFailed }}</div>
|
||||
<MkButton primary rounded style="margin: 0 auto;" @click="retry">{{ i18n.ts.retry }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { login } from '@/accounts.js';
|
||||
import { definePage } from '@/page.js';
|
||||
|
||||
const props = defineProps<{
|
||||
session?: string;
|
||||
}>();
|
||||
|
||||
const state = ref<'loading' | 'error'>('loading');
|
||||
|
||||
async function exchange(): Promise<void> {
|
||||
state.value = 'loading';
|
||||
|
||||
if (!props.session) {
|
||||
state.value = 'error';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await window.fetch('/sso/oidc/exchange', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ session: props.session }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
state.value = 'error';
|
||||
return;
|
||||
}
|
||||
const body = await res.json() as { token?: string };
|
||||
if (!body.token) {
|
||||
state.value = 'error';
|
||||
return;
|
||||
}
|
||||
// Persists the account and reloads into the app.
|
||||
await login(body.token, '/');
|
||||
} catch {
|
||||
state.value = 'error';
|
||||
}
|
||||
}
|
||||
|
||||
function retry(): void {
|
||||
// Restart the whole OIDC flow; the one-time session code is already consumed.
|
||||
window.location.href = '/sso/oidc/login';
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
exchange();
|
||||
});
|
||||
|
||||
definePage(() => ({
|
||||
title: 'SSO',
|
||||
icon: 'ti ti-login',
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
min-height: 100svh;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
padding: 32px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.error {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.errorIcon {
|
||||
font-size: 2.5em;
|
||||
color: var(--MI_THEME-error);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -297,6 +297,12 @@ export const ROUTE_DEF = [{
|
|||
}, {
|
||||
path: '/oauth/authorize',
|
||||
component: page(() => import('@/pages/oauth.vue')),
|
||||
}, {
|
||||
path: '/sso/oidc/redirect',
|
||||
component: page(() => import('@/pages/sso-oidc-redirect.vue')),
|
||||
query: {
|
||||
session: 'session',
|
||||
},
|
||||
}, {
|
||||
path: '/tags/:tag',
|
||||
component: page(() => import('@/pages/tag.vue')),
|
||||
|
|
|
|||
|
|
@ -5264,6 +5264,10 @@ export interface Locale extends ILocale {
|
|||
* パスキーでログイン
|
||||
*/
|
||||
"signinWithPasskey": string;
|
||||
/**
|
||||
* SSOでログイン
|
||||
*/
|
||||
"signinWithSso": string;
|
||||
/**
|
||||
* 登録されていないパスキーです。
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -5555,6 +5555,8 @@ export type components = {
|
|||
maxFileSize: number;
|
||||
/** @enum {string} */
|
||||
federation: 'all' | 'specified' | 'none';
|
||||
ssoOidcEnabled: boolean;
|
||||
ssoOidcName: string | null;
|
||||
};
|
||||
MetaDetailedOnly: {
|
||||
features?: {
|
||||
|
|
|
|||
171
pnpm-lock.yaml
generated
171
pnpm-lock.yaml
generated
|
|
@ -80,7 +80,7 @@ importers:
|
|||
version: 11.5.2
|
||||
start-server-and-test:
|
||||
specifier: 3.0.9
|
||||
version: 3.0.9
|
||||
version: 3.0.9(supports-color@5.5.0)
|
||||
typescript:
|
||||
specifier: 5.9.3
|
||||
version: 5.9.3
|
||||
|
|
@ -292,6 +292,9 @@ importers:
|
|||
nsfwjs:
|
||||
specifier: 4.3.0
|
||||
version: 4.3.0(@tensorflow/tfjs@4.22.0(seedrandom@3.0.5))(buffer@6.0.3)
|
||||
openid-client:
|
||||
specifier: 6.8.4
|
||||
version: 6.8.4
|
||||
os-utils:
|
||||
specifier: 0.0.14
|
||||
version: 0.0.14
|
||||
|
|
@ -475,10 +478,10 @@ importers:
|
|||
version: 8.18.1
|
||||
'@typescript-eslint/eslint-plugin':
|
||||
specifier: 8.61.0
|
||||
version: 8.61.0(@typescript-eslint/parser@8.61.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)
|
||||
version: 8.61.0(@typescript-eslint/parser@8.61.0(eslint@9.39.4)(supports-color@5.5.0)(typescript@5.9.3))(eslint@9.39.4)(supports-color@5.5.0)(typescript@5.9.3)
|
||||
'@typescript-eslint/parser':
|
||||
specifier: 8.61.0
|
||||
version: 8.61.0(eslint@9.39.4)(typescript@5.9.3)
|
||||
version: 8.61.0(eslint@9.39.4)(supports-color@5.5.0)(typescript@5.9.3)
|
||||
'@vitest/coverage-v8':
|
||||
specifier: 4.1.8
|
||||
version: 4.1.8(vitest@4.1.8)
|
||||
|
|
@ -493,7 +496,7 @@ importers:
|
|||
version: 10.1.0
|
||||
eslint-plugin-import:
|
||||
specifier: 2.32.0
|
||||
version: 2.32.0(@typescript-eslint/parser@8.61.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)
|
||||
version: 2.32.0(@typescript-eslint/parser@8.61.0(eslint@9.39.4)(supports-color@5.5.0)(typescript@5.9.3))(eslint@9.39.4)(supports-color@8.1.1)
|
||||
execa:
|
||||
specifier: 9.6.1
|
||||
version: 9.6.1
|
||||
|
|
@ -846,10 +849,10 @@ importers:
|
|||
version: 1.4.6
|
||||
'@typescript-eslint/eslint-plugin':
|
||||
specifier: 8.61.0
|
||||
version: 8.61.0(@typescript-eslint/parser@8.61.0(eslint@9.39.4)(supports-color@10.2.2)(typescript@5.9.3))(eslint@9.39.4)(supports-color@10.2.2)(typescript@5.9.3)
|
||||
version: 8.61.0(@typescript-eslint/parser@8.61.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)
|
||||
'@typescript-eslint/parser':
|
||||
specifier: 8.61.0
|
||||
version: 8.61.0(eslint@9.39.4)(supports-color@10.2.2)(typescript@5.9.3)
|
||||
version: 8.61.0(eslint@9.39.4)(typescript@5.9.3)
|
||||
'@vitest/coverage-v8':
|
||||
specifier: 4.1.8
|
||||
version: 4.1.8(vitest@4.1.8)
|
||||
|
|
@ -870,10 +873,10 @@ importers:
|
|||
version: 15.17.0
|
||||
eslint-plugin-import:
|
||||
specifier: 2.32.0
|
||||
version: 2.32.0(@typescript-eslint/parser@8.61.0(eslint@9.39.4)(supports-color@10.2.2)(typescript@5.9.3))(eslint@9.39.4)(supports-color@8.1.1)
|
||||
version: 2.32.0(@typescript-eslint/parser@8.61.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)
|
||||
eslint-plugin-vue:
|
||||
specifier: 10.9.2
|
||||
version: 10.9.2(@stylistic/eslint-plugin@5.10.0(eslint@9.39.4))(@typescript-eslint/parser@8.61.0(eslint@9.39.4)(supports-color@10.2.2)(typescript@5.9.3))(eslint@9.39.4)(vue-eslint-parser@10.4.1(eslint@9.39.4)(supports-color@10.2.2))
|
||||
version: 10.9.2(@stylistic/eslint-plugin@5.10.0(eslint@9.39.4))(@typescript-eslint/parser@8.61.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(vue-eslint-parser@10.4.1(eslint@9.39.4))
|
||||
happy-dom:
|
||||
specifier: 20.10.2
|
||||
version: 20.10.2(bufferutil@4.1.0)(utf-8-validate@6.0.6)
|
||||
|
|
@ -924,7 +927,7 @@ importers:
|
|||
version: 3.0.5
|
||||
start-server-and-test:
|
||||
specifier: 3.0.9
|
||||
version: 3.0.9(supports-color@10.2.2)
|
||||
version: 3.0.9
|
||||
storybook:
|
||||
specifier: 10.4.3
|
||||
version: 10.4.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@testing-library/dom@10.4.0)(@types/react@19.2.14)(bufferutil@4.1.0)(prettier@3.8.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(utf-8-validate@6.0.6)
|
||||
|
|
@ -954,7 +957,7 @@ importers:
|
|||
version: 3.3.4
|
||||
vue-eslint-parser:
|
||||
specifier: 10.4.1
|
||||
version: 10.4.1(eslint@9.39.4)(supports-color@10.2.2)
|
||||
version: 10.4.1(eslint@9.39.4)
|
||||
vue-tsc:
|
||||
specifier: 3.3.4
|
||||
version: 3.3.4(typescript@5.9.3)
|
||||
|
|
@ -1179,10 +1182,10 @@ importers:
|
|||
version: 8.61.0(eslint@9.39.4)(typescript@5.9.3)
|
||||
eslint-plugin-vue:
|
||||
specifier: 10.9.2
|
||||
version: 10.9.2(@stylistic/eslint-plugin@5.10.0(eslint@9.39.4))(@typescript-eslint/parser@8.61.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(vue-eslint-parser@10.4.1(eslint@9.39.4))
|
||||
version: 10.9.2(@stylistic/eslint-plugin@5.10.0(eslint@9.39.4))(@typescript-eslint/parser@8.61.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(vue-eslint-parser@10.4.1(eslint@9.39.4)(supports-color@5.5.0))
|
||||
vue-eslint-parser:
|
||||
specifier: 10.4.1
|
||||
version: 10.4.1(eslint@9.39.4)
|
||||
version: 10.4.1(eslint@9.39.4)(supports-color@5.5.0)
|
||||
|
||||
packages/i18n:
|
||||
dependencies:
|
||||
|
|
@ -4546,7 +4549,7 @@ packages:
|
|||
engines: {node: '>= 14'}
|
||||
|
||||
aiscript-vscode@https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/1dc7f60cda78d030dadfc518a33c472202b2ef67:
|
||||
resolution: {gitHosted: true, tarball: https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/1dc7f60cda78d030dadfc518a33c472202b2ef67}
|
||||
resolution: {gitHosted: true, integrity: sha512-S4eSTHasZz29AMlnU2/zdGP8zikiDiYfYW9kNooAfwVo8OghXdxKuTDDKDAjWsbBxa1+P4uQHa4BNk9MY74rJQ==, tarball: https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/1dc7f60cda78d030dadfc518a33c472202b2ef67}
|
||||
version: 0.1.16
|
||||
engines: {vscode: ^1.83.0}
|
||||
|
||||
|
|
@ -6782,6 +6785,9 @@ packages:
|
|||
resolution: {integrity: sha512-2/OKlogiESf2Nh3TFCrRjrr9z1DRHeW0I+KReF67+4J0Ns+8hBtHRmoWAZ2OFU6I5+TWLEe6sVlSdXPjHm5UbQ==}
|
||||
engines: {node: '>= 20'}
|
||||
|
||||
jose@6.2.3:
|
||||
resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==}
|
||||
|
||||
js-beautify@1.15.4:
|
||||
resolution: {integrity: sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==}
|
||||
engines: {node: '>=14'}
|
||||
|
|
@ -7632,6 +7638,9 @@ packages:
|
|||
nth-check@2.1.1:
|
||||
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
|
||||
|
||||
oauth4webapi@3.8.6:
|
||||
resolution: {integrity: sha512-iwemM91xz8nryHti2yTmg5fhyEMVOkOXwHNqbvcATjyajb5oQxCQzrNOA6uElRHuMhQQTKUyFKV9y/CNyg25BQ==}
|
||||
|
||||
object-assign@4.1.1:
|
||||
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
|
@ -7721,6 +7730,9 @@ packages:
|
|||
peerDependencies:
|
||||
typescript: ^5.x
|
||||
|
||||
openid-client@6.8.4:
|
||||
resolution: {integrity: sha512-QSw0BA08piujetEwfZsHoTrDpMEha7GDZDicQqVwX4u0ChCjefvjDB++TZ8BTg76UpwhzIQgdvvfgfl3HpCSAw==}
|
||||
|
||||
optionator@0.9.4:
|
||||
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
|
@ -9074,7 +9086,7 @@ packages:
|
|||
engines: {node: '>= 0.4'}
|
||||
|
||||
storybook-addon-misskey-theme@https://codeload.github.com/misskey-dev/storybook-addon-misskey-theme/tar.gz/cf583db098365b2ccc81a82f63ca9c93bc32b640:
|
||||
resolution: {gitHosted: true, tarball: https://codeload.github.com/misskey-dev/storybook-addon-misskey-theme/tar.gz/cf583db098365b2ccc81a82f63ca9c93bc32b640}
|
||||
resolution: {gitHosted: true, integrity: sha512-QaH1uZSlApQ2CZPkHfhmNm89I92L02s3MdbUPG66TmAyqMaqzxd/AvobORBjtTZ0ymUSa3ii482dRXi+fFb19w==, tarball: https://codeload.github.com/misskey-dev/storybook-addon-misskey-theme/tar.gz/cf583db098365b2ccc81a82f63ca9c93bc32b640}
|
||||
version: 0.0.0
|
||||
peerDependencies:
|
||||
'@storybook/blocks': ^7.0.0-rc.4
|
||||
|
|
@ -10485,7 +10497,7 @@ snapshots:
|
|||
'@babel/code-frame': 7.29.0
|
||||
'@babel/generator': 7.29.1
|
||||
'@babel/helper-compilation-targets': 7.28.6
|
||||
'@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0)
|
||||
'@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0(supports-color@10.2.2))
|
||||
'@babel/helpers': 7.29.2
|
||||
'@babel/parser': 7.29.3
|
||||
'@babel/template': 7.28.6
|
||||
|
|
@ -10493,7 +10505,7 @@ snapshots:
|
|||
'@babel/types': 7.29.0
|
||||
'@jridgewell/remapping': 2.3.5
|
||||
convert-source-map: 2.0.0
|
||||
debug: 4.4.3(supports-color@5.5.0)
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
gensync: 1.0.0-beta.2
|
||||
json5: 2.2.3
|
||||
semver: 6.3.1
|
||||
|
|
@ -10505,7 +10517,7 @@ snapshots:
|
|||
'@babel/code-frame': 7.29.0
|
||||
'@babel/generator': 7.29.1
|
||||
'@babel/helper-compilation-targets': 7.28.6
|
||||
'@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0)
|
||||
'@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0(supports-color@10.2.2))
|
||||
'@babel/helpers': 7.29.2
|
||||
'@babel/parser': 7.29.3
|
||||
'@babel/template': 7.28.6
|
||||
|
|
@ -10545,7 +10557,7 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)':
|
||||
'@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0(supports-color@10.2.2))':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0(supports-color@10.2.2)
|
||||
'@babel/helper-module-imports': 7.28.6
|
||||
|
|
@ -10587,7 +10599,7 @@ snapshots:
|
|||
'@babel/parser': 7.29.3
|
||||
'@babel/template': 7.28.6
|
||||
'@babel/types': 7.29.0
|
||||
debug: 4.4.3(supports-color@5.5.0)
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
|
|
@ -10913,7 +10925,7 @@ snapshots:
|
|||
'@eslint/config-array@0.21.2':
|
||||
dependencies:
|
||||
'@eslint/object-schema': 2.1.7
|
||||
debug: 4.4.3(supports-color@5.5.0)
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
minimatch: 3.1.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
|
@ -10933,7 +10945,7 @@ snapshots:
|
|||
'@eslint/eslintrc@3.3.5':
|
||||
dependencies:
|
||||
ajv: 6.15.0
|
||||
debug: 4.4.3(supports-color@5.5.0)
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
espree: 10.4.0
|
||||
globals: 14.0.0
|
||||
ignore: 5.3.2
|
||||
|
|
@ -12826,7 +12838,7 @@ snapshots:
|
|||
|
||||
'@tokenizer/inflate@0.4.1':
|
||||
dependencies:
|
||||
debug: 4.4.3(supports-color@5.5.0)
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
token-types: 6.1.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
|
@ -13077,12 +13089,12 @@ snapshots:
|
|||
dependencies:
|
||||
'@types/node': 24.13.1
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.61.0(@typescript-eslint/parser@8.61.0(eslint@9.39.4)(supports-color@10.2.2)(typescript@5.9.3))(eslint@9.39.4)(supports-color@10.2.2)(typescript@5.9.3)':
|
||||
'@typescript-eslint/eslint-plugin@8.61.0(@typescript-eslint/parser@8.61.0(eslint@9.39.4)(supports-color@5.5.0)(typescript@5.9.3))(eslint@9.39.4)(supports-color@5.5.0)(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@eslint-community/regexpp': 4.12.2
|
||||
'@typescript-eslint/parser': 8.61.0(eslint@9.39.4)(supports-color@10.2.2)(typescript@5.9.3)
|
||||
'@typescript-eslint/parser': 8.61.0(eslint@9.39.4)(supports-color@5.5.0)(typescript@5.9.3)
|
||||
'@typescript-eslint/scope-manager': 8.61.0
|
||||
'@typescript-eslint/type-utils': 8.61.0(eslint@9.39.4)(supports-color@10.2.2)(typescript@5.9.3)
|
||||
'@typescript-eslint/type-utils': 8.61.0(eslint@9.39.4)(supports-color@5.5.0)(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.61.0(eslint@9.39.4)(typescript@5.9.3)
|
||||
'@typescript-eslint/visitor-keys': 8.61.0
|
||||
eslint: 9.39.4
|
||||
|
|
@ -13109,19 +13121,7 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/parser@8.61.0(eslint@9.39.4)(supports-color@10.2.2)(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/scope-manager': 8.61.0
|
||||
'@typescript-eslint/types': 8.61.0
|
||||
'@typescript-eslint/typescript-estree': 8.61.0(supports-color@10.2.2)(typescript@5.9.3)
|
||||
'@typescript-eslint/visitor-keys': 8.61.0
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
eslint: 9.39.4
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/parser@8.61.0(eslint@9.39.4)(typescript@5.9.3)':
|
||||
'@typescript-eslint/parser@8.61.0(eslint@9.39.4)(supports-color@5.5.0)(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/scope-manager': 8.61.0
|
||||
'@typescript-eslint/types': 8.61.0
|
||||
|
|
@ -13133,11 +13133,14 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/project-service@8.61.0(supports-color@10.2.2)(typescript@5.9.3)':
|
||||
'@typescript-eslint/parser@8.61.0(eslint@9.39.4)(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/tsconfig-utils': 8.61.0(typescript@5.9.3)
|
||||
'@typescript-eslint/scope-manager': 8.61.0
|
||||
'@typescript-eslint/types': 8.61.0
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
'@typescript-eslint/typescript-estree': 8.61.0(typescript@5.9.3)
|
||||
'@typescript-eslint/visitor-keys': 8.61.0
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
eslint: 9.39.4
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
|
@ -13146,7 +13149,7 @@ snapshots:
|
|||
dependencies:
|
||||
'@typescript-eslint/tsconfig-utils': 8.61.0(typescript@5.9.3)
|
||||
'@typescript-eslint/types': 8.61.0
|
||||
debug: 4.4.3(supports-color@5.5.0)
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
|
@ -13160,12 +13163,12 @@ snapshots:
|
|||
dependencies:
|
||||
typescript: 5.9.3
|
||||
|
||||
'@typescript-eslint/type-utils@8.61.0(eslint@9.39.4)(supports-color@10.2.2)(typescript@5.9.3)':
|
||||
'@typescript-eslint/type-utils@8.61.0(eslint@9.39.4)(supports-color@5.5.0)(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.61.0
|
||||
'@typescript-eslint/typescript-estree': 8.61.0(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.61.0(eslint@9.39.4)(typescript@5.9.3)
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
debug: 4.4.3(supports-color@5.5.0)
|
||||
eslint: 9.39.4
|
||||
ts-api-utils: 2.5.0(typescript@5.9.3)
|
||||
typescript: 5.9.3
|
||||
|
|
@ -13177,7 +13180,7 @@ snapshots:
|
|||
'@typescript-eslint/types': 8.61.0
|
||||
'@typescript-eslint/typescript-estree': 8.61.0(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.61.0(eslint@9.39.4)(typescript@5.9.3)
|
||||
debug: 4.4.3(supports-color@5.5.0)
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
eslint: 9.39.4
|
||||
ts-api-utils: 2.5.0(typescript@5.9.3)
|
||||
typescript: 5.9.3
|
||||
|
|
@ -13186,28 +13189,13 @@ snapshots:
|
|||
|
||||
'@typescript-eslint/types@8.61.0': {}
|
||||
|
||||
'@typescript-eslint/typescript-estree@8.61.0(supports-color@10.2.2)(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/project-service': 8.61.0(supports-color@10.2.2)(typescript@5.9.3)
|
||||
'@typescript-eslint/tsconfig-utils': 8.61.0(typescript@5.9.3)
|
||||
'@typescript-eslint/types': 8.61.0
|
||||
'@typescript-eslint/visitor-keys': 8.61.0
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
minimatch: 10.2.5
|
||||
semver: 7.8.4
|
||||
tinyglobby: 0.2.17
|
||||
ts-api-utils: 2.5.0(typescript@5.9.3)
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/typescript-estree@8.61.0(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/project-service': 8.61.0(typescript@5.9.3)
|
||||
'@typescript-eslint/tsconfig-utils': 8.61.0(typescript@5.9.3)
|
||||
'@typescript-eslint/types': 8.61.0
|
||||
'@typescript-eslint/visitor-keys': 8.61.0
|
||||
debug: 4.4.3(supports-color@5.5.0)
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
minimatch: 10.2.5
|
||||
semver: 7.8.4
|
||||
tinyglobby: 0.2.17
|
||||
|
|
@ -13810,9 +13798,9 @@ snapshots:
|
|||
|
||||
aws4@1.13.2: {}
|
||||
|
||||
axios@1.16.0(debug@4.4.3(supports-color@10.2.2)):
|
||||
axios@1.16.0(debug@4.4.3(supports-color@5.5.0)):
|
||||
dependencies:
|
||||
follow-redirects: 1.16.0(debug@4.4.3(supports-color@10.2.2))
|
||||
follow-redirects: 1.16.0(debug@4.4.3(supports-color@5.5.0))
|
||||
form-data: 4.0.6
|
||||
proxy-from-env: 2.1.0
|
||||
transitivePeerDependencies:
|
||||
|
|
@ -14943,11 +14931,11 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.61.0(eslint@9.39.4)(supports-color@10.2.2)(typescript@5.9.3))(eslint-import-resolver-node@0.3.10(supports-color@8.1.1))(eslint@9.39.4)(supports-color@8.1.1):
|
||||
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.61.0(eslint@9.39.4)(supports-color@5.5.0)(typescript@5.9.3))(eslint-import-resolver-node@0.3.10(supports-color@8.1.1))(eslint@9.39.4)(supports-color@8.1.1):
|
||||
dependencies:
|
||||
debug: 3.2.7(supports-color@8.1.1)
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 8.61.0(eslint@9.39.4)(supports-color@10.2.2)(typescript@5.9.3)
|
||||
'@typescript-eslint/parser': 8.61.0(eslint@9.39.4)(supports-color@5.5.0)(typescript@5.9.3)
|
||||
eslint: 9.39.4
|
||||
eslint-import-resolver-node: 0.3.10(supports-color@8.1.1)
|
||||
transitivePeerDependencies:
|
||||
|
|
@ -14963,7 +14951,7 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.61.0(eslint@9.39.4)(supports-color@10.2.2)(typescript@5.9.3))(eslint@9.39.4)(supports-color@8.1.1):
|
||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.61.0(eslint@9.39.4)(supports-color@5.5.0)(typescript@5.9.3))(eslint@9.39.4)(supports-color@8.1.1):
|
||||
dependencies:
|
||||
'@rtsao/scc': 1.1.0
|
||||
array-includes: 3.1.9
|
||||
|
|
@ -14974,7 +14962,7 @@ snapshots:
|
|||
doctrine: 2.1.0
|
||||
eslint: 9.39.4
|
||||
eslint-import-resolver-node: 0.3.10(supports-color@8.1.1)
|
||||
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.61.0(eslint@9.39.4)(supports-color@10.2.2)(typescript@5.9.3))(eslint-import-resolver-node@0.3.10(supports-color@8.1.1))(eslint@9.39.4)(supports-color@8.1.1)
|
||||
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.61.0(eslint@9.39.4)(supports-color@5.5.0)(typescript@5.9.3))(eslint-import-resolver-node@0.3.10(supports-color@8.1.1))(eslint@9.39.4)(supports-color@8.1.1)
|
||||
hasown: 2.0.4
|
||||
is-core-module: 2.16.2
|
||||
is-glob: 4.0.3
|
||||
|
|
@ -14986,7 +14974,7 @@ snapshots:
|
|||
string.prototype.trimend: 1.0.9
|
||||
tsconfig-paths: 3.15.0
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 8.61.0(eslint@9.39.4)(supports-color@10.2.2)(typescript@5.9.3)
|
||||
'@typescript-eslint/parser': 8.61.0(eslint@9.39.4)(supports-color@5.5.0)(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
- eslint-import-resolver-typescript
|
||||
- eslint-import-resolver-webpack
|
||||
|
|
@ -15021,7 +15009,7 @@ snapshots:
|
|||
- eslint-import-resolver-webpack
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-vue@10.9.2(@stylistic/eslint-plugin@5.10.0(eslint@9.39.4))(@typescript-eslint/parser@8.61.0(eslint@9.39.4)(supports-color@10.2.2)(typescript@5.9.3))(eslint@9.39.4)(vue-eslint-parser@10.4.1(eslint@9.39.4)(supports-color@10.2.2)):
|
||||
eslint-plugin-vue@10.9.2(@stylistic/eslint-plugin@5.10.0(eslint@9.39.4))(@typescript-eslint/parser@8.61.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(vue-eslint-parser@10.4.1(eslint@9.39.4)(supports-color@5.5.0)):
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4)
|
||||
eslint: 9.39.4
|
||||
|
|
@ -15029,11 +15017,11 @@ snapshots:
|
|||
nth-check: 2.1.1
|
||||
postcss-selector-parser: 7.1.1
|
||||
semver: 7.8.4
|
||||
vue-eslint-parser: 10.4.1(eslint@9.39.4)(supports-color@10.2.2)
|
||||
vue-eslint-parser: 10.4.1(eslint@9.39.4)(supports-color@5.5.0)
|
||||
xml-name-validator: 4.0.0
|
||||
optionalDependencies:
|
||||
'@stylistic/eslint-plugin': 5.10.0(eslint@9.39.4)
|
||||
'@typescript-eslint/parser': 8.61.0(eslint@9.39.4)(supports-color@10.2.2)(typescript@5.9.3)
|
||||
'@typescript-eslint/parser': 8.61.0(eslint@9.39.4)(typescript@5.9.3)
|
||||
|
||||
eslint-plugin-vue@10.9.2(@stylistic/eslint-plugin@5.10.0(eslint@9.39.4))(@typescript-eslint/parser@8.61.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(vue-eslint-parser@10.4.1(eslint@9.39.4)):
|
||||
dependencies:
|
||||
|
|
@ -15086,7 +15074,7 @@ snapshots:
|
|||
ajv: 6.15.0
|
||||
chalk: 4.1.2
|
||||
cross-spawn: 7.0.6
|
||||
debug: 4.4.3(supports-color@5.5.0)
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
escape-string-regexp: 4.0.0
|
||||
eslint-scope: 8.4.0
|
||||
eslint-visitor-keys: 4.2.1
|
||||
|
|
@ -15438,13 +15426,13 @@ snapshots:
|
|||
async: 0.2.10
|
||||
which: 1.3.1
|
||||
|
||||
follow-redirects@1.16.0(debug@4.4.3(supports-color@10.2.2)):
|
||||
follow-redirects@1.16.0(debug@4.4.3(supports-color@5.5.0)):
|
||||
optionalDependencies:
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
debug: 4.4.3(supports-color@5.5.0)
|
||||
|
||||
follow-redirects@1.16.0(debug@4.4.3):
|
||||
optionalDependencies:
|
||||
debug: 4.4.3(supports-color@5.5.0)
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
|
||||
for-each@0.3.5:
|
||||
dependencies:
|
||||
|
|
@ -16175,6 +16163,8 @@ snapshots:
|
|||
'@hapi/topo': 6.0.2
|
||||
'@standard-schema/spec': 1.1.0
|
||||
|
||||
jose@6.2.3: {}
|
||||
|
||||
js-beautify@1.15.4:
|
||||
dependencies:
|
||||
config-chain: 1.1.13
|
||||
|
|
@ -16840,7 +16830,7 @@ snapshots:
|
|||
micromark@4.0.2:
|
||||
dependencies:
|
||||
'@types/debug': 4.1.13
|
||||
debug: 4.4.3(supports-color@5.5.0)
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
decode-named-character-reference: 1.3.0
|
||||
devlop: 1.1.0
|
||||
micromark-core-commonmark: 2.0.3
|
||||
|
|
@ -17211,6 +17201,8 @@ snapshots:
|
|||
dependencies:
|
||||
boolbase: 1.0.0
|
||||
|
||||
oauth4webapi@3.8.6: {}
|
||||
|
||||
object-assign@4.1.1: {}
|
||||
|
||||
object-inspect@1.13.4: {}
|
||||
|
|
@ -17320,6 +17312,11 @@ snapshots:
|
|||
typescript: 5.9.3
|
||||
yargs-parser: 21.1.1
|
||||
|
||||
openid-client@6.8.4:
|
||||
dependencies:
|
||||
jose: 6.2.3
|
||||
oauth4webapi: 3.8.6
|
||||
|
||||
optionator@0.9.4:
|
||||
dependencies:
|
||||
deep-is: 0.1.4
|
||||
|
|
@ -18195,7 +18192,7 @@ snapshots:
|
|||
|
||||
require-in-the-middle@8.0.1:
|
||||
dependencies:
|
||||
debug: 4.4.3(supports-color@5.5.0)
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
module-details-from-path: 1.0.4
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
|
@ -18504,7 +18501,7 @@ snapshots:
|
|||
|
||||
send@1.2.1:
|
||||
dependencies:
|
||||
debug: 4.4.3(supports-color@5.5.0)
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
encodeurl: 2.0.0
|
||||
escape-html: 1.0.3
|
||||
etag: 1.8.1
|
||||
|
|
@ -18804,7 +18801,7 @@ snapshots:
|
|||
dependencies:
|
||||
arg: 5.0.2
|
||||
check-more-types: 2.24.0
|
||||
debug: 4.4.3(supports-color@5.5.0)
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
execa: 5.1.1
|
||||
lazy-ass: 2.0.3
|
||||
tree-kill: 1.2.2
|
||||
|
|
@ -18812,15 +18809,15 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
start-server-and-test@3.0.9(supports-color@10.2.2):
|
||||
start-server-and-test@3.0.9(supports-color@5.5.0):
|
||||
dependencies:
|
||||
arg: 5.0.2
|
||||
check-more-types: 2.24.0
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
debug: 4.4.3(supports-color@5.5.0)
|
||||
execa: 5.1.1
|
||||
lazy-ass: 2.0.3
|
||||
tree-kill: 1.2.2
|
||||
wait-on: 9.0.10(debug@4.4.3(supports-color@10.2.2))
|
||||
wait-on: 9.0.10(debug@4.4.3(supports-color@5.5.0))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
|
|
@ -19640,7 +19637,7 @@ snapshots:
|
|||
|
||||
vue-eslint-parser@10.4.1(eslint@9.39.4):
|
||||
dependencies:
|
||||
debug: 4.4.3(supports-color@5.5.0)
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
eslint: 9.39.4
|
||||
eslint-scope: 9.1.2
|
||||
eslint-visitor-keys: 5.0.1
|
||||
|
|
@ -19650,9 +19647,9 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vue-eslint-parser@10.4.1(eslint@9.39.4)(supports-color@10.2.2):
|
||||
vue-eslint-parser@10.4.1(eslint@9.39.4)(supports-color@5.5.0):
|
||||
dependencies:
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
debug: 4.4.3(supports-color@5.5.0)
|
||||
eslint: 9.39.4
|
||||
eslint-scope: 9.1.2
|
||||
eslint-visitor-keys: 5.0.1
|
||||
|
|
@ -19682,9 +19679,9 @@ snapshots:
|
|||
optionalDependencies:
|
||||
typescript: 5.9.3
|
||||
|
||||
wait-on@9.0.10(debug@4.4.3(supports-color@10.2.2)):
|
||||
wait-on@9.0.10(debug@4.4.3(supports-color@5.5.0)):
|
||||
dependencies:
|
||||
axios: 1.16.0(debug@4.4.3(supports-color@10.2.2))
|
||||
axios: 1.16.0(debug@4.4.3(supports-color@5.5.0))
|
||||
joi: 18.2.1
|
||||
lodash: 4.18.1
|
||||
minimist: 1.2.8
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue