Skip to content

Commit

Permalink
progress
Browse files Browse the repository at this point in the history
  • Loading branch information
nk-coding committed Jul 17, 2024
1 parent 14350a5 commit b3caf96
Show file tree
Hide file tree
Showing 11 changed files with 128 additions and 88 deletions.
6 changes: 3 additions & 3 deletions backend/src/api-internal/auth-redirect.middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,9 @@ export class AuthRedirectMiddleware extends StateMiddleware<
const suggestions = await this.getDataSuggestions(userLoginData, state.strategy);
const suggestionQuery = `&email=${encodeURIComponent(
suggestions.email ?? "",
)}&username=${encodeURIComponent(
suggestions.username ?? "",
)}&displayName=${encodeURIComponent(suggestions.displayName ?? "")}`;
)}&username=${encodeURIComponent(suggestions.username ?? "")}&displayName=${encodeURIComponent(
suggestions.displayName ?? "",
)}&forceSuggestedUsername=${state.strategy.forceSuggestedUsername}`;
const url = `/auth/flow/register?code=${token}&state=${encodedState}` + suggestionQuery;
res.redirect(url);
} else {
Expand Down
44 changes: 44 additions & 0 deletions backend/src/api-login/dto/user-login-data.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { StrategyInstance } from "src/model/postgres/StrategyInstance.entity";
import { LoginState } from "src/model/postgres/UserLoginData.entity";

export class UserLoginDataResponse {
/**
* The unique ID of this login data
*
* @example 12345678-90ab-cdef-fedc-ab0987654321
*/
id: string;

/**
* The state this authentication is in.
*
* Rules:
* - Only UserLoginData in state {@link LoginState.VALID} may be used for login and retrieving an access token.
* - Only UserLoginData in state {@link LoginState.WAITING_FOR_REGISTER} may be used for registration or linking.
* - UserLoginData in state {@link LoginState.BLOCKED} cannot be used for anything
*
* @example "VALID"
*/
state: LoginState

/**
* If not `null`, this authentication should be considered *invalid* on any date+time AFTER this.
* This is to ensure created UserLoginData, that are not used for registration
* or linking in time, are not kept forever.
*
* If `null`, the authentication should not expire by date.
*/
expires: Date | null

/**
* The strategy instance this authentication uses.
*
* For example a UserLoginData containing a password would reference a strategy instance of type userpass
*/
strategyInstance: StrategyInstance

/**
* A description of the authentication
*/
description: string
}
83 changes: 26 additions & 57 deletions backend/src/api-login/users.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import { OpenApiTag } from "src/openapi-tag";
import { ApiStateData } from "./ApiStateData";
import { CheckAccessTokenGuard, NeedsAdmin } from "./check-access-token.guard";
import { CreateUserAsAdminInput } from "./dto/user-inputs.dto";
import { UserLoginDataResponse } from "./dto/user-login-data.dto";
import { StrategiesService } from "src/model/services/strategies.service";

/**
* Controller allowing access to the users in the system
Expand All @@ -46,6 +48,7 @@ export class UsersController {
private readonly userService: LoginUserService,
private readonly backendUserSerice: BackendUserService,
private readonly loginDataSerive: UserLoginDataService,
private readonly strategiesService: StrategiesService,
) {}

/**
Expand Down Expand Up @@ -140,57 +143,6 @@ export class UsersController {
return this.backendUserSerice.createNewUser(input, input.isAdmin);
}

/**
* **NOTE**: Not implemented yet. Will always fail.
*
* Updates an existing user object using the given data.
* Only the entries that are given in the input will be updated in the user.
*
* Needs Admin permissions
*
* @param id The uuid string of the existing user to edit
* @param input If sucessful, the updated user object
*/
@Put(":id")
@ApiOperation({ summary: "NOT IMPLEMENTED! Update an existing user object" })
@ApiParam({ name: "id", type: String, format: "uuid", description: "The uuid string of the existing user to edit" })
@ApiOkResponse({ type: LoginUser, description: "If sucessful, the updated user object" })
@ApiNotFoundResponse({ description: "If no user with the given id could be found" })
@NeedsAdmin()
async editUser(@Param("id") id: string, @Body() input: Partial<CreateUserAsAdminInput>): Promise<LoginUser> {
throw new HttpException(
"Needs to be discussed with backend who stores what and what changes where",
HttpStatus.NOT_IMPLEMENTED,
);
const user = await this.userService.findOneBy({ id });
if (!user) {
throw new HttpException("User with given id not found", HttpStatus.NOT_FOUND);
}
}

/**
* **NOTE**: Not implemented yet. Will always fail.
*
* Permanently deletes an existing user by its id.
*
* Needs Admin permissions
*
* @param id The uuid string of the existing user to delete
* @param input The default response with operation 'delete-user'
*/
@Delete(":id")
@ApiOperation({ summary: "NOT IMPLEMENTED! Update an existing user object" })
@ApiParam({ name: "id", type: String, format: "uuid", description: "The uuid string of the existing user to edit" })
@ApiOkResponse({ type: LoginUser, description: "If sucessful, the updated user object" })
@ApiNotFoundResponse({ description: "If no user with the given id could be found" })
async deleteUser(@Param("id") id: string): Promise<DefaultReturn> {
throw new HttpException(
"I'm not supposed to say wht I think. But why this isn't yet implemented is difficult to explain.",
HttpStatus.NOT_IMPLEMENTED,
);
return new DefaultReturn("delete-user");
}

/**
* Gets the list of all login data of a single user specified by id.
*
Expand Down Expand Up @@ -218,7 +170,7 @@ export class UsersController {
async getLoginDataForUser(
@Param("id") id: string,
@Res({ passthrough: true }) res: Response,
): Promise<UserLoginData[]> {
): Promise<UserLoginDataResponse[]> {
if (!id) {
throw new HttpException("id must be given", HttpStatus.BAD_REQUEST);
}
Expand All @@ -234,10 +186,27 @@ export class UsersController {
);
}
}
return this.loginDataSerive.findBy({
user: {
id: loggedInUser.id,
},
});
return Promise.all(
(
await this.loginDataSerive.find({
where: {
user: {
id,
},
},
relations: ["strategyInstance"],
})
).map(async (loginData) => {
const instance = await loginData.strategyInstance;
const strategy = this.strategiesService.getStrategyByName(instance.type);
return {
id: loginData.id,
state: loginData.state,
expires: loginData.expires,
strategyInstance: instance,
description: await strategy.getLoginDataDescription(loginData),
} satisfies UserLoginDataResponse;
}),
);
}
}
27 changes: 13 additions & 14 deletions backend/src/model/services/user-login-data.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ import { UserLoginData } from "../postgres/UserLoginData.entity";

@Injectable()
export class UserLoginDataService extends Repository<UserLoginData> {
constructor(
private dataSource: DataSource,
) {
constructor(private dataSource: DataSource) {
super(UserLoginData, dataSource.createEntityManager());
}

Expand All @@ -26,20 +24,21 @@ export class UserLoginDataService extends Repository<UserLoginData> {
.getMany();
}

/**
* Finds all login data entities that have a user assigned to them with the given username
* and the id of which are contained in the given set of ids
*
* @param username The username thet the user of the login data is required to have
* @param loginDataIds The set of login data ids from which the login datas to return have to be
* @returns A lit of login datas that are from the given set and have the given username
*/
public async findForUsernameOutOfSet(username: string, loginDataIds: string[]): Promise<UserLoginData[]> {
public async findForStrategyAndUsernameWithDataContaining(
strategyInstance: StrategyInstance,
data: object,
username: string,
): Promise<UserLoginData[]> {
return this.createQueryBuilder("loginData")
.leftJoinAndSelect(`loginData.user`, "user")
.where(`user.username = :username`, { username })
.andWhereInIds(loginDataIds)
.andWhere(`"strategyInstanceId" = :instanceId`, {
instanceId: strategyInstance.id,
})
.andWhere(`(("expires" is null) or ("expires" >= :dateNow))`, {
dateNow: new Date(),
})
.andWhere(`"data" @> :data`, { data })
.getMany();
}

}
11 changes: 11 additions & 0 deletions backend/src/strategies/Strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export abstract class Strategy {
public readonly canSync: boolean = false,
public readonly needsRedirectFlow = false,
public readonly allowsImplicitSignup = false,
public readonly forceSuggestedUsername = false,
) {
strategiesService.addStrategy(typeName, this);
}
Expand Down Expand Up @@ -196,6 +197,16 @@ export abstract class Strategy {
return null;
}

/**
* Gets a description of the login data, e.g. a username or email.
*
* @param loginData The login data for which to get the description
* @returns A description of the login data
*/
async getLoginDataDescription(loginData: UserLoginData): Promise<string> {
return "";
}

/**
* Does the opposite of `getImsUserTemplatedValuesForLoginData`.
*
Expand Down
2 changes: 2 additions & 0 deletions backend/src/strategies/StrategyUsingPassport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export abstract class StrategyUsingPassport extends Strategy {
canSync = false,
needsRedirectFlow = false,
allowsImplicitSignup = false,
forceSuggestedUsername = false,
) {
super(
typeName,
Expand All @@ -28,6 +29,7 @@ export abstract class StrategyUsingPassport extends Strategy {
canSync,
needsRedirectFlow,
allowsImplicitSignup,
forceSuggestedUsername,
);
}

Expand Down
4 changes: 4 additions & 0 deletions backend/src/strategies/github/github.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,4 +205,8 @@ export class GithubStrategyService extends StrategyUsingPassport {
this.passportUserCallback.bind(this, strategyInstance),
);
}

override async getLoginDataDescription(loginData: UserLoginData): Promise<string> {
return loginData.data?.username
}
}
3 changes: 3 additions & 0 deletions backend/src/strategies/perform-auth-function.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@ export class PerformAuthFunctionService {
const authFunction = state.authState.function;
const wantsToDoImplicitRegister =
strategy.allowsImplicitSignup && instance.doesImplicitRegister && authFunction == AuthFunction.LOGIN;
if (authFunction != AuthFunction.LOGIN && !authResult.mayRegister) {
throw new AuthException("Cannot register", instance.id);
}
if (authResult.loginData) {
// sucessfully found login data matching the authentication
if (authResult.loginData.expires != null && authResult.loginData.expires <= new Date()) {
Expand Down
2 changes: 1 addition & 1 deletion backend/src/strategies/strategies.middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export class StrategiesMiddleware extends StateMiddleware<
): Promise<any> {
const id = req.params.id;
const instance = await this.idToStrategyInstance(id);
const strategy = await this.strategiesService.getStrategyByName(instance.type);
const strategy = this.strategiesService.getStrategyByName(instance.type);
this.appendState(res, { strategy });

const functionError = this.performAuthFunctionService.checkFunctionIsAllowed(state, instance, strategy);
Expand Down
27 changes: 16 additions & 11 deletions backend/src/strategies/userpass/userpass.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export class UserpassStrategyService extends StrategyUsingPassport {
@Inject("StateJwtService")
stateJwtService: JwtService,
) {
super("userpass", strategyInstanceService, strategiesService, stateJwtService, true, false, false, false);
super("userpass", strategyInstanceService, strategiesService, stateJwtService, true, false, false, false, true);
}

override get acceptsVariables(): {
Expand Down Expand Up @@ -75,18 +75,23 @@ export class UserpassStrategyService extends StrategyUsingPassport {
}

const dataActiveLogin = {};
const loginDataCandidates = await this.loginDataService.findForStrategyWithDataContaining(strategyInstance, {});
const loginDataForCorrectUser = await this.loginDataService.findForUsernameOutOfSet(
const loginDataForCorrectUser = await this.loginDataService.findForStrategyAndUsernameWithDataContaining(
strategyInstance,
{},
username || "",
loginDataCandidates.map((candidate) => candidate.id),
);

if (loginDataForCorrectUser.length == 0) {
const dataUserLoginData = await this.generateLoginDataData(username, password);
return done(
null,
{ dataActiveLogin, dataUserLoginData, mayRegister: true, noRegisterMessage: "Username or password incorrect" },
{ },
{
dataActiveLogin,
dataUserLoginData,
mayRegister: true,
noRegisterMessage: "Username or password incorrect",
},
{},
);
} else if (loginDataForCorrectUser.length > 1) {
return done("More than one user with same username", false, undefined);
Expand All @@ -96,11 +101,7 @@ export class UserpassStrategyService extends StrategyUsingPassport {
const hasCorrectPassword = await bcrypt.compare(password, loginData.data["password"]);

if (!hasCorrectPassword) {
return done(
null,
false,
{ message: "Username or password incorrect" },
);
return done(null, false, { message: "Username or password incorrect" });
}

return done(null, { loginData, dataActiveLogin, dataUserLoginData: {}, mayRegister: false }, {});
Expand All @@ -121,4 +122,8 @@ export class UserpassStrategyService extends StrategyUsingPassport {
email: loginData.data?.email || undefined,
};
}

override async getLoginDataDescription(loginData: UserLoginData): Promise<string> {
return loginData.data?.username
}
}
7 changes: 5 additions & 2 deletions frontend/src/views/Register.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@
<BaseLayout>
<template #content>
<GropiusCard class="register-container mt-5">
<v-card-title class="pl-0">Register</v-card-title>
<v-card-title class="pl-0">Complete registration</v-card-title>
<v-form class="mt-2" method="POST" action="/auth/api/internal/auth/register">
<v-text-field
v-model="username"
v-if="!forceSuggestedUsername"
name="username"
v-bind="usernameProps"
label="Username"
class="mb-1"
/>
<input v-else type="hidden" name="username" :value="username" />
<v-text-field
v-model="displayName"
name="displayName"
Expand Down Expand Up @@ -39,10 +41,11 @@ import * as yup from "yup";
import { useRoute } from "vue-router";
import axios from "axios";
import { fieldConfig } from "@/util/vuetifyFormConfig";
import { onMounted } from "vue";
import { computed, onMounted } from "vue";
import { asyncComputed } from "@vueuse/core";
const route = useRoute();
const forceSuggestedUsername = computed(() => route.query.forceSuggestedUsername == "true");
const schema = toTypedSchema(
yup.object().shape({
Expand Down

0 comments on commit b3caf96

Please sign in to comment.