diff --git a/docker/README.md b/docker/README.md index 1e0c3937..c7f62ae9 100644 --- a/docker/README.md +++ b/docker/README.md @@ -190,6 +190,46 @@ Or you can run a Dolt SQL server for an existing database on your local machine: When you enter your database configuration from the UI, you can use `my-doltdb` as the host name to connect to the database running in that container. +## Using a reverse proxy + +In some circumstances you may want to add a reverse proxy in front of the Dolt Workbench +for authentication or other purposes. If you'd like to use the authenticated user from the +proxy as the author for commits or tags, you can pass through the user headers. + +For example, given this [NGINX](https://www.nginx.com/) configuration that implements basic authentication: + +```conf +events {} + +http { + server { + listen 80; + server_name localhost; + + location / { + auth_basic "Restricted Access"; + auth_basic_user_file /etc/nginx/.htpasswd; + + proxy_pass http://workbench:3000; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-User $remote_user; + proxy_set_header X-Forwarded-Email $remote_user@dolthub.com; + } + } +} +``` + +The `X-Forwarded-User` and `X-Forwarded-Email` headers are passed through to the workbench +and can used as the +[author](https://docs.dolthub.com/sql-reference/version-control/dolt-sql-procedures#options-6) +when creating a commit. Simply check the "Use name and email from headers as commit +author" checkbox. The commit will be created with the user and email. + +You can also utilize this checkbox when creating releases and merging pull requests. If +the headers are not properly configured the checkbox will be disabled. + ## Contact You can reach us on [Discord](https://discord.com/invite/RFwfYpu) or [file a GitHub issue](https://github.com/dolthub/dolt-workbench/issues). diff --git a/docker/examples/nginx-reverse-proxy/.htpasswd b/docker/examples/nginx-reverse-proxy/.htpasswd new file mode 100644 index 00000000..f8cba9fb --- /dev/null +++ b/docker/examples/nginx-reverse-proxy/.htpasswd @@ -0,0 +1 @@ +taylor:$apr1$0jAEdtKz$bijebRrD5MD9MR0buo6eH1 diff --git a/docker/examples/nginx-reverse-proxy/README.md b/docker/examples/nginx-reverse-proxy/README.md new file mode 100644 index 00000000..1ac10518 --- /dev/null +++ b/docker/examples/nginx-reverse-proxy/README.md @@ -0,0 +1,12 @@ +# nginx-reverse-proxy + +Adds a reverse proxy in front of the Dolt Workbench that implements basic authentication. +It passes through user headers that can be used by the workbench as the author of commits +and tags. + +## Getting started + +``` +% docker compose build +% docker compose up +``` diff --git a/docker/examples/nginx-reverse-proxy/docker-compose.yml b/docker/examples/nginx-reverse-proxy/docker-compose.yml new file mode 100644 index 00000000..be9ac93c --- /dev/null +++ b/docker/examples/nginx-reverse-proxy/docker-compose.yml @@ -0,0 +1,19 @@ +services: + combined: + build: + context: ../../../ + dockerfile: docker/Dockerfile + ports: + - 3000:3000 + - 9002:9002 + image: docker.io/dolthub/dolt-workbench:latest + + proxy: + image: nginx:latest + ports: + - "80:80" + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf:ro + - ./.htpasswd:/etc/nginx/.htpasswd:ro + depends_on: + - combined diff --git a/docker/examples/nginx-reverse-proxy/nginx.conf b/docker/examples/nginx-reverse-proxy/nginx.conf new file mode 100644 index 00000000..9f907fdb --- /dev/null +++ b/docker/examples/nginx-reverse-proxy/nginx.conf @@ -0,0 +1,20 @@ +events {} + +http { + server { + listen 80; + server_name localhost; + + location / { + auth_basic "Restricted Access"; + auth_basic_user_file /etc/nginx/.htpasswd; + + proxy_pass http://combined:3000; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-User $remote_user; + proxy_set_header X-Forwarded-Email $remote_user@dolthub.com; + } + } +} \ No newline at end of file diff --git a/graphql-server/schema.gql b/graphql-server/schema.gql index e2c4458c..6cf3b9df 100644 --- a/graphql-server/schema.gql +++ b/graphql-server/schema.gql @@ -346,8 +346,8 @@ type Mutation { createSchema(schemaName: String!): Boolean! resetDatabase: Boolean! loadDataFile(tableName: String!, refName: String!, databaseName: String!, importOp: ImportOperation!, fileType: FileType!, file: Upload!, modifier: LoadDataModifier): Boolean! - mergePull(databaseName: String!, fromBranchName: String!, toBranchName: String!): Boolean! - createTag(tagName: String!, databaseName: String!, message: String, fromRefName: String!): String! + mergePull(fromBranchName: String!, toBranchName: String!, databaseName: String!, author: AuthorInfo): Boolean! + createTag(tagName: String!, databaseName: String!, message: String, fromRefName: String!, author: AuthorInfo): String! deleteTag(databaseName: String!, tagName: String!): Boolean! } @@ -366,4 +366,9 @@ scalar Upload enum LoadDataModifier { Ignore Replace +} + +input AuthorInfo { + name: String! + email: String! } \ No newline at end of file diff --git a/graphql-server/src/app.module.ts b/graphql-server/src/app.module.ts index 2657b01e..a3a628be 100644 --- a/graphql-server/src/app.module.ts +++ b/graphql-server/src/app.module.ts @@ -3,6 +3,7 @@ import { Module } from "@nestjs/common"; import { ConfigModule } from "@nestjs/config"; import { GraphQLModule } from "@nestjs/graphql"; import { TerminusModule } from "@nestjs/terminus"; +import { ConnectionProvider } from "./connections/connection.provider"; import { DataStoreModule } from "./dataStore/dataStore.module"; import { FileStoreModule } from "./fileStore/fileStore.module"; import resolvers from "./resolvers"; @@ -19,6 +20,6 @@ import resolvers from "./resolvers"; ConfigModule.forRoot({ isGlobal: true }), DataStoreModule, ], - providers: resolvers, + providers: [ConnectionProvider, ...resolvers], }) export class AppModule {} diff --git a/graphql-server/src/branches/branch.resolver.ts b/graphql-server/src/branches/branch.resolver.ts index 50bf9087..e73cd580 100644 --- a/graphql-server/src/branches/branch.resolver.ts +++ b/graphql-server/src/branches/branch.resolver.ts @@ -8,7 +8,7 @@ import { ResolveField, Resolver, } from "@nestjs/graphql"; -import { ConnectionResolver } from "../connections/connection.resolver"; +import { ConnectionProvider } from "../connections/connection.provider"; import { RawRow } from "../queryFactory/types"; import { Table } from "../tables/table.model"; import { TableResolver } from "../tables/table.resolver"; @@ -53,7 +53,7 @@ class ListBranchesArgs extends DBArgsWithOffset { @Resolver(_of => Branch) export class BranchResolver { constructor( - private readonly conn: ConnectionResolver, + private readonly conn: ConnectionProvider, private readonly tableResolver: TableResolver, ) {} diff --git a/graphql-server/src/commits/commit.resolver.ts b/graphql-server/src/commits/commit.resolver.ts index 71a05670..12a3e9ef 100644 --- a/graphql-server/src/commits/commit.resolver.ts +++ b/graphql-server/src/commits/commit.resolver.ts @@ -1,5 +1,5 @@ import { Args, ArgsType, Field, Query, Resolver } from "@nestjs/graphql"; -import { ConnectionResolver } from "../connections/connection.resolver"; +import { ConnectionProvider } from "../connections/connection.provider"; import { RawRow } from "../queryFactory/types"; import { ROW_LIMIT, getNextOffset } from "../utils"; import { DBArgsWithOffset } from "../utils/commonTypes"; @@ -23,7 +23,7 @@ export class ListCommitsArgs extends DBArgsWithOffset { @Resolver(_of => Commit) export class CommitResolver { - constructor(private readonly conn: ConnectionResolver) {} + constructor(private readonly conn: ConnectionProvider) {} @Query(_returns => CommitList) async commits( diff --git a/graphql-server/src/connections/connection.resolver.ts b/graphql-server/src/connections/connection.provider.ts similarity index 96% rename from graphql-server/src/connections/connection.resolver.ts rename to graphql-server/src/connections/connection.provider.ts index 36d9fac5..e519ad2a 100644 --- a/graphql-server/src/connections/connection.resolver.ts +++ b/graphql-server/src/connections/connection.provider.ts @@ -1,4 +1,4 @@ -import { Resolver } from "@nestjs/graphql"; +import { Injectable } from "@nestjs/common"; import * as mysql from "mysql2/promise"; import { DataSource } from "typeorm"; import { DatabaseType } from "../databases/database.enum"; @@ -19,8 +19,8 @@ export class WorkbenchConfig { schema?: string; // Postgres only } -@Resolver() -export class ConnectionResolver { +@Injectable() +export class ConnectionProvider { private ds: DataSource | undefined; private qf: QueryFactory | undefined; diff --git a/graphql-server/src/databases/database.resolver.ts b/graphql-server/src/databases/database.resolver.ts index 71fa0674..5c00e307 100644 --- a/graphql-server/src/databases/database.resolver.ts +++ b/graphql-server/src/databases/database.resolver.ts @@ -7,7 +7,7 @@ import { Query, Resolver, } from "@nestjs/graphql"; -import { ConnectionResolver } from "../connections/connection.resolver"; +import { ConnectionProvider } from "../connections/connection.provider"; import { DataStoreService } from "../dataStore/dataStore.service"; import { FileStoreService } from "../fileStore/fileStore.service"; import { DBArgs, SchemaArgs } from "../utils/commonTypes"; @@ -66,7 +66,7 @@ class RemoveDatabaseConnectionArgs { @Resolver(_of => DatabaseConnection) export class DatabaseResolver { constructor( - private readonly conn: ConnectionResolver, + private readonly conn: ConnectionProvider, private readonly fileStoreService: FileStoreService, private readonly dataStoreService: DataStoreService, ) {} diff --git a/graphql-server/src/diffStats/diffStat.resolver.ts b/graphql-server/src/diffStats/diffStat.resolver.ts index 853c318a..14ced11c 100644 --- a/graphql-server/src/diffStats/diffStat.resolver.ts +++ b/graphql-server/src/diffStats/diffStat.resolver.ts @@ -1,5 +1,5 @@ import { Args, ArgsType, Field, Query, Resolver } from "@nestjs/graphql"; -import { ConnectionResolver } from "../connections/connection.resolver"; +import { ConnectionProvider } from "../connections/connection.provider"; import { CommitDiffType } from "../diffSummaries/diffSummary.enums"; import { DBArgs } from "../utils/commonTypes"; import { DiffStat, fromDoltDiffStat } from "./diffStat.model"; @@ -24,7 +24,7 @@ export class DiffStatArgs extends DBArgs { @Resolver(_of => DiffStat) export class DiffStatResolver { - constructor(private readonly conn: ConnectionResolver) {} + constructor(private readonly conn: ConnectionProvider) {} @Query(_returns => DiffStat) async diffStat(@Args() args: DiffStatArgs): Promise { diff --git a/graphql-server/src/diffSummaries/diffSummary.resolver.ts b/graphql-server/src/diffSummaries/diffSummary.resolver.ts index 3ec2979f..009f4a8d 100644 --- a/graphql-server/src/diffSummaries/diffSummary.resolver.ts +++ b/graphql-server/src/diffSummaries/diffSummary.resolver.ts @@ -1,5 +1,5 @@ import { Args, ArgsType, Field, Query, Resolver } from "@nestjs/graphql"; -import { ConnectionResolver } from "../connections/connection.resolver"; +import { ConnectionProvider } from "../connections/connection.provider"; import { checkArgs } from "../diffStats/diffStat.resolver"; import { QueryFactory } from "../queryFactory"; import { DBArgs } from "../utils/commonTypes"; @@ -26,7 +26,7 @@ class DiffSummaryArgs extends DBArgs { @Resolver(_of => DiffSummary) export class DiffSummaryResolver { - constructor(private readonly conn: ConnectionResolver) {} + constructor(private readonly conn: ConnectionProvider) {} @Query(_returns => [DiffSummary]) async diffSummaries(@Args() args: DiffSummaryArgs): Promise { diff --git a/graphql-server/src/docs/doc.resolver.ts b/graphql-server/src/docs/doc.resolver.ts index 0d34ea69..48b1aa04 100644 --- a/graphql-server/src/docs/doc.resolver.ts +++ b/graphql-server/src/docs/doc.resolver.ts @@ -1,5 +1,5 @@ import { Args, ArgsType, Field, Query, Resolver } from "@nestjs/graphql"; -import { ConnectionResolver } from "../connections/connection.resolver"; +import { ConnectionProvider } from "../connections/connection.provider"; import { RefArgs } from "../utils/commonTypes"; import { DocType } from "./doc.enum"; import { Doc, DocList, fromDoltDocsRow } from "./doc.model"; @@ -12,7 +12,7 @@ class GetDefaultDocArgs extends RefArgs { @Resolver(_of => Doc) export class DocsResolver { - constructor(private readonly conn: ConnectionResolver) {} + constructor(private readonly conn: ConnectionProvider) {} @Query(_returns => DocList) async docs( diff --git a/graphql-server/src/pulls/pull.resolver.ts b/graphql-server/src/pulls/pull.resolver.ts index 86d63f12..d80fee0e 100644 --- a/graphql-server/src/pulls/pull.resolver.ts +++ b/graphql-server/src/pulls/pull.resolver.ts @@ -7,8 +7,8 @@ import { Resolver, } from "@nestjs/graphql"; import { CommitResolver } from "../commits/commit.resolver"; -import { ConnectionResolver } from "../connections/connection.resolver"; -import { DBArgs } from "../utils/commonTypes"; +import { ConnectionProvider } from "../connections/connection.provider"; +import { AuthorInfo, DBArgs } from "../utils/commonTypes"; import { PullWithDetails, fromAPIModelPullWithDetails } from "./pull.model"; @ArgsType() @@ -20,10 +20,16 @@ class PullArgs extends DBArgs { toBranchName: string; } +@ArgsType() +class MergePullArgs extends PullArgs { + @Field({ nullable: true }) + author?: AuthorInfo; +} + @Resolver(_of => PullWithDetails) export class PullResolver { constructor( - private readonly conn: ConnectionResolver, + private readonly conn: ConnectionProvider, private readonly commitResolver: CommitResolver, ) {} @@ -41,12 +47,13 @@ export class PullResolver { } @Mutation(_returns => Boolean) - async mergePull(@Args() args: PullArgs): Promise { + async mergePull(@Args() args: MergePullArgs): Promise { const conn = this.conn.connection(); await conn.callMerge({ databaseName: args.databaseName, fromBranchName: args.fromBranchName, toBranchName: args.toBranchName, + author: args.author, }); return true; } diff --git a/graphql-server/src/queryFactory/dolt/index.ts b/graphql-server/src/queryFactory/dolt/index.ts index ccbd0c6a..5f3776d2 100644 --- a/graphql-server/src/queryFactory/dolt/index.ts +++ b/graphql-server/src/queryFactory/dolt/index.ts @@ -18,7 +18,7 @@ import * as myqh from "../mysql/queries"; import { mapTablesRes } from "../mysql/utils"; import * as t from "../types"; import * as qh from "./queries"; -import { handleRefNotFound, unionCols } from "./utils"; +import { getAuthorString, handleRefNotFound, unionCols } from "./utils"; export class DoltQueryFactory extends MySQLQueryFactory @@ -345,11 +345,19 @@ export class DoltQueryFactory args: t.TagArgs & { fromRefName: string; message?: string; + author?: t.CommitAuthor; }, ): t.PR { + const params = [args.tagName, args.fromRefName]; + if (args.message) { + params.push(args.message); + } + if (args.author) { + params.push(getAuthorString(args.author)); + } return this.query( - qh.getCallNewTag(!!args.message), - [args.tagName, args.fromRefName, args.message], + qh.getCallNewTag(!!args.message, !!args.author), + params, args.databaseName, ); } @@ -358,20 +366,21 @@ export class DoltQueryFactory return this.query(qh.callDeleteTag, [args.tagName], args.databaseName); } - async callMerge(args: t.BranchesArgs): Promise { + async callMerge( + args: t.BranchesArgs & { author?: t.CommitAuthor }, + ): Promise { return this.queryMultiple( async query => { await query("BEGIN"); - const res = await query(qh.callMerge, [ + const params = [ args.fromBranchName, `Merge branch ${args.fromBranchName}`, - // TODO: add commit author - // commitAuthor: { - // name: currentUser.username, - // email: currentUser.emailAddressesList[0].address, - // }, - ]); + ]; + if (args.author) { + params.push(getAuthorString(args.author)); + } + const res = await query(qh.getCallMerge(!!args.author), params); if (res.length && res[0].conflicts !== "0") { await query("ROLLBACK"); diff --git a/graphql-server/src/queryFactory/dolt/queries.ts b/graphql-server/src/queryFactory/dolt/queries.ts index a9b1167b..9835cddb 100644 --- a/graphql-server/src/queryFactory/dolt/queries.ts +++ b/graphql-server/src/queryFactory/dolt/queries.ts @@ -71,7 +71,8 @@ export const threeDotSchemaDiffQuery = `SELECT * FROM DOLT_SCHEMA_DIFF(?, ?)`; // PULLS -export const callMerge = `CALL DOLT_MERGE(?, "--no-ff", "-m", ?)`; +export const getCallMerge = (hasAuthor = false) => + `CALL DOLT_MERGE(?, "--no-ff", "-m", ?${getAuthorNameString(hasAuthor)})`; // TAGS diff --git a/graphql-server/src/queryFactory/dolt/utils.ts b/graphql-server/src/queryFactory/dolt/utils.ts index 029cc201..b38b526a 100644 --- a/graphql-server/src/queryFactory/dolt/utils.ts +++ b/graphql-server/src/queryFactory/dolt/utils.ts @@ -1,4 +1,4 @@ -import { RawRows } from "../types"; +import { CommitAuthor, RawRows } from "../types"; export async function handleRefNotFound(q: () => Promise): Promise { try { @@ -24,3 +24,7 @@ export function unionCols(a: RawRows, b: RawRows): RawRows { }, set); return unionArray; } + +export function getAuthorString(commitAuthor: CommitAuthor): string { + return `${commitAuthor.name} <${commitAuthor.email}>`; +} diff --git a/graphql-server/src/queryFactory/index.ts b/graphql-server/src/queryFactory/index.ts index 5c003d31..4d0b105d 100644 --- a/graphql-server/src/queryFactory/index.ts +++ b/graphql-server/src/queryFactory/index.ts @@ -123,12 +123,15 @@ export declare class QueryFactory { args: t.TagArgs & { fromRefName: string; message?: string; + author?: t.CommitAuthor; }, ): t.PR; callDeleteTag(args: t.TagArgs): t.PR; - callMerge(args: t.BranchesArgs): Promise; + callMerge( + args: t.BranchesArgs & { author?: t.CommitAuthor }, + ): Promise; resolveRefs( args: t.RefsArgs & { type?: CommitDiffType }, diff --git a/graphql-server/src/queryFactory/types.ts b/graphql-server/src/queryFactory/types.ts index bc3e4d71..f7ac4344 100644 --- a/graphql-server/src/queryFactory/types.ts +++ b/graphql-server/src/queryFactory/types.ts @@ -41,3 +41,4 @@ export type ParQuery = (q: string, p?: Params) => PR; export type TableRowPagination = { pkCols: string[]; offset: number }; export type DiffRes = Promise<{ colsUnion: RawRows; diff: RawRows }>; export type CommitsRes = Promise<{ fromCommitId: string; toCommitId: string }>; +export type CommitAuthor = { name: string; email: string }; diff --git a/graphql-server/src/resolvers.ts b/graphql-server/src/resolvers.ts index a4abd4ce..f92ffd35 100644 --- a/graphql-server/src/resolvers.ts +++ b/graphql-server/src/resolvers.ts @@ -1,6 +1,5 @@ import { BranchResolver } from "./branches/branch.resolver"; import { CommitResolver } from "./commits/commit.resolver"; -import { ConnectionResolver } from "./connections/connection.resolver"; import { DatabaseResolver } from "./databases/database.resolver"; import { DiffStatResolver } from "./diffStats/diffStat.resolver"; import { DiffSummaryResolver } from "./diffSummaries/diffSummary.resolver"; @@ -19,7 +18,6 @@ import { TagResolver } from "./tags/tag.resolver"; const resolvers = [ BranchResolver, CommitResolver, - ConnectionResolver, DatabaseResolver, DiffStatResolver, DiffSummaryResolver, diff --git a/graphql-server/src/rowDiffs/rowDiff.resolver.ts b/graphql-server/src/rowDiffs/rowDiff.resolver.ts index ce93264b..b9cb191e 100644 --- a/graphql-server/src/rowDiffs/rowDiff.resolver.ts +++ b/graphql-server/src/rowDiffs/rowDiff.resolver.ts @@ -1,5 +1,5 @@ import { Args, ArgsType, Field, Query, Resolver } from "@nestjs/graphql"; -import { ConnectionResolver } from "../connections/connection.resolver"; +import { ConnectionProvider } from "../connections/connection.provider"; import { CommitDiffType, TableDiffType, @@ -38,7 +38,7 @@ class ListRowDiffsArgs extends DBArgsWithOffset { @Resolver(_of => RowDiff) export class RowDiffResolver { - constructor(private readonly conn: ConnectionResolver) {} + constructor(private readonly conn: ConnectionProvider) {} @Query(_returns => RowDiffList) async rowDiffs( diff --git a/graphql-server/src/rows/row.resolver.ts b/graphql-server/src/rows/row.resolver.ts index df79a928..b1d40465 100644 --- a/graphql-server/src/rows/row.resolver.ts +++ b/graphql-server/src/rows/row.resolver.ts @@ -1,5 +1,5 @@ import { Args, ArgsType, Field, Int, Query, Resolver } from "@nestjs/graphql"; -import { ConnectionResolver } from "../connections/connection.resolver"; +import { ConnectionProvider } from "../connections/connection.provider"; import { RefArgs } from "../utils/commonTypes"; import { Row, RowList, fromDoltListRowRes } from "./row.model"; @@ -14,7 +14,7 @@ export class ListRowsArgs extends RefArgs { @Resolver(_of => Row) export class RowResolver { - constructor(private readonly conn: ConnectionResolver) {} + constructor(private readonly conn: ConnectionProvider) {} @Query(_returns => RowList) async rows(@Args() args: ListRowsArgs): Promise { diff --git a/graphql-server/src/schemaDiffs/schemaDiff.resolver.ts b/graphql-server/src/schemaDiffs/schemaDiff.resolver.ts index fbb2e665..45b4a4af 100644 --- a/graphql-server/src/schemaDiffs/schemaDiff.resolver.ts +++ b/graphql-server/src/schemaDiffs/schemaDiff.resolver.ts @@ -1,5 +1,5 @@ import { Args, ArgsType, Field, Query, Resolver } from "@nestjs/graphql"; -import { ConnectionResolver } from "../connections/connection.resolver"; +import { ConnectionProvider } from "../connections/connection.provider"; import { CommitDiffType } from "../diffSummaries/diffSummary.enums"; import { DBArgs } from "../utils/commonTypes"; import { SchemaDiff, fromDoltSchemaDiffRows } from "./schemaDiff.model"; @@ -24,7 +24,7 @@ class SchemaDiffArgs extends DBArgs { @Resolver(_of => SchemaDiff) export class SchemaDiffResolver { - constructor(private readonly conn: ConnectionResolver) {} + constructor(private readonly conn: ConnectionProvider) {} @Query(_returns => SchemaDiff, { nullable: true }) async schemaDiff( diff --git a/graphql-server/src/schemas/schema.resolver.ts b/graphql-server/src/schemas/schema.resolver.ts index ba3d75e7..1d1fe1fd 100644 --- a/graphql-server/src/schemas/schema.resolver.ts +++ b/graphql-server/src/schemas/schema.resolver.ts @@ -1,12 +1,12 @@ import { Args, Query, Resolver } from "@nestjs/graphql"; -import { ConnectionResolver } from "../connections/connection.resolver"; +import { ConnectionProvider } from "../connections/connection.provider"; import { RefArgs } from "../utils/commonTypes"; import { SchemaType } from "./schema.enums"; import { SchemaItem } from "./schema.model"; @Resolver(_of => SchemaItem) export class SchemaResolver { - constructor(private readonly conn: ConnectionResolver) {} + constructor(private readonly conn: ConnectionProvider) {} @Query(_returns => [SchemaItem]) async doltSchemas( diff --git a/graphql-server/src/sqlSelects/sqlSelect.resolver.ts b/graphql-server/src/sqlSelects/sqlSelect.resolver.ts index fffdc9ba..49c5082d 100644 --- a/graphql-server/src/sqlSelects/sqlSelect.resolver.ts +++ b/graphql-server/src/sqlSelects/sqlSelect.resolver.ts @@ -1,5 +1,5 @@ import { Args, ArgsType, Field, Query, Resolver } from "@nestjs/graphql"; -import { ConnectionResolver } from "../connections/connection.resolver"; +import { ConnectionProvider } from "../connections/connection.provider"; import { RawRows } from "../queryFactory/types"; import { getCellValue } from "../rows/row.model"; import { RefArgs } from "../utils/commonTypes"; @@ -13,7 +13,7 @@ export class SqlSelectArgs extends RefArgs { @Resolver(_of => SqlSelect) export class SqlSelectResolver { - constructor(private readonly conn: ConnectionResolver) {} + constructor(private readonly conn: ConnectionProvider) {} @Query(_returns => SqlSelect) async sqlSelect(@Args() args: SqlSelectArgs): Promise { diff --git a/graphql-server/src/status/status.resolver.ts b/graphql-server/src/status/status.resolver.ts index 332e919d..4bd76782 100644 --- a/graphql-server/src/status/status.resolver.ts +++ b/graphql-server/src/status/status.resolver.ts @@ -1,11 +1,11 @@ import { Args, Query, Resolver } from "@nestjs/graphql"; -import { ConnectionResolver } from "../connections/connection.resolver"; +import { ConnectionProvider } from "../connections/connection.provider"; import { RefArgs } from "../utils/commonTypes"; import { Status, fromStatusRows } from "./status.model"; @Resolver(_of => Status) export class StatusResolver { - constructor(private readonly conn: ConnectionResolver) {} + constructor(private readonly conn: ConnectionProvider) {} @Query(_returns => [Status]) async status(@Args() args: RefArgs): Promise { diff --git a/graphql-server/src/tables/table.resolver.ts b/graphql-server/src/tables/table.resolver.ts index ad2e37e0..dc1d4e57 100644 --- a/graphql-server/src/tables/table.resolver.ts +++ b/graphql-server/src/tables/table.resolver.ts @@ -1,5 +1,5 @@ import { Args, ArgsType, Field, Query, Resolver } from "@nestjs/graphql"; -import { ConnectionResolver } from "../connections/connection.resolver"; +import { ConnectionProvider } from "../connections/connection.provider"; import { handleTableNotFound } from "../utils"; import { RefArgs, TableArgs } from "../utils/commonTypes"; import { Table, TableNames, fromDoltRowRes } from "./table.model"; @@ -12,7 +12,7 @@ class ListTableArgs extends RefArgs { @Resolver(_of => Table) export class TableResolver { - constructor(private readonly conn: ConnectionResolver) {} + constructor(private readonly conn: ConnectionProvider) {} @Query(_returns => Table) async table(@Args() args: TableArgs): Promise { diff --git a/graphql-server/src/tables/upload.resolver.ts b/graphql-server/src/tables/upload.resolver.ts index fa4449f7..56cf4e19 100644 --- a/graphql-server/src/tables/upload.resolver.ts +++ b/graphql-server/src/tables/upload.resolver.ts @@ -4,7 +4,7 @@ import { ReadStream } from "fs"; import GraphQLUpload from "graphql-upload/GraphQLUpload.js"; import { from as copyFrom } from "pg-copy-streams"; import { pipeline } from "stream/promises"; -import { ConnectionResolver } from "../connections/connection.resolver"; +import { ConnectionProvider } from "../connections/connection.provider"; import { DatabaseType } from "../databases/database.enum"; import { useDB } from "../queryFactory/mysql/queries"; import { setSearchPath } from "../queryFactory/postgres/queries"; @@ -36,7 +36,7 @@ class TableImportArgs extends TableArgs { @Resolver(_of => Table) export class FileUploadResolver { - constructor(private readonly connResolver: ConnectionResolver) {} + constructor(private readonly connResolver: ConnectionProvider) {} @Mutation(_returns => Boolean) async loadDataFile(@Args() args: TableImportArgs): Promise { diff --git a/graphql-server/src/tags/tag.resolver.ts b/graphql-server/src/tags/tag.resolver.ts index 2810deee..70db776f 100644 --- a/graphql-server/src/tags/tag.resolver.ts +++ b/graphql-server/src/tags/tag.resolver.ts @@ -6,19 +6,10 @@ import { Query, Resolver, } from "@nestjs/graphql"; -import { ConnectionResolver } from "../connections/connection.resolver"; -import { DBArgs, TagArgs } from "../utils/commonTypes"; +import { ConnectionProvider } from "../connections/connection.provider"; +import { AuthorInfo, DBArgs, TagArgs } from "../utils/commonTypes"; import { Tag, TagList, fromDoltRowRes } from "./tag.model"; -// @InputType() -// class AuthorInfo { -// @Field() -// name: string; - -// @Field() -// email: string; -// } - @ArgsType() class CreateTagArgs extends TagArgs { @Field({ nullable: true }) @@ -27,13 +18,13 @@ class CreateTagArgs extends TagArgs { @Field() fromRefName: string; - // @Field({ nullable: true }) - // author?: AuthorInfo; + @Field({ nullable: true }) + author?: AuthorInfo; } @Resolver(_of => Tag) export class TagResolver { - constructor(private readonly conn: ConnectionResolver) {} + constructor(private readonly conn: ConnectionProvider) {} @Query(_returns => TagList) async tags(@Args() args: DBArgs): Promise { @@ -66,11 +57,3 @@ export class TagResolver { return true; } } - -// TODO: commit author -// export type CommitAuthor = { name: string; email: string }; - -// function getAuthorString(commitAuthor?: CommitAuthor): string { -// if (!commitAuthor) return ""; -// return `${commitAuthor.name} <${commitAuthor.email}>`; -// } diff --git a/graphql-server/src/utils/commonTypes.ts b/graphql-server/src/utils/commonTypes.ts index 66d355b8..2e639347 100644 --- a/graphql-server/src/utils/commonTypes.ts +++ b/graphql-server/src/utils/commonTypes.ts @@ -1,4 +1,4 @@ -import { ArgsType, Field, Int, ObjectType } from "@nestjs/graphql"; +import { ArgsType, Field, InputType, Int, ObjectType } from "@nestjs/graphql"; @ObjectType() export class ListOffsetRes { @@ -47,3 +47,12 @@ export class TagArgs extends DBArgs { @Field() tagName: string; } + +@InputType() +export class AuthorInfo { + @Field() + name: string; + + @Field() + email: string; +} diff --git a/web/components/HeaderUserCheckbox/index.tsx b/web/components/HeaderUserCheckbox/index.tsx new file mode 100644 index 00000000..cccaca35 --- /dev/null +++ b/web/components/HeaderUserCheckbox/index.tsx @@ -0,0 +1,37 @@ +import { Checkbox } from "@dolthub/react-components"; +import { UserHeaders } from "@hooks/useUserHeaders"; +import { useEffect } from "react"; + +type Props = { + shouldAddAuthor: boolean; + setShouldAddAuthor: (s: boolean) => void; + userHeaders: UserHeaders | null; + className?: string; + kind?: string; +}; + +export default function HeaderUserCheckbox(props: Props) { + const disabled = !props.userHeaders; + const authorKind = props.kind ? `${props.kind} author` : "author"; + + useEffect(() => { + if (!props.userHeaders?.email || !props.userHeaders.user) return; + props.setShouldAddAuthor(true); + }, [props.userHeaders]); + + return ( + props.setShouldAddAuthor(e.target.checked)} + description={ + disabled + ? "Disabled, headers not found. See Docker Hub for more information about user headers." + : `Recommended. If unchecked, SQL user will be used as ${authorKind}.` + } + disabled={disabled} + className={props.className} + /> + ); +} diff --git a/web/components/StatusWithOptions/CommitModal.tsx b/web/components/StatusWithOptions/CommitModal.tsx index f315dd90..513c3aa1 100644 --- a/web/components/StatusWithOptions/CommitModal.tsx +++ b/web/components/StatusWithOptions/CommitModal.tsx @@ -1,5 +1,8 @@ +import HeaderUserCheckbox from "@components/HeaderUserCheckbox"; import { FormModal, Textarea } from "@dolthub/react-components"; +import { Maybe } from "@dolthub/web-utils"; import { StatusFragment } from "@gen/graphql-types"; +import { useUserHeaders } from "@hooks/useUserHeaders"; import { ModalProps } from "@lib/modalProps"; import { RefParams } from "@lib/params"; import { sqlQuery } from "@lib/urls"; @@ -16,11 +19,16 @@ export default function CommitModal(props: Props) { const router = useRouter(); const defaultMsg = getDefaultCommitMsg(props.params.refName, props.status); const [msg, setMsg] = useState(defaultMsg); - // const [addCommitAuthor, setAddCommitAuthor] = useState(true); + const userHeaders = useUserHeaders(); + const [addCommitAuthor, setAddCommitAuthor] = useState(!!userHeaders); const onSubmit = (e: SyntheticEvent) => { e.preventDefault(); - const q = `CALL DOLT_COMMIT("-Am", "${msg}")`; + const q = `CALL DOLT_COMMIT("-Am", "${msg}"${ + addCommitAuthor + ? getAuthorInfo(userHeaders?.user, userHeaders?.email) + : "" + })`; const { href, as } = sqlQuery({ ...props.params, q }); router.push(href, as).catch(console.error); }; @@ -54,16 +62,12 @@ export default function CommitModal(props: Props) { required light /> - {/*
- setAddCommitAuthor(e.target.checked)} - description="Recommended. If unchecked, Dolt System Account will be used as - commit author." - /> -
*/} + ); @@ -78,7 +82,7 @@ function getDefaultCommitMsg( .join(", ")} from ${refName}`; } -// function getAuthorInfo(currentUser?: CurrentUserFragment): string { -// if (!currentUser) return ""; -// return `, "--author", "${currentUser.username} <${currentUser.emailAddressesList[0].address}>"`; -// } +function getAuthorInfo(name: Maybe, email: Maybe): string { + if (!name && !email) return ""; + return `, "--author", "${name} <${email}>"`; +} diff --git a/web/components/pageComponents/DatabasePage/ForPulls/PullActions/Merge/MergeButton.tsx b/web/components/pageComponents/DatabasePage/ForPulls/PullActions/Merge/MergeButton.tsx index 4b62897d..9069fd4f 100644 --- a/web/components/pageComponents/DatabasePage/ForPulls/PullActions/Merge/MergeButton.tsx +++ b/web/components/pageComponents/DatabasePage/ForPulls/PullActions/Merge/MergeButton.tsx @@ -1,3 +1,4 @@ +import HeaderUserCheckbox from "@components/HeaderUserCheckbox"; import { Button } from "@dolthub/react-components"; import { PullDetailsForPullDetailsDocument, @@ -5,6 +6,7 @@ import { useMergePullMutation, } from "@gen/graphql-types"; import useMutation from "@hooks/useMutation"; +import { useUserHeaders } from "@hooks/useUserHeaders"; import { gqlPullHasConflicts } from "@lib/errors/graphql"; import { errorMatches } from "@lib/errors/helpers"; import { PullDiffParams } from "@lib/params"; @@ -24,7 +26,10 @@ type Props = { }; export default function MergeButton(props: Props) { - // const { setState } = usePullDetailsContext(); + const userHeaders = useUserHeaders(); + const [addAuthor, setAddAuthor] = useState( + !!(userHeaders?.email && userHeaders.user), + ); const [showDirections, setShowDirections] = useState(false); const variables = { ...props.params, toBranchName: props.params.refName }; const { mutateFn: merge, ...res } = useMutation({ @@ -38,8 +43,15 @@ export default function MergeButton(props: Props) { const red = hasConflicts; const onClick = async () => { - await merge({ variables }); - // setState({ isMerging: true }); + await merge({ + variables: { + ...variables, + author: + addAuthor && userHeaders?.email && userHeaders.user + ? { name: userHeaders.user, email: userHeaders.email } + : undefined, + }, + }); }; return ( @@ -70,6 +82,13 @@ export default function MergeButton(props: Props) { )}
+ View{" "} diff --git a/web/components/pageComponents/DatabasePage/ForPulls/PullActions/Merge/index.module.css b/web/components/pageComponents/DatabasePage/ForPulls/PullActions/Merge/index.module.css index d0ac96a2..c41136b9 100644 --- a/web/components/pageComponents/DatabasePage/ForPulls/PullActions/Merge/index.module.css +++ b/web/components/pageComponents/DatabasePage/ForPulls/PullActions/Merge/index.module.css @@ -51,6 +51,10 @@ @apply px-6 py-2 border-x border-b rounded-b-xl bg-white border-acc-green; } +.userCheckbox { + @apply text-sm mt-4; +} + .note { @apply font-normal text-ld-darkgrey mt-0.5 py-1 block; } diff --git a/web/components/pageComponents/DatabasePage/ForPulls/PullActions/Merge/queries.ts b/web/components/pageComponents/DatabasePage/ForPulls/PullActions/Merge/queries.ts index 0c13522f..533858d9 100644 --- a/web/components/pageComponents/DatabasePage/ForPulls/PullActions/Merge/queries.ts +++ b/web/components/pageComponents/DatabasePage/ForPulls/PullActions/Merge/queries.ts @@ -5,11 +5,13 @@ export const MERGE_PULL = gql` $databaseName: String! $fromBranchName: String! $toBranchName: String! + $author: AuthorInfo ) { mergePull( databaseName: $databaseName fromBranchName: $fromBranchName toBranchName: $toBranchName + author: $author ) } `; diff --git a/web/components/pageComponents/DatabasePage/ForReleases/NewReleasePage/NewReleaseForm/index.tsx b/web/components/pageComponents/DatabasePage/ForReleases/NewReleasePage/NewReleaseForm/index.tsx index 0b1abab0..1db8b854 100644 --- a/web/components/pageComponents/DatabasePage/ForReleases/NewReleasePage/NewReleaseForm/index.tsx +++ b/web/components/pageComponents/DatabasePage/ForReleases/NewReleasePage/NewReleaseForm/index.tsx @@ -1,4 +1,5 @@ import BranchAndCommitSelector from "@components/FormSelectForRefs/BranchAndCommitSelector"; +import HeaderUserCheckbox from "@components/HeaderUserCheckbox"; import { Button, ButtonsWithError, @@ -62,17 +63,14 @@ export default function NewTagForm(props: Props): JSX.Element { className={css.textarea} data-cy="new-tag-description-textarea" /> - {/*
- - createTagRes.setFormData({ addTagAuthor: e.target.checked }) - } - description="Recommended. If unchecked, Dolt System Account will be used as tag author." - /> -
*/} + + createTagRes.setFormData({ addTagAuthor: s }) + } + userHeaders={createTagRes.userHeaders} + kind="tag" + /> >; + userHeaders: UserHeaders | null; }; // A helper function to create a tag using a specific revision type export default function useCreateTag(params: DatabaseParams): ReturnType { + const userHeaders = useUserHeaders(); const [formData, setFormData] = useSetState({ tagName: "", message: "", fromRefName: null as Maybe, - addTagAuthor: false, + addTagAuthor: !!(userHeaders?.user && userHeaders.email), }); const [loading, setLoading] = useState(false); @@ -51,13 +54,13 @@ export default function useCreateTag(params: DatabaseParams): ReturnType { tagName: formData.tagName, message: formData.message, fromRefName: formData.fromRefName, - // author: - // formData.addTagAuthor && currentUser - // ? { - // name: currentUser.username, - // email: currentUser.emailAddressesList[0].address, - // } : - // undefined, + author: + formData.addTagAuthor && userHeaders?.user && userHeaders.email + ? { + name: userHeaders.user, + email: userHeaders.email, + } + : undefined, }, }); return data?.createTag; @@ -73,5 +76,6 @@ export default function useCreateTag(params: DatabaseParams): ReturnType { canCreateTag, formData, setFormData, + userHeaders, }; } diff --git a/web/gen/graphql-types.tsx b/web/gen/graphql-types.tsx index 81167822..e9e63f46 100644 --- a/web/gen/graphql-types.tsx +++ b/web/gen/graphql-types.tsx @@ -19,6 +19,11 @@ export type Scalars = { Upload: { input: any; output: any; } }; +export type AuthorInfo = { + email: Scalars['String']['input']; + name: Scalars['String']['input']; +}; + export type Branch = { __typename?: 'Branch'; _id: Scalars['ID']['output']; @@ -258,6 +263,7 @@ export type MutationCreateSchemaArgs = { export type MutationCreateTagArgs = { + author?: InputMaybe; databaseName: Scalars['String']['input']; fromRefName: Scalars['String']['input']; message?: InputMaybe; @@ -289,6 +295,7 @@ export type MutationLoadDataFileArgs = { export type MutationMergePullArgs = { + author?: InputMaybe; databaseName: Scalars['String']['input']; fromBranchName: Scalars['String']['input']; toBranchName: Scalars['String']['input']; @@ -1016,6 +1023,7 @@ export type MergePullMutationVariables = Exact<{ databaseName: Scalars['String']['input']; fromBranchName: Scalars['String']['input']; toBranchName: Scalars['String']['input']; + author?: InputMaybe; }>; @@ -1056,6 +1064,7 @@ export type CreateTagMutationVariables = Exact<{ tagName: Scalars['String']['input']; message?: InputMaybe; fromRefName: Scalars['String']['input']; + author?: InputMaybe; }>; @@ -2958,11 +2967,12 @@ export type GetBranchForPullLazyQueryHookResult = ReturnType; export type GetBranchForPullQueryResult = Apollo.QueryResult; export const MergePullDocument = gql` - mutation MergePull($databaseName: String!, $fromBranchName: String!, $toBranchName: String!) { + mutation MergePull($databaseName: String!, $fromBranchName: String!, $toBranchName: String!, $author: AuthorInfo) { mergePull( databaseName: $databaseName fromBranchName: $fromBranchName toBranchName: $toBranchName + author: $author ) } `; @@ -2984,6 +2994,7 @@ export type MergePullMutationFn = Apollo.MutationFunction; export type RefPageQueryQueryResult = Apollo.QueryResult; export const CreateTagDocument = gql` - mutation CreateTag($databaseName: String!, $tagName: String!, $message: String, $fromRefName: String!) { + mutation CreateTag($databaseName: String!, $tagName: String!, $message: String, $fromRefName: String!, $author: AuthorInfo) { createTag( databaseName: $databaseName tagName: $tagName message: $message fromRefName: $fromRefName + author: $author ) } `; @@ -3118,6 +3130,7 @@ export type CreateTagMutationFn = Apollo.MutationFunction(null); + + useEffectAsync(async () => { + const fetchHeaders = async () => { + const response = await fetch("/api/headers"); // This endpoint should return headers + const headersData = await response.json(); + const user = headersData["x-forwarded-user"]; + const email = headersData["x-forwarded-email"]; + if (user || email) { + setHeaders({ user, email }); + } + }; + + await fetchHeaders(); + }, []); + + return headers; +} diff --git a/web/pages/api/headers.ts b/web/pages/api/headers.ts new file mode 100644 index 00000000..79f69207 --- /dev/null +++ b/web/pages/api/headers.ts @@ -0,0 +1,5 @@ +import { NextApiRequest, NextApiResponse } from "next"; + +export default function handler(req: NextApiRequest, res: NextApiResponse) { + res.status(200).json(req.headers); +}