diff --git a/.github/workflows/packer.yaml b/.github/workflows/packer.yaml
index 9026f7dca9..2d579bfb2e 100644
--- a/.github/workflows/packer.yaml
+++ b/.github/workflows/packer.yaml
@@ -46,7 +46,7 @@ jobs:
CLOUDFLARE_TOKEN: ${{secrets.CLOUDFLARE_TOKEN}}
SSL_CERT: ${{secrets.SSL_CERT}}
SSL_KEY: ${{secrets.SSL_KEY}}
- API_VERSION: 561d8d6b19c01a273b6b039628d1ff7b6b295b4e
+ API_VERSION: 439169b9522852b0582634b692260c7b8853eff2
# get the image information from gcloud
- name: Get image information
diff --git a/app/docs/nexus-openapi.json b/app/docs/nexus-openapi.json
index 2014648694..e803b1718b 100644
--- a/app/docs/nexus-openapi.json
+++ b/app/docs/nexus-openapi.json
@@ -174,6 +174,28 @@
}
}
},
+ "/login": {
+ "post": {
+ "operationId": "spoof_login",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/LoginParams"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {}
+ }
+ },
+ "/logout": {
+ "post": {
+ "operationId": "logout",
+ "responses": {}
+ }
+ },
"/organizations": {
"get": {
"description": "List all organizations.",
@@ -3110,6 +3132,15 @@
"minLength": 1,
"maxLength": 11
},
+ "LoginParams": {
+ "type": "object",
+ "properties": {
+ "username": {
+ "type": "string"
+ }
+ },
+ "required": ["username"]
+ },
"Name": {
"title": "A name used in the API",
"description": "Names must begin with a lower case ASCII letter, be composed exclusively of lowercase ASCII, uppercase ASCII, numbers, and '-', and may not end with a '-'.",
diff --git a/app/pages/LoginPage.tsx b/app/pages/LoginPage.tsx
new file mode 100644
index 0000000000..d34b6183ba
--- /dev/null
+++ b/app/pages/LoginPage.tsx
@@ -0,0 +1,101 @@
+import React from 'react'
+import { useNavigate } from 'react-router'
+
+import { useApiMutation } from '@oxide/api'
+import { Button, Warning12Icon, Success16Icon } from '@oxide/ui'
+import { useToast } from '../hooks'
+
+/**
+ * Placeholder login page for demo purposes.
+ *
+ * The demo login form is only in the console bundle for the convenience of
+ * using existing tooling and using the generated API client. In the real rack,
+ * login will go through the customer's IdP; no form controlled by us will be
+ * involved. If Nexus *does* end up serving a login form, e.g., for use by
+ * admins before the IdP is set up, that will be a separate bundle with minimal
+ * JS (ideally so minimal we could inline it in the HTML response) and it would
+ * not use the generated API client at all. It could even use an HTML form POST.
+ *
+ * Login and logout endpoints are only a temporary addition to the OpenAPI spec.
+ */
+export default function LoginPage() {
+ const navigate = useNavigate()
+ const addToast = useToast()
+ const loginPost = useApiMutation('spoofLogin', {
+ onSuccess: () => {
+ addToast({
+ title: 'Logged in',
+ icon: ,
+ timeout: 4000,
+ })
+ navigate('/')
+ },
+ onError: () => {
+ addToast({
+ title: 'Bad credentials',
+ icon: ,
+ variant: 'error',
+ timeout: 4000,
+ })
+ },
+ })
+
+ const logout = useApiMutation('logout', {
+ onSuccess: () => {
+ addToast({
+ title: 'Logged out',
+ icon: ,
+ timeout: 4000,
+ })
+ },
+ })
+ return (
+
+
+
Log in as
+
+ loginPost.mutate({ loginParams: { username: 'privileged' } })
+ }
+ >
+ Privileged
+
+
+ loginPost.mutate({ loginParams: { username: 'unprivileged' } })
+ }
+ >
+ Unprivileged
+
+
+ loginPost.mutate({ loginParams: { username: 'other' } })
+ }
+ >
+ Bad Request
+
+ logout.mutate({})}
+ >
+ Log out
+
+
+
+ )
+}
diff --git a/app/pages/OrgPage.tsx b/app/pages/OrgPage.tsx
index df2190bedb..67234b1534 100644
--- a/app/pages/OrgPage.tsx
+++ b/app/pages/OrgPage.tsx
@@ -22,7 +22,7 @@ export default function OrgPage() {
-
+
Create project
diff --git a/app/pages/ProjectsPage.tsx b/app/pages/ProjectsPage.tsx
index 7a0923ee81..c0ab1b679d 100644
--- a/app/pages/ProjectsPage.tsx
+++ b/app/pages/ProjectsPage.tsx
@@ -46,10 +46,7 @@ const ProjectsPage = () => {
}>Projects
-
+
New Project
@@ -66,10 +63,7 @@ const ProjectsPage = () => {
>
diff --git a/app/routes.tsx b/app/routes.tsx
index 75931e03a8..8eadeeb984 100644
--- a/app/routes.tsx
+++ b/app/routes.tsx
@@ -19,6 +19,7 @@ function lazyLoad(importFunc: () => Promise<{ default: React.ComponentType }>) {
)
}
+import LoginPage from './pages/LoginPage'
import InstanceCreatePage from './pages/instances/create'
import InstanceStorage from './pages/instances/Storage'
// Recharts is 350 KB
@@ -63,6 +64,8 @@ const instanceCrumb = (m: RouteMatch) => m.params.instanceName!
/** React Router route config in JSX form */
export const routes = (
+ } />
+
}
diff --git a/libs/api/__generated__/.openapi-generator/FILES b/libs/api/__generated__/.openapi-generator/FILES
index aabf90fae4..7a9656b0c3 100644
--- a/libs/api/__generated__/.openapi-generator/FILES
+++ b/libs/api/__generated__/.openapi-generator/FILES
@@ -19,6 +19,7 @@ models/Instance.ts
models/InstanceCreateParams.ts
models/InstanceResultsPage.ts
models/InstanceState.ts
+models/LoginParams.ts
models/NameOrIdSortMode.ts
models/NameSortMode.ts
models/Organization.ts
diff --git a/libs/api/__generated__/OMICRON_VERSION b/libs/api/__generated__/OMICRON_VERSION
index 02d2f8f892..40e0109141 100644
--- a/libs/api/__generated__/OMICRON_VERSION
+++ b/libs/api/__generated__/OMICRON_VERSION
@@ -1,2 +1,2 @@
# generated file. do not update manually. see docs/update-pinned-api.md
-561d8d6b19c01a273b6b039628d1ff7b6b295b4e
+439169b9522852b0582634b692260c7b8853eff2
diff --git a/libs/api/__generated__/apis/DefaultApi.ts b/libs/api/__generated__/apis/DefaultApi.ts
index 9cd4fa3103..3f5c7f54d3 100644
--- a/libs/api/__generated__/apis/DefaultApi.ts
+++ b/libs/api/__generated__/apis/DefaultApi.ts
@@ -38,6 +38,9 @@ import {
InstanceResultsPage,
InstanceResultsPageFromJSON,
InstanceResultsPageToJSON,
+ LoginParams,
+ LoginParamsFromJSON,
+ LoginParamsToJSON,
NameOrIdSortMode,
NameOrIdSortModeFromJSON,
NameOrIdSortModeToJSON,
@@ -399,6 +402,10 @@ export interface SagasGetSagaRequest {
sagaId: string
}
+export interface SpoofLoginRequest {
+ loginParams: LoginParams
+}
+
export interface VpcFirewallRulesGetRequest {
organizationName: string
projectName: string
@@ -1069,6 +1076,34 @@ export class DefaultApi extends runtime.BaseAPI {
return await response.value()
}
+ /**
+ */
+ async logoutRaw(
+ initOverrides?: RequestInit
+ ): Promise> {
+ const queryParameters: any = {}
+
+ const headerParameters: runtime.HTTPHeaders = {}
+
+ const response = await this.request(
+ {
+ path: `/logout`,
+ method: 'POST',
+ headers: headerParameters,
+ query: queryParameters,
+ },
+ initOverrides
+ )
+
+ return new runtime.VoidApiResponse(response)
+ }
+
+ /**
+ */
+ async logout(initOverrides?: RequestInit): Promise {
+ await this.logoutRaw(initOverrides)
+ }
+
/**
* Delete a specific project.
*/
@@ -3639,6 +3674,51 @@ export class DefaultApi extends runtime.BaseAPI {
return await response.value()
}
+ /**
+ */
+ async spoofLoginRaw(
+ requestParameters: SpoofLoginRequest,
+ initOverrides?: RequestInit
+ ): Promise> {
+ if (
+ requestParameters.loginParams === null ||
+ requestParameters.loginParams === undefined
+ ) {
+ throw new runtime.RequiredError(
+ 'loginParams',
+ 'Required parameter requestParameters.loginParams was null or undefined when calling spoofLogin.'
+ )
+ }
+
+ const queryParameters: any = {}
+
+ const headerParameters: runtime.HTTPHeaders = {}
+
+ headerParameters['Content-Type'] = 'application/json'
+
+ const response = await this.request(
+ {
+ path: `/login`,
+ method: 'POST',
+ headers: headerParameters,
+ query: queryParameters,
+ body: LoginParamsToJSON(requestParameters.loginParams),
+ },
+ initOverrides
+ )
+
+ return new runtime.VoidApiResponse(response)
+ }
+
+ /**
+ */
+ async spoofLogin(
+ requestParameters: SpoofLoginRequest,
+ initOverrides?: RequestInit
+ ): Promise {
+ await this.spoofLoginRaw(requestParameters, initOverrides)
+ }
+
/**
* List firewall rules for a VPC.
*/
diff --git a/libs/api/__generated__/models/LoginParams.ts b/libs/api/__generated__/models/LoginParams.ts
new file mode 100644
index 0000000000..6684e29f55
--- /dev/null
+++ b/libs/api/__generated__/models/LoginParams.ts
@@ -0,0 +1,56 @@
+/* tslint:disable */
+/* eslint-disable */
+/**
+ * Oxide Region API
+ * API for interacting with the Oxide control plane
+ *
+ * The version of the OpenAPI document: 0.0.1
+ * Contact: api@oxide.computer
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+import { exists, mapValues } from '../runtime'
+/**
+ *
+ * @export
+ * @interface LoginParams
+ */
+export interface LoginParams {
+ /**
+ *
+ * @type {string}
+ * @memberof LoginParams
+ */
+ username: string
+}
+
+export function LoginParamsFromJSON(json: any): LoginParams {
+ return LoginParamsFromJSONTyped(json, false)
+}
+
+export function LoginParamsFromJSONTyped(
+ json: any,
+ ignoreDiscriminator: boolean
+): LoginParams {
+ if (json === undefined || json === null) {
+ return json
+ }
+ return {
+ username: json['username'],
+ }
+}
+
+export function LoginParamsToJSON(value?: LoginParams | null): any {
+ if (value === undefined) {
+ return undefined
+ }
+ if (value === null) {
+ return null
+ }
+ return {
+ username: value.username,
+ }
+}
diff --git a/libs/api/__generated__/models/index.ts b/libs/api/__generated__/models/index.ts
index 4dbefa868f..0ed3d46013 100644
--- a/libs/api/__generated__/models/index.ts
+++ b/libs/api/__generated__/models/index.ts
@@ -18,6 +18,7 @@ export * from './Instance'
export * from './InstanceCreateParams'
export * from './InstanceResultsPage'
export * from './InstanceState'
+export * from './LoginParams'
export * from './NameOrIdSortMode'
export * from './NameSortMode'
export * from './Organization'
diff --git a/libs/api/index.ts b/libs/api/index.ts
index 580e8e48fd..4c625592e3 100644
--- a/libs/api/index.ts
+++ b/libs/api/index.ts
@@ -8,15 +8,7 @@ import {
const basePath =
process.env.NODE_ENV === 'production' ? process.env.API_URL : '/api'
-const config = new Configuration({
- basePath,
- headers: {
- // privileged test user
- // we're going to use this both locally and on gcp for now
- // TODO: make configurable through env vars?
- 'oxide-authn-spoof': '001de000-05e4-0000-0000-000000004007',
- },
-})
+const config = new Configuration({ basePath })
const api = new DefaultApi(config)
export const useApiQuery = getUseApiQuery(api)
diff --git a/packer/omicron.toml b/packer/omicron.toml
index ea484a3591..b6c2edfe5f 100644
--- a/packer/omicron.toml
+++ b/packer/omicron.toml
@@ -5,11 +5,16 @@
# Identifier for this instance of Nexus
id = "e6bff1ff-24fb-49dc-a54e-c6a350cd4d6c"
-[authn]
-schemes_external = ["spoof"]
+[console]
+# Directory of static assets for the console. Relative to nexus/.
+assets_directory = "tests/fixtures"
+cache_control_max_age_minutes = 10
session_idle_timeout_minutes = 60
session_absolute_timeout_minutes = 480
+[authn]
+schemes_external = ["spoof"]
+
[database]
# URL for connecting to the database
url = "postgresql://root@0.0.0.0:26257/omicron?sslmode=disable"
diff --git a/packer/oxapi_demo b/packer/oxapi_demo
index cf94b7fb39..d8b0ee4cc0 100755
--- a/packer/oxapi_demo
+++ b/packer/oxapi_demo
@@ -234,7 +234,7 @@ function cmd_instance_create_demo
# memory is 1024 * 1024 * 256
[[ $# != 3 ]] && usage "expected ORGANIZATION_NAME PROJECT_NAME INSTANCE_NAME"
mkjson name="$3" description="an instance called $3" ncpus=1 \
- memory=268435456 bootDiskSize=1 hostname="$2" |
+ memory=268435456 bootDiskSize=1 hostname="$3" |
do_curl "/organizations/$1/projects/$2/instances" -X POST -T -
}
diff --git a/tools/copy_assets.sh b/tools/copy_assets.sh
new file mode 100755
index 0000000000..8af908da44
--- /dev/null
+++ b/tools/copy_assets.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+
+set -o errexit # exit if anything fails
+set -o pipefail
+set -o xtrace
+
+API_URL=http://127.0.0.1:12220 yarn build
+rm -rf ~/oxide/omicron/nexus/assets
+cp -R dist/assets ~/oxide/omicron/nexus/assets
+cp dist/index.html ~/oxide/omicron/nexus/assets
\ No newline at end of file
diff --git a/tools/create_gcp_instance.sh b/tools/create_gcp_instance.sh
index 6389013de3..cd5b09a2e7 100755
--- a/tools/create_gcp_instance.sh
+++ b/tools/create_gcp_instance.sh
@@ -41,7 +41,7 @@ retry 2 gcloud compute instances create "$INSTANCE_NAME" \
--description="Machine automatically generated from branch ${BRANCH_NAME} of the oxidecomputer/console git repo." \
--hostname="${INSTANCE_NAME}.internal.oxide.computer" \
--zone=$ZONE \
- --image=packer-1637708444 \
+ --image=packer-1637796423 \
--maintenance-policy=TERMINATE \
--restart-on-failure \
--machine-type=$INSTANCE_TYPE \