mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-29 04:55:31 +00:00
client/web: add advanced login options
This adds an expandable section of the login view to allow users to specify an auth key and an alternate control URL. Input and Collapsible components and accompanying styles were brought over from the adminpanel. Updates #10261 Signed-off-by: Will Norris <will@tailscale.com>
This commit is contained in:
parent
f0613ab606
commit
42dc843a87
@ -9,6 +9,7 @@
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@radix-ui/react-popover": "^1.0.6",
|
||||
"@radix-ui/react-collapsible": "^1.0.3",
|
||||
"classnames": "^2.3.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
|
@ -1,7 +1,9 @@
|
||||
import React, { useCallback } from "react"
|
||||
import React, { useCallback, useState } from "react"
|
||||
import { apiFetch } from "src/api"
|
||||
import { ReactComponent as TailscaleIcon } from "src/assets/icons/tailscale-icon.svg"
|
||||
import { NodeData } from "src/hooks/node-data"
|
||||
import Collapsible from "src/ui/collapsible"
|
||||
import Input from "src/ui/input"
|
||||
|
||||
/**
|
||||
* LoginView is rendered when the client is not authenticated
|
||||
@ -14,6 +16,9 @@ export default function LoginView({
|
||||
data: NodeData
|
||||
refreshData: () => void
|
||||
}) {
|
||||
const [controlURL, setControlURL] = useState<string>("")
|
||||
const [authKey, setAuthKey] = useState<string>("")
|
||||
|
||||
const login = useCallback(
|
||||
(opt: TailscaleUpOptions) => {
|
||||
tailscaleUp(opt).then(refreshData)
|
||||
@ -76,11 +81,44 @@ export default function LoginView({
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => login({ Reauthenticate: true })}
|
||||
onClick={() =>
|
||||
login({
|
||||
Reauthenticate: true,
|
||||
ControlURL: controlURL,
|
||||
AuthKey: authKey,
|
||||
})
|
||||
}
|
||||
className="button button-blue w-full mb-4"
|
||||
>
|
||||
Log In
|
||||
</button>
|
||||
<Collapsible trigger="Advanced options">
|
||||
<h4 className="font-medium mb-1 mt-2">Auth Key</h4>
|
||||
<p className="text-sm text-gray-500">
|
||||
Connect with a pre-authenticated key.{" "}
|
||||
<a
|
||||
href="https://tailscale.com/kb/1085/auth-keys/"
|
||||
className="link"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more →
|
||||
</a>
|
||||
</p>
|
||||
<Input
|
||||
className="mt-2"
|
||||
value={authKey}
|
||||
onChange={(e) => setAuthKey(e.target.value)}
|
||||
placeholder="tskey-auth-XXX"
|
||||
/>
|
||||
<h4 className="font-medium mt-3 mb-1">Server URL</h4>
|
||||
<p className="text-sm text-gray-500">Base URL of control server.</p>
|
||||
<Input
|
||||
className="mt-2"
|
||||
value={controlURL}
|
||||
onChange={(e) => setControlURL(e.target.value)}
|
||||
placeholder="https://login.tailscale.com/"
|
||||
/>
|
||||
</Collapsible>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@ -89,6 +127,8 @@ export default function LoginView({
|
||||
|
||||
type TailscaleUpOptions = {
|
||||
Reauthenticate?: boolean // force reauthentication
|
||||
ControlURL?: string
|
||||
AuthKey?: string
|
||||
}
|
||||
|
||||
function tailscaleUp(options: TailscaleUpOptions) {
|
||||
|
@ -45,7 +45,7 @@
|
||||
}
|
||||
|
||||
.description {
|
||||
@apply text-neutral-500 leading-snug
|
||||
@apply text-neutral-500 leading-snug;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -144,6 +144,48 @@
|
||||
.toggle-small:checked:enabled:active::after {
|
||||
@apply w-[0.675rem] translate-x-[0.55rem];
|
||||
}
|
||||
|
||||
/**
|
||||
* .input defines default text input field styling. These styles should
|
||||
* correspond to .button, sharing a similar height and rounding, since .input
|
||||
* and .button are commonly used together.
|
||||
*/
|
||||
|
||||
.input,
|
||||
.input-wrapper {
|
||||
@apply appearance-none leading-tight rounded-md bg-white border border-gray-300 hover:border-gray-400 transition-colors w-full h-input;
|
||||
}
|
||||
|
||||
.input {
|
||||
@apply px-3;
|
||||
}
|
||||
|
||||
.input::placeholder,
|
||||
.input-wrapper::placeholder {
|
||||
@apply text-gray-400;
|
||||
}
|
||||
|
||||
.input:disabled,
|
||||
.input-wrapper:disabled {
|
||||
@apply border-gray-300;
|
||||
@apply bg-gray-0;
|
||||
@apply cursor-not-allowed;
|
||||
}
|
||||
|
||||
.input:focus,
|
||||
.input-wrapper:focus-within {
|
||||
@apply outline-none ring border-gray-400;
|
||||
}
|
||||
|
||||
.input-error {
|
||||
@apply border-red-200;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.h-input {
|
||||
@apply h-[2.375rem];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
33
client/web/src/ui/collapsible.tsx
Normal file
33
client/web/src/ui/collapsible.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import * as Primitive from "@radix-ui/react-collapsible"
|
||||
import React, { useState } from "react"
|
||||
import { ReactComponent as ChevronDown } from "src/assets/icons/chevron-down.svg"
|
||||
|
||||
type CollapsibleProps = {
|
||||
trigger?: string
|
||||
children: React.ReactNode
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}
|
||||
|
||||
export default function Collapsible(props: CollapsibleProps) {
|
||||
const { children, trigger, onOpenChange } = props
|
||||
const [open, setOpen] = useState(props.open)
|
||||
|
||||
return (
|
||||
<Primitive.Root
|
||||
open={open}
|
||||
onOpenChange={(open) => {
|
||||
setOpen(open)
|
||||
onOpenChange?.(open)
|
||||
}}
|
||||
>
|
||||
<Primitive.Trigger className="inline-flex items-center text-gray-600 cursor-pointer hover:bg-stone-100 rounded text-sm font-medium pr-3 py-1 transition-colors">
|
||||
<span className="ml-2 mr-1.5 group-hover:text-gray-500 -rotate-90 state-open:rotate-0">
|
||||
<ChevronDown strokeWidth={3} className="stroke-gray-400 w-4" />
|
||||
</span>
|
||||
{trigger}
|
||||
</Primitive.Trigger>
|
||||
<Primitive.Content className="mt-2">{children}</Primitive.Content>
|
||||
</Primitive.Root>
|
||||
)
|
||||
}
|
41
client/web/src/ui/input.tsx
Normal file
41
client/web/src/ui/input.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import cx from "classnames"
|
||||
import React, { InputHTMLAttributes } from "react"
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
inputClassName?: string
|
||||
error?: boolean
|
||||
suffix?: JSX.Element
|
||||
} & InputHTMLAttributes<HTMLInputElement>
|
||||
|
||||
// Input is styled in a way that only works for text inputs.
|
||||
const Input = React.forwardRef<HTMLInputElement, Props>((props, ref) => {
|
||||
const {
|
||||
className,
|
||||
inputClassName,
|
||||
error,
|
||||
prefix,
|
||||
suffix,
|
||||
disabled,
|
||||
...rest
|
||||
} = props
|
||||
return (
|
||||
<div className={cx("relative", className)}>
|
||||
<input
|
||||
ref={ref}
|
||||
className={cx("input z-10", inputClassName, {
|
||||
"input-error": error,
|
||||
})}
|
||||
disabled={disabled}
|
||||
{...rest}
|
||||
/>
|
||||
{suffix ? (
|
||||
<div className="bg-white top-1 bottom-1 right-1 rounded-r-md absolute flex items-center">
|
||||
{suffix}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export default Input
|
@ -1,9 +1,8 @@
|
||||
const plugin = require("tailwindcss/plugin")
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||
theme: {
|
||||
fontFamily: {
|
||||
sans: [
|
||||
@ -32,6 +31,16 @@ module.exports = {
|
||||
},
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
plugins: [
|
||||
plugin(function ({ addVariant }) {
|
||||
addVariant("state-open", [
|
||||
'&[data-state="open"]',
|
||||
'[data-state="open"] &',
|
||||
])
|
||||
addVariant("state-closed", [
|
||||
'&[data-state="closed"]',
|
||||
'[data-state="closed"] &',
|
||||
])
|
||||
}),
|
||||
],
|
||||
}
|
||||
|
||||
|
@ -790,10 +790,18 @@ func (s *Server) tailscaleUp(ctx context.Context, st *ipnstate.Status, opt tails
|
||||
|
||||
go func() {
|
||||
if !isRunning {
|
||||
s.lc.Start(ctx, ipn.Options{})
|
||||
ipnOptions := ipn.Options{AuthKey: opt.AuthKey}
|
||||
if opt.ControlURL != "" {
|
||||
ipnOptions.UpdatePrefs = &ipn.Prefs{ControlURL: opt.ControlURL}
|
||||
}
|
||||
if err := s.lc.Start(ctx, ipnOptions); err != nil {
|
||||
s.logf("start: %v", err)
|
||||
}
|
||||
}
|
||||
if opt.Reauthenticate {
|
||||
s.lc.StartLoginInteractive(ctx)
|
||||
if err := s.lc.StartLoginInteractive(ctx); err != nil {
|
||||
s.logf("startLogin: %v", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
@ -802,6 +810,9 @@ func (s *Server) tailscaleUp(ctx context.Context, st *ipnstate.Status, opt tails
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if n.State != nil && *n.State == ipn.Running {
|
||||
return "", nil
|
||||
}
|
||||
if n.ErrMessage != nil {
|
||||
msg := *n.ErrMessage
|
||||
return "", fmt.Errorf("backend error: %v", msg)
|
||||
@ -816,6 +827,9 @@ type tailscaleUpOptions struct {
|
||||
// If true, force reauthentication of the client.
|
||||
// Otherwise simply reconnect, the same as running `tailscale up`.
|
||||
Reauthenticate bool
|
||||
|
||||
ControlURL string
|
||||
AuthKey string
|
||||
}
|
||||
|
||||
// serveTailscaleUp serves requests to /api/up.
|
||||
|
@ -478,6 +478,21 @@
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-primitive" "1.0.3"
|
||||
|
||||
"@radix-ui/react-collapsible@^1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-collapsible/-/react-collapsible-1.0.3.tgz#df0e22e7a025439f13f62d4e4a9e92c4a0df5b81"
|
||||
integrity sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/primitive" "1.0.1"
|
||||
"@radix-ui/react-compose-refs" "1.0.1"
|
||||
"@radix-ui/react-context" "1.0.1"
|
||||
"@radix-ui/react-id" "1.0.1"
|
||||
"@radix-ui/react-presence" "1.0.1"
|
||||
"@radix-ui/react-primitive" "1.0.3"
|
||||
"@radix-ui/react-use-controllable-state" "1.0.1"
|
||||
"@radix-ui/react-use-layout-effect" "1.0.1"
|
||||
|
||||
"@radix-ui/react-compose-refs@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz#7ed868b66946aa6030e580b1ffca386dd4d21989"
|
||||
|
Loading…
Reference in New Issue
Block a user