Skip to content
Merged
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
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
125 changes: 125 additions & 0 deletions src/main/java/teammates/storage/entity/MagicLink.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
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 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)
private String email;

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

@Column(nullable = false)
private Instant expiresAt;

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;
}

/**
* 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() + "]";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?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>
</createTable>
<addUniqueConstraint columnNames="token_hash" constraintName="UC_MAGIC_LINKSTOKEN_HASH_COL" tableName="magic_links"/>
</changeSet>
</databaseChangeLog>
Loading