Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ class ServletAwareConfigurator extends ServerEndpointConfig.Configurator with La
null,
null,
null,
null,
null
)
)
Expand Down Expand Up @@ -107,6 +108,7 @@ class ServletAwareConfigurator extends ServerEndpointConfig.Configurator with La
null,
null,
null,
null,
null
)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import org.apache.texera.web.resource.dashboard.admin.execution.AdminExecutionRe
import org.apache.texera.web.resource.dashboard.admin.settings.AdminSettingsResource
import org.apache.texera.web.resource.dashboard.admin.user.AdminUserResource
import org.apache.texera.web.resource.dashboard.hub.HubResource
import org.apache.texera.web.resource.dashboard.user.UserResource
import org.apache.texera.web.resource.dashboard.user.project.{
ProjectAccessResource,
ProjectResource,
Expand Down Expand Up @@ -140,6 +141,7 @@ class TexeraWebApplication
environment.jersey.register(classOf[WorkflowAccessResource])
environment.jersey.register(classOf[WorkflowResource])
environment.jersey.register(classOf[HubResource])
environment.jersey.register(classOf[UserResource])
environment.jersey.register(classOf[WorkflowVersionResource])
environment.jersey.register(classOf[ProjectResource])
environment.jersey.register(classOf[ProjectAccessResource])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import javax.ws.rs.core.SecurityContext
}

val GUEST: User =
new User(null, "guest", null, null, null, null, UserRoleEnum.REGULAR, null, null)
new User(null, "guest", null, null, null, null, UserRoleEnum.REGULAR, null, null, null)
}

