Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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
1 change: 1 addition & 0 deletions src/main/java/teammates/common/util/Const.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public final class Const {
public static final Duration FEEDBACK_SESSION_EVENT_EMAIL_LOOKBACK_WINDOW = Duration.ofDays(2);
public static final Duration LOGS_RETENTION_PERIOD = Duration.ofDays(30);
public static final Duration COOKIE_VALIDITY_PERIOD = Duration.ofDays(7);
public static final Duration MAGIC_LINK_VALIDITY_PERIOD = Duration.ofMinutes(5);

public static final int SEARCH_QUERY_SIZE_LIMIT = 50;

Expand Down
138 changes: 138 additions & 0 deletions src/main/java/teammates/storage/entity/MagicLink.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package teammates.storage.entity;

import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

import org.hibernate.annotations.UpdateTimestamp;

import teammates.common.util.Const;
import teammates.common.util.FieldValidator;
import teammates.common.util.SanitizationHelper;

/**
* Represents a passwordless login magic link.
*/
@Entity
@Table(name = "MagicLinks")
public class MagicLink extends BaseEntity {

@Id
private UUID id;

@Column(nullable = false, unique = true)
private String email;
Comment thread
TobyCyan marked this conversation as resolved.
Outdated

@Column(nullable = false, unique = true)
private String tokenHash;

@Column(nullable = false)
private Instant expiresAt;

@UpdateTimestamp
private Instant updatedAt;
Comment thread
TobyCyan marked this conversation as resolved.
Outdated

protected MagicLink() {
// required by Hibernate
}

public MagicLink(String email, String tokenHash, Instant now) {
this.setId(UUID.randomUUID());
this.setEmail(email);
this.setTokenHash(tokenHash);
this.setExpiresAt(now.plus(Const.MAGIC_LINK_VALIDITY_PERIOD));
}

@Override
public List<String> getInvalidityInfo() {
List<String> errors = new ArrayList<>();

addNonEmptyError(FieldValidator.getInvalidityInfoForEmail(email), errors);

return errors;
}

public UUID getId() {
return id;
}

public void setId(UUID id) {
this.id = id;
}

public String getEmail() {
return email;
}

public void setEmail(String email) {
this.email = SanitizationHelper.sanitizeEmail(email);
}

public String getTokenHash() {
return tokenHash;
}

public void setTokenHash(String tokenHash) {
this.tokenHash = SanitizationHelper.sanitizeTextField(tokenHash);
}

public Instant getExpiresAt() {
return expiresAt;
}

public void setExpiresAt(Instant expiresAt) {
this.expiresAt = expiresAt;
}

public Instant getUpdatedAt() {
return updatedAt;
}

public void setUpdatedAt(Instant updatedAt) {
this.updatedAt = updatedAt;
}

/**
* Returns true if the magic link expires at or before {@code now}.
*/
public boolean isExpired(Instant now) {
return !expiresAt.isAfter(now);
}

/**
* Returns true if the magic link can still be used.
*/
public boolean isUsable(Instant now) {
return !isExpired(now);
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}

if (!(o instanceof MagicLink other)) {
return false;
}

return getId() != null && getId().equals(other.getId());
}

@Override
public int hashCode() {
return getClass().hashCode();
}

@Override
public String toString() {
return "MagicLink [id=" + id + ", email=" + email + ", expiresAt=" + expiresAt
+ ", createdAt=" + getCreatedAt() + ", updatedAt=" + updatedAt + "]";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
<changeSet author="TobyCyan" id="1784084184512-1">
<createTable tableName="magic_links">
<column name="id" type="UUID">
<constraints nullable="false" primaryKey="true" primaryKeyName="magic_linksPK"/>
</column>
<column name="created_at" type="TIMESTAMP(6) WITH TIME ZONE"/>
<column name="email" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
<column name="expires_at" type="TIMESTAMP(6) WITH TIME ZONE">
<constraints nullable="false"/>
</column>
<column name="token_hash" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
<column name="updated_at" type="TIMESTAMP(6) WITH TIME ZONE"/>
</createTable>
<addUniqueConstraint columnNames="token_hash" constraintName="UC_MAGIC_LINKSTOKEN_HASH_COL" tableName="magic_links"/>
<addUniqueConstraint columnNames="email" constraintName="UK6gxypheo8erwn0q8svo6osc1w" tableName="magic_links"/>
</changeSet>
</databaseChangeLog>
Loading