@PreMatching
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,18 @@ object UserAuthenticator extends Authenticator[JwtContext, SessionUser] with Laz
val accountCreation =
context.getJwtClaims.getClaimValue("accountCreation").asInstanceOf[OffsetDateTime]
val user =
new User(userId, userName, email, null, googleId, null, role, comment, accountCreation)
new User(
userId,
userName,
email,
null,
googleId,
null,
role,
comment,
accountCreation,
null
)
Optional.of(new SessionUser(user))
} catch {
case e: Exception =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ case class UserInfo(
googleAvatar: String,
comment: String,
lastLogin: java.time.OffsetDateTime, // will be null if never logged in
accountCreation: java.time.OffsetDateTime
accountCreation: java.time.OffsetDateTime,
affiliation: String
)

object AdminUserResource {
Expand Down Expand Up @@ -78,7 +79,8 @@ class AdminUserResource {
USER.GOOGLE_AVATAR,
USER.COMMENT,
USER_LAST_ACTIVE_TIME.LAST_ACTIVE_TIME,
USER.ACCOUNT_CREATION_TIME
USER.ACCOUNT_CREATION_TIME,
USER.AFFILIATION
)
.from(USER)
.leftJoin(USER_LAST_ACTIVE_TIME)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.apache.texera.web.resource.dashboard.user

import org.apache.texera.dao.SqlServer
import org.apache.texera.dao.jooq.generated.tables.daos.UserDao
import org.apache.texera.dao.jooq.generated.tables.User.USER
import javax.ws.rs._
import javax.ws.rs.core.{MediaType, Response}

case class AffiliationUpdateRequest(uid: Int, affiliation: String)

object UserResource {
private lazy val context = SqlServer.getInstance().createDSLContext()
private lazy val userDao = new UserDao(context.configuration)
}

@Path("/user")
class UserResource {

/**
* Update the affiliation of a user.
* Used by a first-time user to set their own affiliation.
*/
@PUT
@Path("/affiliation")
@Consumes(Array(MediaType.APPLICATION_JSON))
def updateAffiliation(request: AffiliationUpdateRequest): Unit = {
val rowsUpdated = UserResource.context
.update(USER)
.set(USER.AFFILIATION, request.affiliation)
.where(USER.UID.eq(request.uid))
.execute()

if (rowsUpdated == 0) {
throw new WebApplicationException("User not found", Response.Status.NOT_FOUND)
}
}

/**
* Gets affiliation with uid. Returns "", null or affiliation.
* "": Prompted and no response
* null: never prompted
* @param uid
* @return
*/
@GET
@Path("/affiliation")
@Produces(Array(MediaType.APPLICATION_JSON))
def needsAffiliation(@QueryParam("uid") uid: Int): java.lang.Boolean = {
val user = UserResource.userDao.fetchOneByUid(uid)
if (user == null) {
throw new WebApplicationException("User not found", Response.Status.NOT_FOUND)
}
java.lang.Boolean.valueOf(user.getAffiliation == null)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ object JwtParser extends LazyLogging {
val role = UserRoleEnum.valueOf(jwtClaims.getClaimValue("role").asInstanceOf[String])
val googleId = jwtClaims.getClaimValue("googleId", classOf[String])

val user = new User(userId, userName, email, null, googleId, null, role, null, null)
val user = new User(userId, userName, email, null, googleId, null, role, null, null, null)
Optional.of(new SessionUser(user))
} catch {
case _: UnresolvableKeyException =>
Expand Down
37 changes: 36 additions & 1 deletion frontend/src/app/common/service/user/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
*/

import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { AppSettings } from "../../app-setting";
import { Observable, of, ReplaySubject } from "rxjs";
import { Role, User } from "../../type/user";
import { AuthService } from "./auth.service";
Expand All @@ -39,7 +41,8 @@ export class UserService {

constructor(
private authService: AuthService,
private config: GuiConfigService
private config: GuiConfigService,
private http: HttpClient
) {
const user = this.authService.loginWithExistingToken();
this.changeUser(user);
Expand Down Expand Up @@ -82,6 +85,38 @@ export class UserService {
.pipe(map(({ accessToken }) => this.handleAccessToken(accessToken)));
}

/**
* Retrieves affiliation from backend and return if affiliation has been prompted
* true: already prompted
* false: never prompted
*/
public checkAffiliation(): Observable<Boolean> {
const user = this.currentUser;
if (!user) {
return of(false);
}
return this.http.get<Boolean>(`${AppSettings.getApiEndpoint()}/user/affiliation`, {
params: { uid: user.uid.toString() },
});
}

/**
* updates a new registered user's affiliation
* @param affiliation
*/
public updateAffiliation(affiliation: string): Observable<void> {
const user = this.currentUser;

if (!user) {
return of(void 0);
}

return this.http.put<void>(`${AppSettings.getApiEndpoint()}/user/affiliation`, {
uid: user.uid,
affiliation: affiliation,
});
}

/**
* changes the current user and triggers currentUserSubject
* @param user
Expand Down
1 change: 1 addition & 0 deletions frontend/src/app/common/type/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export interface User
comment: string;
lastLogin?: number;
accountCreation?: Second;
affiliation?: string;
}> {}

export interface File
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
nzType="search"></span>
</nz-filter-trigger>
</th>
<th>Affiliation</th>
<th
[nzSortFn]="sortByComment"
[nzSortDirections]="['ascend', 'descend']"
Expand Down Expand Up @@ -230,6 +231,7 @@
</ng-template>
</div>
</td>
<td>{{ user.affiliation }}</td>
<td>
<div (focusout)="saveEdit()">
<ng-container *ngIf="editUid !== user.uid || editAttribute !== 'comment'; else editCommentTemplate">
Expand Down
32 changes: 32 additions & 0 deletions frontend/src/app/dashboard/component/dashboard.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -213,4 +213,36 @@
</nz-content>
</nz-layout>
</div>
<nz-modal
[(nzVisible)]="affiliationModalVisible"
[nzMaskClosable]="true"
[nzClosable]="true"
(nzOnCancel)="onAffiliationCancel()"
nzTitle="Tell us your affiliation">
<ng-container *nzModalContent>
<p>
To help us understand our users better, please tell us your affiliation (for example, your university, company,
or organization).
</p>
<input
nz-input
[(ngModel)]="affiliationInput"
placeholder="e.g. UC Irvine" />
</ng-container>

<ng-container *nzModalFooter>
<button
nz-button
(click)="skipAffiliation()">
Skip
</button>
<button
nz-button
nzType="primary"
[nzLoading]="affiliationSaving"
(click)="saveAffiliation()">
Save
</button>
</ng-container>
</nz-modal>
</nz-layout>
69 changes: 68 additions & 1 deletion frontend/src/app/dashboard/component/dashboard.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
} from "../../app-routing.constant";
import { Version } from "../../../environments/version";
import { SidebarTabs } from "../../common/type/gui-config";
import { User } from "../../common/type/user";

@Component({
selector: "texera-dashboard",
Expand Down Expand Up @@ -74,6 +75,10 @@ export class DashboardComponent implements OnInit {
forum_enabled: false,
about_enabled: false,
};
// Variables related to updating user's affiliation
affiliationModalVisible = false;
affiliationInput: string = "";
affiliationSaving = false;

protected readonly DASHBOARD_USER_PROJECT = DASHBOARD_USER_PROJECT;
protected readonly DASHBOARD_USER_WORKFLOW = DASHBOARD_USER_WORKFLOW;
Expand Down Expand Up @@ -114,11 +119,12 @@ export class DashboardComponent implements OnInit {
this.userService
.userChanged()
.pipe(untilDestroyed(this))
.subscribe(() => {
.subscribe(user => {
this.ngZone.run(() => {
this.isLogin = this.userService.isLogin();
this.isAdmin = this.userService.isAdmin();
this.forumLogin();
this.checkAffiliationPrompt(user);
this.cdr.detectChanges();
});
});
Expand Down Expand Up @@ -194,6 +200,67 @@ export class DashboardComponent implements OnInit {
}
}

/**
* Prompts user to enter affiliation if they have not been prompted before
* @param user
*/
checkAffiliationPrompt(user: User | undefined): void {
// Null affiliation = never prompted before
if (!user || !this.config.env.googleLogin) {
return;
}

this.userService
.checkAffiliation()
.pipe(untilDestroyed(this))
.subscribe(response => {
if (response) {
this.affiliationInput = "";
this.affiliationModalVisible = true;
} else {
this.affiliationModalVisible = false;
}
});
}

/**
* Saves the affiliation
*/
saveAffiliation(): void {
const value = this.affiliationInput?.trim() ?? "";
this.affiliationSaving = true;

this.userService
.updateAffiliation(value)
.pipe(untilDestroyed(this))
.subscribe({
next: () => {
this.affiliationSaving = false;
this.affiliationModalVisible = false;
},
error: () => {
this.affiliationSaving = false;
this.affiliationModalVisible = false;
},
});
}

/**
* Skips the affiliation input and update the database to store an empty string, which means the user has
* already been prompted.
*/
skipAffiliation(): void {
this.affiliationInput = "";
this.saveAffiliation();
}

/**
* Skips the affiliation input when user closed the prompt window via outside click, ESC
*/
onAffiliationCancel(): void {
this.skipAffiliation();
}

checkRoute() {
const currentRoute = this.router.url;
this.displayNavbar = this.isNavbarEnabled(currentRoute);
Expand Down
1 change: 1 addition & 0 deletions sql/texera_ddl.sql
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ CREATE TABLE IF NOT EXISTS "user"
role user_role_enum NOT NULL DEFAULT 'INACTIVE',
comment TEXT,
account_creation_time TIMESTAMPTZ NOT NULL DEFAULT now(),
affiliation VARCHAR(128),
-- check that either password or google_id is not null
CONSTRAINT ck_nulltest CHECK ((password IS NOT NULL) OR (google_id IS NOT NULL))
);
Expand Down
Loading